Simplify app to certificate recognition and MfApi restart only.
Remove XApi, WinRM, LocalCmd, HTTP API, TLS probe, SQL deploy model, and deploy UI. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,167 +0,0 @@
|
|||||||
using Microsoft.Data.SqlClient;
|
|
||||||
using ZA.CoreService.ESBCertificateManager.Models;
|
|
||||||
|
|
||||||
namespace ZA.CoreService.ESBCertificateManager.Data;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Schreibt Deployment-Lauf-Ergebnisse in die SQL-Datenbank.
|
|
||||||
/// Tabellen: dbo.DeploymentRuns (GUID-PK) + dbo.DeploymentTargetResults.
|
|
||||||
/// Ist kein ConnectionString konfiguriert, werden alle Operationen still übersprungen.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class SqlRunLogger
|
|
||||||
{
|
|
||||||
private readonly string? _connectionString;
|
|
||||||
|
|
||||||
public SqlRunLogger(string? connectionString)
|
|
||||||
{
|
|
||||||
_connectionString = string.IsNullOrWhiteSpace(connectionString)
|
|
||||||
? null
|
|
||||||
: connectionString;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsEnabled => _connectionString is not null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Legt einen neuen Lauf-Datensatz an.
|
|
||||||
/// </summary>
|
|
||||||
public async Task BeginRunAsync(
|
|
||||||
Guid runId,
|
|
||||||
DateTimeOffset startedAt,
|
|
||||||
string certificateFilePath,
|
|
||||||
string certificateFingerprint,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (_connectionString is null) return;
|
|
||||||
|
|
||||||
const string sql = """
|
|
||||||
INSERT INTO dbo.DeploymentRuns
|
|
||||||
(Id, StartedAtUtc, SourceFile, SourceFingerprint, StartedBy, MachineName, OverallStatus)
|
|
||||||
VALUES
|
|
||||||
(@Id, @StartedAtUtc, @SourceFile, @SourceFingerprint, @StartedBy, @MachineName, N'Running');
|
|
||||||
""";
|
|
||||||
|
|
||||||
await using SqlConnection conn = new(_connectionString);
|
|
||||||
await conn.OpenAsync(cancellationToken);
|
|
||||||
await using SqlCommand cmd = new(sql, conn);
|
|
||||||
|
|
||||||
cmd.Parameters.AddWithValue("@Id", runId);
|
|
||||||
cmd.Parameters.AddWithValue("@StartedAtUtc", startedAt.UtcDateTime);
|
|
||||||
cmd.Parameters.AddWithValue("@SourceFile", Path.GetFileName(certificateFilePath));
|
|
||||||
cmd.Parameters.AddWithValue("@SourceFingerprint", certificateFingerprint);
|
|
||||||
cmd.Parameters.AddWithValue("@StartedBy", Environment.UserName);
|
|
||||||
cmd.Parameters.AddWithValue("@MachineName", Environment.MachineName);
|
|
||||||
|
|
||||||
await cmd.ExecuteNonQueryAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Aktualisiert den Lauf-Datensatz mit Endzeitpunkt und Gesamtstatus.
|
|
||||||
/// </summary>
|
|
||||||
public async Task CompleteRunAsync(
|
|
||||||
Guid runId,
|
|
||||||
DateTimeOffset finishedAt,
|
|
||||||
bool overallSuccess,
|
|
||||||
int successCount,
|
|
||||||
int totalCount,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (_connectionString is null) return;
|
|
||||||
|
|
||||||
string status = overallSuccess
|
|
||||||
? "Success"
|
|
||||||
: (successCount > 0 ? "PartialFailure" : "Failure");
|
|
||||||
|
|
||||||
const string sql = """
|
|
||||||
UPDATE dbo.DeploymentRuns
|
|
||||||
SET FinishedAtUtc = @FinishedAtUtc,
|
|
||||||
OverallStatus = @OverallStatus
|
|
||||||
WHERE Id = @Id;
|
|
||||||
""";
|
|
||||||
|
|
||||||
await using SqlConnection conn = new(_connectionString);
|
|
||||||
await conn.OpenAsync(cancellationToken);
|
|
||||||
await using SqlCommand cmd = new(sql, conn);
|
|
||||||
|
|
||||||
cmd.Parameters.AddWithValue("@Id", runId);
|
|
||||||
cmd.Parameters.AddWithValue("@FinishedAtUtc", finishedAt.UtcDateTime);
|
|
||||||
cmd.Parameters.AddWithValue("@OverallStatus", status);
|
|
||||||
|
|
||||||
await cmd.ExecuteNonQueryAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Schreibt das Ergebnis eines einzelnen Deployment-Ziels.
|
|
||||||
/// </summary>
|
|
||||||
public async Task WriteTargetResultAsync(
|
|
||||||
Guid runId,
|
|
||||||
TargetStepResult step,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (_connectionString is null) return;
|
|
||||||
|
|
||||||
string status = step.Success ? "Success" : "Failure";
|
|
||||||
|
|
||||||
const string sql = """
|
|
||||||
INSERT INTO dbo.DeploymentTargetResults
|
|
||||||
(DeploymentRunId, TargetId, TargetName,
|
|
||||||
StartedAtUtc, FinishedAtUtc,
|
|
||||||
CopySucceeded, RestartSucceeded, TlsSucceeded,
|
|
||||||
ObservedFingerprint, Status, ErrorMessage)
|
|
||||||
VALUES
|
|
||||||
(@RunId, @TargetId, @TargetName,
|
|
||||||
@StartedAtUtc, @FinishedAtUtc,
|
|
||||||
@CopySucceeded, @RestartSucceeded, @TlsSucceeded,
|
|
||||||
@ObservedFingerprint, @Status, @ErrorMessage);
|
|
||||||
""";
|
|
||||||
|
|
||||||
await using SqlConnection conn = new(_connectionString);
|
|
||||||
await conn.OpenAsync(cancellationToken);
|
|
||||||
await using SqlCommand cmd = new(sql, conn);
|
|
||||||
|
|
||||||
cmd.Parameters.AddWithValue("@RunId", runId);
|
|
||||||
cmd.Parameters.AddWithValue("@TargetId", step.TargetId);
|
|
||||||
cmd.Parameters.AddWithValue("@TargetName", step.TargetName);
|
|
||||||
cmd.Parameters.AddWithValue("@StartedAtUtc", step.StartedAt.UtcDateTime);
|
|
||||||
cmd.Parameters.AddWithValue("@FinishedAtUtc", step.FinishedAt.UtcDateTime);
|
|
||||||
cmd.Parameters.AddWithValue("@CopySucceeded", step.CopySucceeded);
|
|
||||||
cmd.Parameters.AddWithValue("@RestartSucceeded", step.RestartSucceeded);
|
|
||||||
cmd.Parameters.AddWithValue("@TlsSucceeded", step.TlsSucceeded);
|
|
||||||
cmd.Parameters.AddWithValue("@ObservedFingerprint", (object?)step.ObservedFingerprint ?? DBNull.Value);
|
|
||||||
cmd.Parameters.AddWithValue("@Status", status);
|
|
||||||
cmd.Parameters.AddWithValue("@ErrorMessage", step.Success ? DBNull.Value : (object?)(step.Detail ?? step.StatusText));
|
|
||||||
|
|
||||||
await cmd.ExecuteNonQueryAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Persistiert ein vollständiges <see cref="DeploymentRunResult"/> in einem einzigen Aufruf.
|
|
||||||
/// </summary>
|
|
||||||
public async Task PersistRunResultAsync(
|
|
||||||
DeploymentRunResult runResult,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (_connectionString is null) return;
|
|
||||||
|
|
||||||
await BeginRunAsync(
|
|
||||||
runResult.RunId,
|
|
||||||
runResult.StartedAt,
|
|
||||||
runResult.CertificateFilePath,
|
|
||||||
runResult.CertificateFingerprint,
|
|
||||||
cancellationToken);
|
|
||||||
|
|
||||||
foreach (TargetStepResult step in runResult.TargetResults)
|
|
||||||
{
|
|
||||||
await WriteTargetResultAsync(runResult.RunId, step, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
int successCount = runResult.TargetResults.Count(r => r.Success);
|
|
||||||
|
|
||||||
await CompleteRunAsync(
|
|
||||||
runResult.RunId,
|
|
||||||
runResult.FinishedAt,
|
|
||||||
runResult.OverallSuccess,
|
|
||||||
successCount,
|
|
||||||
runResult.TargetResults.Count,
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
using Microsoft.Data.SqlClient;
|
|
||||||
using ZA.CoreService.ESBCertificateManager.Models;
|
|
||||||
|
|
||||||
namespace ZA.CoreService.ESBCertificateManager.Data;
|
|
||||||
|
|
||||||
public sealed class SqlTargetRepository : ITargetRepository
|
|
||||||
{
|
|
||||||
private readonly string _connectionString;
|
|
||||||
|
|
||||||
public SqlTargetRepository(string connectionString)
|
|
||||||
{
|
|
||||||
_connectionString = connectionString;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string SourceDescription => "SQL Server (dbo.DeploymentTargets)";
|
|
||||||
|
|
||||||
public async Task<IReadOnlyList<DeploymentTarget>> GetActiveTargetsAsync(
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(_connectionString))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Es ist keine SQL-Verbindungszeichenfolge konfiguriert.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const string sql = """
|
|
||||||
SELECT
|
|
||||||
Id,
|
|
||||||
Name,
|
|
||||||
Environment,
|
|
||||||
IsActive,
|
|
||||||
CertificateTargetPath,
|
|
||||||
CertificateFileName,
|
|
||||||
ContainerName,
|
|
||||||
RestartType,
|
|
||||||
RestartCommand,
|
|
||||||
RestartArguments,
|
|
||||||
RestartTimeoutSeconds,
|
|
||||||
SonicConnectionName,
|
|
||||||
XapiSourcePath,
|
|
||||||
TlsHost,
|
|
||||||
TlsPort,
|
|
||||||
TlsServerName,
|
|
||||||
ExpectedFingerprint,
|
|
||||||
SortOrder
|
|
||||||
FROM dbo.DeploymentTargets
|
|
||||||
WHERE IsActive = 1
|
|
||||||
ORDER BY SortOrder, Name;
|
|
||||||
""";
|
|
||||||
|
|
||||||
List<DeploymentTarget> targets = [];
|
|
||||||
|
|
||||||
await using SqlConnection connection = new(_connectionString);
|
|
||||||
await connection.OpenAsync(cancellationToken);
|
|
||||||
|
|
||||||
await using SqlCommand command = new(sql, connection);
|
|
||||||
await using SqlDataReader reader = await command.ExecuteReaderAsync(cancellationToken);
|
|
||||||
|
|
||||||
int id = reader.GetOrdinal("Id");
|
|
||||||
int name = reader.GetOrdinal("Name");
|
|
||||||
int environment = reader.GetOrdinal("Environment");
|
|
||||||
int isActive = reader.GetOrdinal("IsActive");
|
|
||||||
int certificateTargetPath = reader.GetOrdinal("CertificateTargetPath");
|
|
||||||
int certificateFileName = reader.GetOrdinal("CertificateFileName");
|
|
||||||
int containerName = reader.GetOrdinal("ContainerName");
|
|
||||||
int restartType = reader.GetOrdinal("RestartType");
|
|
||||||
int restartCommand = reader.GetOrdinal("RestartCommand");
|
|
||||||
int restartArguments = reader.GetOrdinal("RestartArguments");
|
|
||||||
int restartTimeoutSeconds = reader.GetOrdinal("RestartTimeoutSeconds");
|
|
||||||
int sonicConnectionName = reader.GetOrdinal("SonicConnectionName");
|
|
||||||
int xapiSourcePath = reader.GetOrdinal("XapiSourcePath");
|
|
||||||
int tlsHost = reader.GetOrdinal("TlsHost");
|
|
||||||
int tlsPort = reader.GetOrdinal("TlsPort");
|
|
||||||
int tlsServerName = reader.GetOrdinal("TlsServerName");
|
|
||||||
int expectedFingerprint = reader.GetOrdinal("ExpectedFingerprint");
|
|
||||||
int sortOrder = reader.GetOrdinal("SortOrder");
|
|
||||||
|
|
||||||
while (await reader.ReadAsync(cancellationToken))
|
|
||||||
{
|
|
||||||
RestartType parsedRestartType = Enum.TryParse(
|
|
||||||
reader.GetString(restartType),
|
|
||||||
ignoreCase: true,
|
|
||||||
out RestartType parsed)
|
|
||||||
? parsed
|
|
||||||
: RestartType.None;
|
|
||||||
|
|
||||||
targets.Add(new DeploymentTarget
|
|
||||||
{
|
|
||||||
Id = reader.GetInt32(id),
|
|
||||||
Name = reader.GetString(name),
|
|
||||||
Environment = reader.GetString(environment),
|
|
||||||
IsActive = reader.GetBoolean(isActive),
|
|
||||||
TargetDirectory = reader.GetString(certificateTargetPath),
|
|
||||||
CertificateFileName = reader.GetString(certificateFileName),
|
|
||||||
ContainerName = reader.GetString(containerName),
|
|
||||||
RestartType = parsedRestartType,
|
|
||||||
RestartCommand = reader.IsDBNull(restartCommand) ? string.Empty : reader.GetString(restartCommand),
|
|
||||||
RestartArguments = reader.GetString(restartArguments),
|
|
||||||
RestartTimeoutSeconds = reader.GetInt32(restartTimeoutSeconds),
|
|
||||||
SonicConnectionName = reader.GetString(sonicConnectionName),
|
|
||||||
XapiSourcePath = reader.GetString(xapiSourcePath),
|
|
||||||
TlsHost = reader.GetString(tlsHost),
|
|
||||||
TlsPort = reader.IsDBNull(tlsPort) ? null : reader.GetInt32(tlsPort),
|
|
||||||
TlsServerName = reader.GetString(tlsServerName),
|
|
||||||
ExpectedFingerprint = reader.IsDBNull(expectedFingerprint) ? null : reader.GetString(expectedFingerprint),
|
|
||||||
SortOrder = reader.GetInt32(sortOrder)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return targets;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,34 +11,8 @@ public static class TargetRepositoryFactory
|
|||||||
string samplePath = Path.GetFullPath(
|
string samplePath = Path.GetFullPath(
|
||||||
Path.Combine(AppContext.BaseDirectory, settings.SampleTargetsPath));
|
Path.Combine(AppContext.BaseDirectory, settings.SampleTargetsPath));
|
||||||
|
|
||||||
if (settings.UseOfflineSampleData)
|
|
||||||
{
|
|
||||||
ITargetRepository jsonRepo = new JsonTargetRepository(samplePath);
|
ITargetRepository jsonRepo = new JsonTargetRepository(samplePath);
|
||||||
_ = await jsonRepo.GetActiveTargetsAsync(cancellationToken);
|
_ = await jsonRepo.GetActiveTargetsAsync(cancellationToken);
|
||||||
return (jsonRepo, $"Ziele geladen aus {jsonRepo.SourceDescription}.");
|
return (jsonRepo, $"Ziele geladen aus {jsonRepo.SourceDescription}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(settings.ConnectionString))
|
|
||||||
{
|
|
||||||
return OfflineSample(samplePath, "Keine SQL-Verbindung konfiguriert – Offline-Sample wird verwendet.");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
SqlTargetRepository sqlRepo = new(settings.ConnectionString);
|
|
||||||
_ = await sqlRepo.GetActiveTargetsAsync(cancellationToken);
|
|
||||||
return (sqlRepo, $"Ziele geladen aus {sqlRepo.SourceDescription}.");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return OfflineSample(
|
|
||||||
samplePath,
|
|
||||||
$"SQL nicht erreichbar ({ex.Message}). Offline-Sample wird verwendet.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (ITargetRepository Repository, string LoadMessage) OfflineSample(
|
|
||||||
string samplePath,
|
|
||||||
string message)
|
|
||||||
=> (new JsonTargetRepository(samplePath), message);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,39 +4,8 @@
|
|||||||
"Name": "DE-Test / ct-ZADBService",
|
"Name": "DE-Test / ct-ZADBService",
|
||||||
"Environment": "TEST",
|
"Environment": "TEST",
|
||||||
"IsActive": true,
|
"IsActive": true,
|
||||||
"TargetDirectory": "DeploySandbox/target-esb",
|
|
||||||
"CertificateFileName": "esb-cert.cer",
|
|
||||||
"ContainerName": "ct-ZADBService",
|
"ContainerName": "ct-ZADBService",
|
||||||
"RestartType": "SonicContainer",
|
|
||||||
"RestartCommand": "",
|
|
||||||
"RestartArguments": "",
|
|
||||||
"RestartTimeoutSeconds": 120,
|
|
||||||
"SonicConnectionName": "DE-Test",
|
"SonicConnectionName": "DE-Test",
|
||||||
"XapiSourcePath": "",
|
|
||||||
"TlsHost": "",
|
|
||||||
"TlsPort": null,
|
|
||||||
"TlsServerName": "",
|
|
||||||
"ExpectedFingerprint": null,
|
|
||||||
"SortOrder": 10
|
"SortOrder": 10
|
||||||
},
|
|
||||||
{
|
|
||||||
"Id": 2,
|
|
||||||
"Name": "Lokaler CMD-Test (Echo)",
|
|
||||||
"Environment": "DEV",
|
|
||||||
"IsActive": false,
|
|
||||||
"TargetDirectory": "DeploySandbox/target-cmd",
|
|
||||||
"CertificateFileName": "esb-cert.cer",
|
|
||||||
"ContainerName": "",
|
|
||||||
"RestartType": "Command",
|
|
||||||
"RestartCommand": "cmd.exe",
|
|
||||||
"RestartArguments": "/c echo Neustart-Simulation OK",
|
|
||||||
"RestartTimeoutSeconds": 15,
|
|
||||||
"SonicConnectionName": "",
|
|
||||||
"XapiSourcePath": "",
|
|
||||||
"TlsHost": "",
|
|
||||||
"TlsPort": null,
|
|
||||||
"TlsServerName": "",
|
|
||||||
"ExpectedFingerprint": null,
|
|
||||||
"SortOrder": 20
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,16 +2,8 @@ namespace ZA.CoreService.ESBCertificateManager.Models;
|
|||||||
|
|
||||||
public sealed class AppSettings
|
public sealed class AppSettings
|
||||||
{
|
{
|
||||||
public string ConnectionString { get; set; } = string.Empty;
|
|
||||||
public bool UseOfflineSampleData { get; set; } = true;
|
public bool UseOfflineSampleData { get; set; } = true;
|
||||||
public string SampleTargetsPath { get; set; } = "Data/targets.sample.json";
|
public string SampleTargetsPath { get; set; } = "Data/targets.sample.json";
|
||||||
public string LogDirectory { get; set; } = "Logs";
|
public string LogDirectory { get; set; } = "Logs";
|
||||||
public int TlsTimeoutSeconds { get; set; } = 8;
|
|
||||||
public int TlsRetryCount { get; set; } = 2;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verbindungskonfigurationen für Progress Sonic ESB Management Instanzen.
|
|
||||||
/// Jeder Eintrag entspricht einer Sonic-Domain (z.B. einer Umgebung oder Tochtergesellschaft).
|
|
||||||
/// </summary>
|
|
||||||
public List<SonicConnection> SonicConnections { get; set; } = [];
|
public List<SonicConnection> SonicConnections { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ public sealed class DeploymentRunResult
|
|||||||
public bool OverallSuccess => TargetResults.All(r => r.Success);
|
public bool OverallSuccess => TargetResults.All(r => r.Success);
|
||||||
public DateTimeOffset StartedAt { get; init; }
|
public DateTimeOffset StartedAt { get; init; }
|
||||||
public DateTimeOffset FinishedAt { get; init; }
|
public DateTimeOffset FinishedAt { get; init; }
|
||||||
public string CertificateFilePath { get; init; } = string.Empty;
|
|
||||||
public string CertificateFingerprint { get; init; } = string.Empty;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class TargetStepResult
|
public sealed class TargetStepResult
|
||||||
@@ -18,13 +16,8 @@ public sealed class TargetStepResult
|
|||||||
public bool Success { get; init; }
|
public bool Success { get; init; }
|
||||||
public required string StatusText { get; init; }
|
public required string StatusText { get; init; }
|
||||||
public string? Detail { get; init; }
|
public string? Detail { get; init; }
|
||||||
public IReadOnlyList<string> Steps { get; init; } = [];
|
|
||||||
public DateTimeOffset StartedAt { get; init; }
|
public DateTimeOffset StartedAt { get; init; }
|
||||||
public DateTimeOffset FinishedAt { get; init; }
|
public DateTimeOffset FinishedAt { get; init; }
|
||||||
public bool CopySucceeded { get; init; }
|
|
||||||
public bool RestartSucceeded { get; init; }
|
|
||||||
public bool TlsSucceeded { get; init; }
|
|
||||||
public string? ObservedFingerprint { get; init; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ValidationIssue
|
public sealed class ValidationIssue
|
||||||
@@ -38,3 +31,5 @@ public sealed class PreflightValidationResult
|
|||||||
public bool IsValid => Issues.Count == 0;
|
public bool IsValid => Issues.Count == 0;
|
||||||
public List<ValidationIssue> Issues { get; } = [];
|
public List<ValidationIssue> Issues { get; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public readonly record struct TargetProgressUpdate(int TargetId, string StatusText, bool? SuccessHint);
|
||||||
|
|||||||
@@ -4,64 +4,9 @@ public sealed class DeploymentTarget
|
|||||||
{
|
{
|
||||||
public int Id { get; init; }
|
public int Id { get; init; }
|
||||||
public required string Name { get; init; }
|
public required string Name { get; init; }
|
||||||
public required string Environment { get; init; }
|
public string Environment { get; init; } = "TEST";
|
||||||
public bool IsActive { get; init; } = true;
|
public bool IsActive { get; init; } = true;
|
||||||
public required string TargetDirectory { get; init; }
|
|
||||||
public required string CertificateFileName { get; init; }
|
|
||||||
|
|
||||||
/// <summary>Name des Sonic-ESB-Containers (z.B. "sonic-container-a").</summary>
|
|
||||||
public string ContainerName { get; init; } = string.Empty;
|
public string ContainerName { get; init; } = string.Empty;
|
||||||
|
|
||||||
public RestartType RestartType { get; init; } = RestartType.None;
|
|
||||||
|
|
||||||
// --- Command-basierter Neustart ---
|
|
||||||
public string RestartCommand { get; init; } = string.Empty;
|
|
||||||
public string RestartArguments { get; init; } = string.Empty;
|
|
||||||
public int RestartTimeoutSeconds { get; init; } = 60;
|
|
||||||
|
|
||||||
// --- Sonic-ESB-Management-Neustart ---
|
|
||||||
/// <summary>
|
|
||||||
/// Referenz auf den Namen einer <see cref="SonicConnection"/> in AppSettings.
|
|
||||||
/// Pflichtfeld bei RestartType = SonicContainer oder SonicContainerWithXapi.
|
|
||||||
/// </summary>
|
|
||||||
public string SonicConnectionName { get; init; } = string.Empty;
|
public string SonicConnectionName { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Pfad zur XApi-Ressourcendatei (.xml/.zip), die vor dem Neustart importiert wird.
|
|
||||||
/// Pflichtfeld bei RestartType = SonicContainerWithXapi.
|
|
||||||
/// </summary>
|
|
||||||
public string XapiSourcePath { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
// --- TLS-Probe nach Deployment ---
|
|
||||||
public string TlsHost { get; init; } = string.Empty;
|
|
||||||
public int? TlsPort { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Hostname, der im TLS-Handshake als ServerName (SNI) verwendet wird.
|
|
||||||
/// Wichtig wenn TlsHost eine IP-Adresse ist, das Zertifikat aber einen DNS-Namen trägt.
|
|
||||||
/// Ist leer, wird TlsHost als ServerName verwendet.
|
|
||||||
/// </summary>
|
|
||||||
public string TlsServerName { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Erwarteter SHA-256-Fingerprint des Zertifikats nach dem Deployment (ohne Trennzeichen).
|
|
||||||
/// Wenn gesetzt, schlägt der TLS-Probe fehl wenn der Fingerprint abweicht.
|
|
||||||
/// </summary>
|
|
||||||
public string? ExpectedFingerprint { get; init; }
|
|
||||||
|
|
||||||
public int SortOrder { get; init; }
|
public int SortOrder { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum RestartType
|
|
||||||
{
|
|
||||||
None = 0,
|
|
||||||
|
|
||||||
/// <summary>Neustart über einen lokalen Betriebssystem-Prozess (RestartCommand).</summary>
|
|
||||||
Command = 1,
|
|
||||||
|
|
||||||
/// <summary>Container-Neustart wie in der Sonic Management Console (stop/startcontainer bzw. SMC-API).</summary>
|
|
||||||
SonicContainer = 2,
|
|
||||||
|
|
||||||
/// <summary>XApi-Ressourcen importieren und danach Container neu starten (Sonic ESB).</summary>
|
|
||||||
SonicContainerWithXapi = 3
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,398 +1,22 @@
|
|||||||
namespace ZA.CoreService.ESBCertificateManager.Models;
|
namespace ZA.CoreService.ESBCertificateManager.Models;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Verbindungskonfiguration für eine Progress Sonic ESB Management Instanz.
|
/// SMC-/Domain-Manager-Verbindung für MfApi-Neustart.
|
||||||
/// Name = Verbindungs-Alias (hier oft gleich ContainerName, z.B. "DE-Test").
|
/// Name = Alias (z.B. DE-Test), ContainerName separat (z.B. ct-ZADBService).
|
||||||
/// DomainName = Sonic-Domain (z.B. "proalpha-test").
|
|
||||||
/// Container wird in KnownContainers / DeploymentTarget.ContainerName geführt (z.B. "DE-Test").
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class SonicConnection
|
public sealed class SonicConnection
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Alias der Verbindung; referenziert von <see cref="DeploymentTarget.SonicConnectionName"/>.
|
|
||||||
/// </summary>
|
|
||||||
public required string Name { get; init; }
|
public required string Name { get; init; }
|
||||||
|
|
||||||
/// <summary>Sonic-Domain, z.B. "proalpha-test" (nicht der Containername).</summary>
|
|
||||||
public required string DomainName { get; init; }
|
public required string DomainName { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Dieselbe Broker-/Management-URL wie in der Sonic Management Console (SMC),
|
|
||||||
/// z.B. "tcp://dekun-painwbdet:13070". Keine separate Domain-Console nötig.
|
|
||||||
/// </summary>
|
|
||||||
public required string ConnectionUrl { get; init; }
|
public required string ConnectionUrl { get; init; }
|
||||||
|
|
||||||
/// <summary>Login wie in der Sonic Management Console (nicht für WinRM).</summary>
|
|
||||||
public required string Username { get; init; }
|
public required string Username { get; init; }
|
||||||
|
|
||||||
/// <summary>Passwort wie in der Sonic Management Console (nicht für WinRM).</summary>
|
|
||||||
public required string Password { get; init; }
|
public required string Password { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
public string SonicHome { get; init; } = @"C:\DEV\MQ10.0";
|
||||||
/// Windows-Konto für WinRM (<c>Invoke-Command -Credential</c>).
|
|
||||||
/// Leer = aktueller Prozess-Benutzer ohne explizite Credentials.
|
|
||||||
/// Nicht mit <see cref="Username"/>/<see cref="Password"/> (Sonic SMC) verwechseln.
|
|
||||||
/// </summary>
|
|
||||||
public string WinRmUsername { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>Windows-Passwort für WinRM; nur relevant wenn <see cref="WinRmUsername"/> gesetzt ist.</summary>
|
|
||||||
public string WinRmPassword { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// MfApi = Sonic MF Management Runtime API über ConnectionUrl + SMC-Logins (Standard);
|
|
||||||
/// WinRm / LocalCmd = optionaler Fallback über stopcontainer/startcontainer;
|
|
||||||
/// HttpApi = REST (falls vorhanden).
|
|
||||||
/// </summary>
|
|
||||||
/// <summary>
|
|
||||||
/// Standard: MfApi = Management Application API (wie SMC):
|
|
||||||
/// JMSConnectorClient + MFProxyFactory.createAgentProxy + IAgentProxy.restart.
|
|
||||||
/// Laut CX-Messenger-Doku-Index: Docs2017/api/mgmt_api (nicht stopcontainer.bat).
|
|
||||||
/// LocalCmd nur wenn Server-Scripts existieren (bei reiner SMC oft nicht der Fall).
|
|
||||||
/// </summary>
|
|
||||||
public SonicManagementMode ManagementMode { get; init; } = SonicManagementMode.MfApi;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Effektiver Modus: bei WinRm und lokalem ConnectionUrl-Host wird LocalCmd erzwungen.
|
|
||||||
/// MfApi bleibt unverändert (nutzt Domain-Manager-Verbindung).
|
|
||||||
/// </summary>
|
|
||||||
public SonicManagementMode EffectiveManagementMode
|
|
||||||
=> ManagementMode == SonicManagementMode.WinRm && IsConnectionHostLocal(ConnectionUrl)
|
|
||||||
? SonicManagementMode.LocalCmd
|
|
||||||
: ManagementMode;
|
|
||||||
|
|
||||||
/// <summary>True wenn WinRm konfiguriert war, aber wegen lokalem Host auf LocalCmd umgestellt wurde.</summary>
|
|
||||||
public bool IsLocalCmdAutoForced
|
|
||||||
=> ManagementMode == SonicManagementMode.WinRm
|
|
||||||
&& EffectiveManagementMode == SonicManagementMode.LocalCmd;
|
|
||||||
|
|
||||||
/// <summary>Anzeigetext für UI (inkl. Auto-Erkennung).</summary>
|
|
||||||
public string ManagementModeDisplay
|
|
||||||
=> IsLocalCmdAutoForced
|
|
||||||
? "LocalCmd (Host lokal erkannt)"
|
|
||||||
: EffectiveManagementMode switch
|
|
||||||
{
|
|
||||||
SonicManagementMode.MfApi => "MfApi (SMC Management Application API)",
|
|
||||||
SonicManagementMode.LocalCmd => "LocalCmd (nur wenn stopcontainer.bat existiert)",
|
|
||||||
_ => EffectiveManagementMode.ToString()
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// True wenn der Host aus ConnectionUrl dieser Maschine entspricht
|
|
||||||
/// (localhost / 127.0.0.1 / ::1 / Computername).
|
|
||||||
/// </summary>
|
|
||||||
public static bool IsConnectionHostLocal(string? connectionUrl)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(connectionUrl))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
string host;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
host = new Uri(connectionUrl).Host;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
host = connectionUrl.Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(host))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (host.Equals("localhost", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| host.Equals("127.0.0.1", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| host.Equals("::1", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| host.Equals("[::1]", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
string machine = Environment.MachineName;
|
|
||||||
if (host.Equals(machine, StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| host.StartsWith(machine + ".", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string dnsName = System.Net.Dns.GetHostName();
|
|
||||||
if (!string.IsNullOrWhiteSpace(dnsName)
|
|
||||||
&& (host.Equals(dnsName, StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| host.StartsWith(dnsName + ".", StringComparison.OrdinalIgnoreCase)))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// DNS optional
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sonic-/MQ-/ESB-Installationsroot, z.B. "C:\Sonic\MQ10.0" (Client-JARs oft unter lib).
|
|
||||||
/// Bei ESB-Home: ggf. MfClientLibPath auf MQ\lib setzen.
|
|
||||||
/// </summary>
|
|
||||||
public string SonicHome { get; init; } = @"C:\Sonic\MQ10.0";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Optional: JRE/JDK-Home (z.B. C:\Sonic\MQ10.0\jre) oder Pfad zu java.exe.
|
|
||||||
/// Leer = automatische Suche (JAVA_HOME/JRE_HOME, setenv.bat, rekursiv unter SonicHome, Registry, …).
|
|
||||||
/// </summary>
|
|
||||||
public string JavaHome { get; init; } = string.Empty;
|
public string JavaHome { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Optionaler direkter Pfad zu java.exe (überschreibt JavaHome wenn gesetzt).
|
|
||||||
/// </summary>
|
|
||||||
public string JavaPath { get; init; } = string.Empty;
|
public string JavaPath { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Optionaler Pfad zu Sonic-Client-JARs für MfApi (mgmt_client.jar etc.).
|
|
||||||
/// Leer = SonicHome\lib, MQ_HOME\lib, ESB_HOME\lib bzw. nested MQ*/lib.
|
|
||||||
/// </summary>
|
|
||||||
public string MfClientLibPath { get; init; } = string.Empty;
|
public string MfClientLibPath { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Container-Namen (kurz), z.B. ["DE-Test"]. Nicht die Domain.
|
|
||||||
/// </summary>
|
|
||||||
public List<string> KnownContainers { get; init; } = [];
|
public List<string> KnownContainers { get; init; } = [];
|
||||||
|
|
||||||
public int ManagementHttpPort { get; init; } = 8080;
|
|
||||||
public string ApiBasePath { get; init; } = "/api/v1";
|
|
||||||
public string ContainerListPath { get; init; } = string.Empty;
|
|
||||||
public string ContainerRestartPath { get; init; } = string.Empty;
|
|
||||||
public string ContainerStopPath { get; init; } = string.Empty;
|
|
||||||
public string ContainerStartPath { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
public int WinRmPort { get; init; } = 5985;
|
|
||||||
public string WinRmRestartScript { get; init; } = string.Empty;
|
|
||||||
public string WinRmContainerListScript { get; init; } = string.Empty;
|
|
||||||
public string WinRmXapiImportScript { get; init; } = string.Empty;
|
|
||||||
|
|
||||||
public int TimeoutSeconds { get; init; } = 120;
|
public int TimeoutSeconds { get; init; } = 120;
|
||||||
public int PostRestartDelaySeconds { get; init; } = 20;
|
public int PostRestartDelaySeconds { get; init; } = 20;
|
||||||
|
|
||||||
public string ResolveRestartScript()
|
|
||||||
=> string.IsNullOrWhiteSpace(WinRmRestartScript)
|
|
||||||
? DefaultRestartScript
|
|
||||||
: WinRmRestartScript;
|
|
||||||
|
|
||||||
public string ResolveContainerListScript()
|
|
||||||
=> string.IsNullOrWhiteSpace(WinRmContainerListScript)
|
|
||||||
? DefaultContainerListScript
|
|
||||||
: WinRmContainerListScript;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Offizieller MF-Container-Neustart laut Aurea CX Messenger Doku:
|
|
||||||
/// SonicHome\bin\stopcontainer.bat Domain.Container
|
|
||||||
/// SonicHome\bin\startcontainer.bat Domain.Container
|
|
||||||
/// Danach Prozess-Verifikation (alte PIDs weg, neue PIDs da).
|
|
||||||
/// </summary>
|
|
||||||
public const string DefaultRestartScript =
|
|
||||||
"""
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
$sonicHome = '{sonicHome}'
|
|
||||||
$domain = '{domain}'
|
|
||||||
$container = '{container}'
|
|
||||||
|
|
||||||
$bin = Join-Path $sonicHome 'bin'
|
|
||||||
$stopBat = Join-Path $bin 'stopcontainer.bat'
|
|
||||||
$startBat = Join-Path $bin 'startcontainer.bat'
|
|
||||||
|
|
||||||
# Kurzname = Container (z.B. DE-Test), Full = Domain.Container (z.B. proalpha-test.DE-Test)
|
|
||||||
$shortName = if ($container -like '*.*') { ($container -split '\.', 2)[1] } else { $container }
|
|
||||||
$fullName = if ($container -like '*.*') { $container } else { "$domain.$container" }
|
|
||||||
$markers = @($shortName, $fullName, "$domain.$shortName") | Select-Object -Unique
|
|
||||||
|
|
||||||
function Get-ContainerPids {
|
|
||||||
$pids = @()
|
|
||||||
Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
|
||||||
Where-Object {
|
|
||||||
$cmd = $_.CommandLine
|
|
||||||
if (-not $cmd) { return $false }
|
|
||||||
if ($_.Name -notmatch 'java|javaw|sonic') { return $false }
|
|
||||||
foreach ($m in $markers) {
|
|
||||||
if ($cmd -like "*$m*") { return $true }
|
|
||||||
}
|
|
||||||
return $false
|
|
||||||
} |
|
|
||||||
ForEach-Object { $pids += [int]$_.ProcessId }
|
|
||||||
return @($pids | Select-Object -Unique)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Wait-PidsGone([int[]]$pids, [int]$seconds) {
|
|
||||||
$deadline = (Get-Date).AddSeconds($seconds)
|
|
||||||
while ((Get-Date) -lt $deadline) {
|
|
||||||
$alive = @($pids | Where-Object { Get-Process -Id $_ -ErrorAction SilentlyContinue })
|
|
||||||
if ($alive.Count -eq 0) { return $true }
|
|
||||||
Start-Sleep -Seconds 2
|
|
||||||
}
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
function Wait-NewPids([int[]]$oldPids, [int]$seconds) {
|
|
||||||
$deadline = (Get-Date).AddSeconds($seconds)
|
|
||||||
while ((Get-Date) -lt $deadline) {
|
|
||||||
$now = @(Get-ContainerPids)
|
|
||||||
$fresh = @($now | Where-Object { $oldPids -notcontains $_ })
|
|
||||||
if ($fresh.Count -gt 0) { return ,$fresh }
|
|
||||||
Start-Sleep -Seconds 2
|
|
||||||
}
|
|
||||||
return ,@()
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-ContainerBat([string]$bat, [string]$name) {
|
|
||||||
if (-not (Test-Path -LiteralPath $bat)) {
|
|
||||||
throw "Sonic BAT fehlt: $bat (SonicHome pruefen)"
|
|
||||||
}
|
|
||||||
$arg = '/c "' + $bat + '" "' + $name + '"'
|
|
||||||
$p = Start-Process -FilePath 'cmd.exe' -ArgumentList $arg -Wait -PassThru -NoNewWindow
|
|
||||||
return [int]$p.ExitCode
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Output "INFO:Domain=$domain Container=$shortName Full=$fullName"
|
|
||||||
Write-Output "INFO:RestartVia=stopcontainer/startcontainer (MF Container)"
|
|
||||||
$before = @(Get-ContainerPids)
|
|
||||||
Write-Output "INFO:PIDsBefore=$($before -join ',')"
|
|
||||||
|
|
||||||
if ($before.Count -eq 0) {
|
|
||||||
Write-Output "WARN:No running Java/Sonic process for '$shortName' - starting anyway"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 1) Offiziell: stopcontainer.bat Domain.Container (danach Kurzname als Fallback)
|
|
||||||
$stopOk = $false
|
|
||||||
foreach ($n in @($fullName, $shortName)) {
|
|
||||||
Write-Output "INFO:stopcontainer $n"
|
|
||||||
$code = Invoke-ContainerBat -bat $stopBat -name $n
|
|
||||||
Write-Output "INFO:stopcontainer ExitCode=$code Name=$n"
|
|
||||||
if ($code -eq 0) { $stopOk = $true; break }
|
|
||||||
}
|
|
||||||
if (-not $stopOk) {
|
|
||||||
Write-Output "WARN:stopcontainer non-zero; will force-stop remaining PIDs if any"
|
|
||||||
}
|
|
||||||
|
|
||||||
Start-Sleep -Seconds 3
|
|
||||||
$still = @(Get-ContainerPids)
|
|
||||||
|
|
||||||
# 2) Force-Stop falls BAT den Prozess nicht beendet (sonst kein echter Restart in SMC)
|
|
||||||
if ($still.Count -gt 0) {
|
|
||||||
Write-Output "INFO:Force-Stop PIDs=$($still -join ',')"
|
|
||||||
foreach ($procId in $still) {
|
|
||||||
Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($before.Count -gt 0) {
|
|
||||||
if (-not (Wait-PidsGone -pids $before -seconds 45)) {
|
|
||||||
throw "Container process still running after stop (PIDs=$($before -join ','))"
|
|
||||||
}
|
|
||||||
Write-Output "INFO:Old PIDs gone"
|
|
||||||
}
|
|
||||||
|
|
||||||
Start-Sleep -Seconds 3
|
|
||||||
|
|
||||||
# 3) Offiziell: startcontainer.bat Domain.Container
|
|
||||||
$started = $false
|
|
||||||
foreach ($n in @($fullName, $shortName)) {
|
|
||||||
Write-Output "INFO:startcontainer $n"
|
|
||||||
$code = Invoke-ContainerBat -bat $startBat -name $n
|
|
||||||
Write-Output "INFO:startcontainer ExitCode=$code Name=$n"
|
|
||||||
if ($code -eq 0) { $started = $true; break }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $started) {
|
|
||||||
throw "startcontainer failed for $fullName / $shortName"
|
|
||||||
}
|
|
||||||
|
|
||||||
$after = @(Wait-NewPids -oldPids $before -seconds 60)
|
|
||||||
if ($after.Count -eq 0) {
|
|
||||||
$any = @(Get-ContainerPids)
|
|
||||||
if ($any.Count -eq 0) {
|
|
||||||
throw "After start no process for container '$shortName'. Check SMC / SonicHome / ContainerName."
|
|
||||||
}
|
|
||||||
Write-Output "INFO:PIDsAfter=$($any -join ',')"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Output "INFO:PIDsAfter=$($after -join ',')"
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Output "OK:ContainerRestartVerified Domain=$domain Container=$shortName"
|
|
||||||
""";
|
|
||||||
|
|
||||||
public const string DefaultContainerListScript =
|
|
||||||
"""
|
|
||||||
$ErrorActionPreference = 'Continue'
|
|
||||||
$sonicHome = '{sonicHome}'
|
|
||||||
$domain = '{domain}'
|
|
||||||
$names = New-Object System.Collections.Generic.List[string]
|
|
||||||
|
|
||||||
function Add-Name([string]$n) {
|
|
||||||
if ([string]::IsNullOrWhiteSpace($n)) { return }
|
|
||||||
$n = $n.Trim()
|
|
||||||
if (-not $names.Contains($n)) { [void]$names.Add($n) }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Test-Path -LiteralPath $sonicHome)) {
|
|
||||||
Write-Output "WARN:SonicHomeNichtGefunden:$sonicHome"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Output "INFO:SonicHomeOk:$sonicHome Domain=$domain"
|
|
||||||
|
|
||||||
Get-ChildItem -LiteralPath $sonicHome -Directory -ErrorAction SilentlyContinue |
|
|
||||||
ForEach-Object {
|
|
||||||
if ($_.Name -like '*.cache') {
|
|
||||||
Add-Name ($_.Name -replace '\.cache$', '')
|
|
||||||
}
|
|
||||||
Get-ChildItem -LiteralPath $_.FullName -Directory -ErrorAction SilentlyContinue |
|
|
||||||
Where-Object { $_.Name -like '*.cache' } |
|
|
||||||
ForEach-Object { Add-Name ($_.Name -replace '\.cache$', '') }
|
|
||||||
}
|
|
||||||
|
|
||||||
Get-ChildItem -LiteralPath $sonicHome -Recurse -Filter 'container.xml' -File -ErrorAction SilentlyContinue |
|
|
||||||
Select-Object -First 40 |
|
|
||||||
ForEach-Object {
|
|
||||||
Add-Name (Split-Path $_.DirectoryName -Leaf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
|
||||||
Where-Object { $_.CommandLine -match 'sonic|mf\.framework|container\.xml' } |
|
|
||||||
ForEach-Object {
|
|
||||||
if ($_.CommandLine -match '([A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+)\.cache') {
|
|
||||||
Add-Name $Matches[1]
|
|
||||||
}
|
|
||||||
elseif ($_.CommandLine -match '\\([A-Za-z0-9_\-]+)\\container\.xml') {
|
|
||||||
Add-Name $Matches[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Kurzform Domain.Container → Container
|
|
||||||
foreach ($n in @($names.ToArray())) {
|
|
||||||
if ($n -like '*.*') {
|
|
||||||
$parts = $n -split '\.', 2
|
|
||||||
if ($parts.Count -eq 2) { Add-Name $parts[1] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($names.Count -eq 0) {
|
|
||||||
Write-Output "WARN:KeineContainerGefunden Domain=$domain SonicHome=$sonicHome"
|
|
||||||
}
|
|
||||||
|
|
||||||
$names | Sort-Object -Unique
|
|
||||||
""";
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum SonicManagementMode
|
|
||||||
{
|
|
||||||
HttpApi = 0,
|
|
||||||
WinRm = 1,
|
|
||||||
LocalCmd = 2,
|
|
||||||
/// <summary>Sonic MF Management API über Domain-Manager (ConnectionUrl + SMC-Credentials).</summary>
|
|
||||||
MfApi = 3
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
using System.Security.Cryptography;
|
|
||||||
using ZA.CoreService.ESBCertificateManager.Models;
|
|
||||||
|
|
||||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
|
||||||
|
|
||||||
public sealed class CertificateDeployer
|
|
||||||
{
|
|
||||||
public Task<(bool Success, string Status, string? Detail, string? BackupPath)> DeployAsync(
|
|
||||||
string sourceCertificatePath,
|
|
||||||
DeploymentTarget target,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return Task.Run(() => Deploy(sourceCertificatePath, target, cancellationToken), cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (bool Success, string Status, string? Detail, string? BackupPath) Deploy(
|
|
||||||
string sourceCertificatePath,
|
|
||||||
DeploymentTarget target,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
if (!File.Exists(sourceCertificatePath))
|
|
||||||
{
|
|
||||||
return (false, "Quelle fehlt", $"Quelldatei nicht gefunden: {sourceCertificatePath}", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
string targetDirectory = PathResolver.ResolvePath(target.TargetDirectory);
|
|
||||||
Directory.CreateDirectory(targetDirectory);
|
|
||||||
|
|
||||||
string destinationPath = Path.Combine(targetDirectory, target.CertificateFileName);
|
|
||||||
string? backupPath = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (File.Exists(destinationPath))
|
|
||||||
{
|
|
||||||
backupPath = $"{destinationPath}.bak-{DateTime.Now:yyyyMMddHHmmss}";
|
|
||||||
File.Copy(destinationPath, backupPath, overwrite: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
File.Copy(sourceCertificatePath, destinationPath, overwrite: true);
|
|
||||||
|
|
||||||
string sourceHash = ComputeSha256(sourceCertificatePath);
|
|
||||||
string destHash = ComputeSha256(destinationPath);
|
|
||||||
|
|
||||||
if (!string.Equals(sourceHash, destHash, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
RestoreFromBackupOrDelete(destinationPath, backupPath);
|
|
||||||
return (false, "Hash-Fehler", "SHA-256 von Quelle und Ziel stimmen nicht überein. Rollback ausgeführt.", backupPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (true, "Kopiert", $"Ziel: {destinationPath}", backupPath);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (backupPath is not null && File.Exists(backupPath) && File.Exists(destinationPath))
|
|
||||||
{
|
|
||||||
File.Copy(backupPath, destinationPath, overwrite: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Rollback best-effort
|
|
||||||
}
|
|
||||||
|
|
||||||
return (false, "Kopierfehler", ex.Message, backupPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void RestoreFromBackupOrDelete(string destinationPath, string? backupPath)
|
|
||||||
{
|
|
||||||
if (backupPath is not null && File.Exists(backupPath))
|
|
||||||
{
|
|
||||||
File.Copy(backupPath, destinationPath, overwrite: true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (File.Exists(destinationPath))
|
|
||||||
{
|
|
||||||
File.Delete(destinationPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string ComputeSha256(string filePath)
|
|
||||||
{
|
|
||||||
using FileStream stream = File.OpenRead(filePath);
|
|
||||||
byte[] hash = SHA256.HashData(stream);
|
|
||||||
return Convert.ToHexString(hash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +1,25 @@
|
|||||||
using ZA.CoreService.ESBCertificateManager.Data;
|
|
||||||
using ZA.CoreService.ESBCertificateManager.Models;
|
using ZA.CoreService.ESBCertificateManager.Models;
|
||||||
|
|
||||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Nur Container-Neustart (kein Deploy, kein TLS, kein XApi).
|
||||||
|
/// </summary>
|
||||||
public sealed class DeploymentOrchestrator
|
public sealed class DeploymentOrchestrator
|
||||||
{
|
{
|
||||||
private readonly CertificateDeployer _deployer = new();
|
|
||||||
private readonly RestartExecutor _restartExecutor;
|
private readonly RestartExecutor _restartExecutor;
|
||||||
private readonly TlsCertificateProbe _tlsProbe;
|
|
||||||
private readonly PreflightValidator _preflightValidator = new();
|
private readonly PreflightValidator _preflightValidator = new();
|
||||||
private readonly SqlRunLogger _sqlRunLogger;
|
|
||||||
private readonly AppSettings _settings;
|
private readonly AppSettings _settings;
|
||||||
|
|
||||||
public DeploymentOrchestrator(AppSettings settings)
|
public DeploymentOrchestrator(AppSettings settings)
|
||||||
{
|
{
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
_tlsProbe = new TlsCertificateProbe(settings.TlsTimeoutSeconds, settings.TlsRetryCount);
|
|
||||||
_restartExecutor = new RestartExecutor(settings.SonicConnections);
|
_restartExecutor = new RestartExecutor(settings.SonicConnections);
|
||||||
_sqlRunLogger = new SqlRunLogger(settings.ConnectionString);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public PreflightValidationResult ValidatePreflight(
|
|
||||||
string? certificatePath,
|
|
||||||
CertificateInfo? certificateInfo,
|
|
||||||
IReadOnlyList<DeploymentTarget> selectedTargets)
|
|
||||||
=> _preflightValidator.Validate(certificatePath, certificateInfo, selectedTargets);
|
|
||||||
|
|
||||||
public PreflightValidationResult ValidateRestartOnly(IReadOnlyList<DeploymentTarget> selectedTargets)
|
public PreflightValidationResult ValidateRestartOnly(IReadOnlyList<DeploymentTarget> selectedTargets)
|
||||||
=> _preflightValidator.ValidateRestartOnly(selectedTargets);
|
=> _preflightValidator.ValidateRestartOnly(selectedTargets);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Nur ESB-/Container-Neustart – ohne Zertifikatskopieren und ohne TLS-Probe.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<DeploymentRunResult> RestartOnlyAsync(
|
public async Task<DeploymentRunResult> RestartOnlyAsync(
|
||||||
IReadOnlyList<DeploymentTarget> selectedTargets,
|
IReadOnlyList<DeploymentTarget> selectedTargets,
|
||||||
IProgress<TargetProgressUpdate>? progress,
|
IProgress<TargetProgressUpdate>? progress,
|
||||||
@@ -41,223 +29,43 @@ public sealed class DeploymentOrchestrator
|
|||||||
List<TargetStepResult> results = [];
|
List<TargetStepResult> results = [];
|
||||||
|
|
||||||
using RunLogger logger = new(_settings.LogDirectory);
|
using RunLogger logger = new(_settings.LogDirectory);
|
||||||
logger.Write($"Neustart-only gestartet für {selectedTargets.Count} Ziel(e).");
|
logger.Write($"Neustart gestartet für {selectedTargets.Count} Ziel(e).");
|
||||||
|
|
||||||
foreach (DeploymentTarget target in selectedTargets)
|
foreach (DeploymentTarget target in selectedTargets)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
results.Add(await RestartTargetOnlyAsync(target, logger, progress, cancellationToken));
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTimeOffset finishedAt = DateTimeOffset.Now;
|
|
||||||
DeploymentRunResult runResult = new()
|
|
||||||
{
|
|
||||||
TargetResults = results,
|
|
||||||
StartedAt = startedAt,
|
|
||||||
FinishedAt = finishedAt,
|
|
||||||
CertificateFilePath = string.Empty,
|
|
||||||
CertificateFingerprint = string.Empty
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.Write(
|
|
||||||
$"Neustart-only beendet. Erfolg={runResult.OverallSuccess}; Dauer={(finishedAt - startedAt).TotalSeconds:F1}s; Log={logger.LogFilePath}");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _sqlRunLogger.PersistRunResultAsync(runResult, cancellationToken);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.Write($"SQL-Protokollierung fehlgeschlagen (nicht kritisch): {ex.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return runResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<TargetStepResult> RestartTargetOnlyAsync(
|
|
||||||
DeploymentTarget target,
|
|
||||||
RunLogger logger,
|
|
||||||
IProgress<TargetProgressUpdate>? progress,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
List<string> steps = [];
|
|
||||||
DateTimeOffset targetStart = DateTimeOffset.Now;
|
DateTimeOffset targetStart = DateTimeOffset.Now;
|
||||||
progress?.Report(new TargetProgressUpdate(target.Id, "Neustart…", false));
|
progress?.Report(new TargetProgressUpdate(target.Id, $"Neustart {target.ContainerName}…", null));
|
||||||
|
|
||||||
(bool restartOk, string restartStatus, string? restartDetail) =
|
(bool ok, string status, string? detail) =
|
||||||
await _restartExecutor.ExecuteAsync(target, cancellationToken);
|
await _restartExecutor.ExecuteAsync(target, cancellationToken);
|
||||||
|
|
||||||
RecordStep(steps, logger, target.Name, "Restart", restartStatus, restartDetail);
|
logger.Write($"[{target.Name}] {status} | {detail}");
|
||||||
|
progress?.Report(new TargetProgressUpdate(target.Id, status, ok));
|
||||||
|
|
||||||
TargetStepResult targetResult = new()
|
results.Add(new TargetStepResult
|
||||||
{
|
{
|
||||||
TargetId = target.Id,
|
TargetId = target.Id,
|
||||||
TargetName = target.Name,
|
TargetName = target.Name,
|
||||||
Success = restartOk,
|
Success = ok,
|
||||||
StatusText = restartStatus,
|
|
||||||
Detail = restartDetail,
|
|
||||||
Steps = steps,
|
|
||||||
StartedAt = targetStart,
|
|
||||||
FinishedAt = DateTimeOffset.Now,
|
|
||||||
CopySucceeded = true,
|
|
||||||
RestartSucceeded = restartOk,
|
|
||||||
TlsSucceeded = true,
|
|
||||||
ObservedFingerprint = null
|
|
||||||
};
|
|
||||||
|
|
||||||
progress?.Report(new TargetProgressUpdate(target.Id, restartStatus, restartOk));
|
|
||||||
return targetResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<DeploymentRunResult> RunAsync(
|
|
||||||
string certificatePath,
|
|
||||||
CertificateInfo certificateInfo,
|
|
||||||
IReadOnlyList<DeploymentTarget> selectedTargets,
|
|
||||||
IProgress<TargetProgressUpdate>? progress,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
DateTimeOffset startedAt = DateTimeOffset.Now;
|
|
||||||
List<TargetStepResult> results = [];
|
|
||||||
|
|
||||||
using RunLogger logger = new(_settings.LogDirectory);
|
|
||||||
logger.Write($"Deployment gestartet für {selectedTargets.Count} Ziel(e). Zertifikat={Path.GetFileName(certificatePath)}");
|
|
||||||
|
|
||||||
foreach (DeploymentTarget target in selectedTargets)
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
results.Add(await RunTargetAsync(target, certificatePath, certificateInfo, logger, progress, cancellationToken));
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTimeOffset finishedAt = DateTimeOffset.Now;
|
|
||||||
DeploymentRunResult runResult = new()
|
|
||||||
{
|
|
||||||
TargetResults = results,
|
|
||||||
StartedAt = startedAt,
|
|
||||||
FinishedAt = finishedAt,
|
|
||||||
CertificateFilePath = certificatePath,
|
|
||||||
CertificateFingerprint = certificateInfo.FingerprintSha256
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.Write(
|
|
||||||
$"Deployment beendet. Erfolg={runResult.OverallSuccess}; Dauer={(finishedAt - startedAt).TotalSeconds:F1}s; Log={logger.LogFilePath}");
|
|
||||||
|
|
||||||
// Ergebnis in SQL-Datenbank protokollieren (wenn ConnectionString konfiguriert)
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await _sqlRunLogger.PersistRunResultAsync(runResult, cancellationToken);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.Write($"SQL-Protokollierung fehlgeschlagen (nicht kritisch): {ex.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return runResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<TargetStepResult> RunTargetAsync(
|
|
||||||
DeploymentTarget target,
|
|
||||||
string certificatePath,
|
|
||||||
CertificateInfo certificateInfo,
|
|
||||||
RunLogger logger,
|
|
||||||
IProgress<TargetProgressUpdate>? progress,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
List<string> steps = [];
|
|
||||||
DateTimeOffset targetStart = DateTimeOffset.Now;
|
|
||||||
progress?.Report(new TargetProgressUpdate(target.Id, "Läuft…", false));
|
|
||||||
|
|
||||||
(bool copyOk, string copyStatus, string? copyDetail, _) =
|
|
||||||
await _deployer.DeployAsync(certificatePath, target, cancellationToken);
|
|
||||||
|
|
||||||
RecordStep(steps, logger, target.Name, "Deploy", copyStatus, copyDetail);
|
|
||||||
if (!copyOk)
|
|
||||||
{
|
|
||||||
return Fail(target, copyStatus, copyDetail, steps, targetStart, progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
progress?.Report(new TargetProgressUpdate(target.Id, copyStatus, false));
|
|
||||||
|
|
||||||
(bool restartOk, string restartStatus, string? restartDetail) =
|
|
||||||
await _restartExecutor.ExecuteAsync(target, cancellationToken);
|
|
||||||
|
|
||||||
RecordStep(steps, logger, target.Name, "Restart", restartStatus, restartDetail);
|
|
||||||
if (!restartOk)
|
|
||||||
{
|
|
||||||
return Fail(target, restartStatus, restartDetail, steps, targetStart, progress,
|
|
||||||
copySucceeded: copyOk);
|
|
||||||
}
|
|
||||||
|
|
||||||
progress?.Report(new TargetProgressUpdate(target.Id, restartStatus, false));
|
|
||||||
|
|
||||||
(bool tlsOk, string tlsStatus, string? tlsDetail, string? observedFingerprint) = await _tlsProbe.ProbeAsync(
|
|
||||||
target,
|
|
||||||
certificateInfo.FingerprintSha256,
|
|
||||||
cancellationToken);
|
|
||||||
|
|
||||||
RecordStep(steps, logger, target.Name, "TLS", tlsStatus, tlsDetail);
|
|
||||||
|
|
||||||
bool success = tlsOk;
|
|
||||||
string finalStatus = success
|
|
||||||
? (string.IsNullOrWhiteSpace(target.TlsHost) ? "Erfolg" : tlsStatus)
|
|
||||||
: tlsStatus;
|
|
||||||
|
|
||||||
TargetStepResult targetResult = new()
|
|
||||||
{
|
|
||||||
TargetId = target.Id,
|
|
||||||
TargetName = target.Name,
|
|
||||||
Success = success,
|
|
||||||
StatusText = finalStatus,
|
|
||||||
Detail = tlsDetail,
|
|
||||||
Steps = steps,
|
|
||||||
StartedAt = targetStart,
|
|
||||||
FinishedAt = DateTimeOffset.Now,
|
|
||||||
CopySucceeded = copyOk,
|
|
||||||
RestartSucceeded = restartOk,
|
|
||||||
TlsSucceeded = tlsOk,
|
|
||||||
ObservedFingerprint = observedFingerprint
|
|
||||||
};
|
|
||||||
progress?.Report(new TargetProgressUpdate(target.Id, finalStatus, success));
|
|
||||||
return targetResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void RecordStep(
|
|
||||||
List<string> steps,
|
|
||||||
RunLogger logger,
|
|
||||||
string targetName,
|
|
||||||
string phase,
|
|
||||||
string status,
|
|
||||||
string? detail)
|
|
||||||
{
|
|
||||||
steps.Add($"{status}: {detail}");
|
|
||||||
logger.Write($"[{targetName}] {phase}: {status} | {detail}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static TargetStepResult Fail(
|
|
||||||
DeploymentTarget target,
|
|
||||||
string status,
|
|
||||||
string? detail,
|
|
||||||
List<string> steps,
|
|
||||||
DateTimeOffset startedAt,
|
|
||||||
IProgress<TargetProgressUpdate>? progress,
|
|
||||||
bool copySucceeded = false,
|
|
||||||
bool restartSucceeded = false)
|
|
||||||
{
|
|
||||||
progress?.Report(new TargetProgressUpdate(target.Id, status, false));
|
|
||||||
return new TargetStepResult
|
|
||||||
{
|
|
||||||
TargetId = target.Id,
|
|
||||||
TargetName = target.Name,
|
|
||||||
Success = false,
|
|
||||||
StatusText = status,
|
StatusText = status,
|
||||||
Detail = detail,
|
Detail = detail,
|
||||||
Steps = steps,
|
StartedAt = targetStart,
|
||||||
|
FinishedAt = DateTimeOffset.Now
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTimeOffset finishedAt = DateTimeOffset.Now;
|
||||||
|
DeploymentRunResult runResult = new()
|
||||||
|
{
|
||||||
|
TargetResults = results,
|
||||||
StartedAt = startedAt,
|
StartedAt = startedAt,
|
||||||
FinishedAt = DateTimeOffset.Now,
|
FinishedAt = finishedAt
|
||||||
CopySucceeded = copySucceeded,
|
|
||||||
RestartSucceeded = restartSucceeded,
|
|
||||||
TlsSucceeded = false
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
logger.Write(
|
||||||
|
$"Neustart beendet. Erfolg={runResult.OverallSuccess}; Dauer={(finishedAt - startedAt).TotalSeconds:F1}s; Log={logger.LogFilePath}");
|
||||||
|
|
||||||
|
return runResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly record struct TargetProgressUpdate(int TargetId, string StatusText, bool? SuccessHint);
|
|
||||||
|
|||||||
@@ -4,136 +4,37 @@ namespace ZA.CoreService.ESBCertificateManager.Services;
|
|||||||
|
|
||||||
public sealed class PreflightValidator
|
public sealed class PreflightValidator
|
||||||
{
|
{
|
||||||
public PreflightValidationResult Validate(
|
|
||||||
string? certificatePath,
|
|
||||||
CertificateInfo? certificateInfo,
|
|
||||||
IReadOnlyList<DeploymentTarget> selectedTargets)
|
|
||||||
{
|
|
||||||
PreflightValidationResult result = new();
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(certificatePath) || !File.Exists(certificatePath))
|
|
||||||
{
|
|
||||||
AddIssue(result, "Es ist keine gültige Zertifikatsdatei ausgewählt.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (certificateInfo is null)
|
|
||||||
{
|
|
||||||
AddIssue(result, "Zertifikatsmetadaten sind nicht geladen.");
|
|
||||||
}
|
|
||||||
else if (!certificateInfo.IsCurrentlyValid)
|
|
||||||
{
|
|
||||||
AddIssue(result, "Das geladene Zertifikat ist abgelaufen oder ungültig.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedTargets.Count == 0)
|
|
||||||
{
|
|
||||||
AddIssue(result, "Bitte mindestens ein Ziel anhaken.");
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (DeploymentTarget target in selectedTargets)
|
|
||||||
{
|
|
||||||
ValidateTarget(result, target, requireDeployPaths: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Vorabprüfung nur für ESB-Neustart (ohne Zertifikat / Kopierpfade).
|
|
||||||
/// </summary>
|
|
||||||
public PreflightValidationResult ValidateRestartOnly(IReadOnlyList<DeploymentTarget> selectedTargets)
|
public PreflightValidationResult ValidateRestartOnly(IReadOnlyList<DeploymentTarget> selectedTargets)
|
||||||
{
|
{
|
||||||
PreflightValidationResult result = new();
|
PreflightValidationResult result = new();
|
||||||
|
|
||||||
if (selectedTargets.Count == 0)
|
if (selectedTargets.Count == 0)
|
||||||
{
|
{
|
||||||
AddIssue(result, "Bitte mindestens ein Ziel anhaken.");
|
result.Issues.Add(new ValidationIssue { Message = "Bitte mindestens ein Ziel anhaken." });
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (DeploymentTarget target in selectedTargets)
|
foreach (DeploymentTarget target in selectedTargets)
|
||||||
{
|
|
||||||
if (target.RestartType is RestartType.None)
|
|
||||||
{
|
|
||||||
AddIssue(result, $"Ziel '{target.Name}': RestartType=None – kein Neustart konfiguriert.", target.Id);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ValidateTarget(result, target, requireDeployPaths: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ValidateTarget(
|
|
||||||
PreflightValidationResult result,
|
|
||||||
DeploymentTarget target,
|
|
||||||
bool requireDeployPaths)
|
|
||||||
{
|
|
||||||
if (requireDeployPaths)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(target.TargetDirectory))
|
|
||||||
{
|
|
||||||
AddIssue(result, $"Ziel '{target.Name}': TargetDirectory fehlt.", target.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(target.CertificateFileName))
|
|
||||||
{
|
|
||||||
AddIssue(result, $"Ziel '{target.Name}': CertificateFileName fehlt.", target.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target.RestartType == RestartType.Command
|
|
||||||
&& string.IsNullOrWhiteSpace(target.RestartCommand))
|
|
||||||
{
|
|
||||||
AddIssue(result, $"Ziel '{target.Name}': RestartCommand fehlt bei RestartType=Command.", target.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target.RestartType is RestartType.SonicContainer or RestartType.SonicContainerWithXapi)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(target.ContainerName))
|
if (string.IsNullOrWhiteSpace(target.ContainerName))
|
||||||
{
|
{
|
||||||
AddIssue(result, $"Ziel '{target.Name}': ContainerName fehlt bei RestartType={target.RestartType}.", target.Id);
|
result.Issues.Add(new ValidationIssue
|
||||||
|
{
|
||||||
|
Message = $"Ziel '{target.Name}': ContainerName fehlt.",
|
||||||
|
TargetId = target.Id
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(target.SonicConnectionName))
|
if (string.IsNullOrWhiteSpace(target.SonicConnectionName))
|
||||||
{
|
|
||||||
AddIssue(result, $"Ziel '{target.Name}': SonicConnectionName fehlt bei RestartType={target.RestartType}.", target.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target.RestartType == RestartType.SonicContainerWithXapi
|
|
||||||
&& string.IsNullOrWhiteSpace(target.XapiSourcePath))
|
|
||||||
{
|
|
||||||
AddIssue(result, $"Ziel '{target.Name}': XapiSourcePath fehlt bei RestartType=SonicContainerWithXapi.", target.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!requireDeployPaths || string.IsNullOrWhiteSpace(target.TargetDirectory))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string directory = PathResolver.ResolvePath(target.TargetDirectory);
|
|
||||||
Directory.CreateDirectory(directory);
|
|
||||||
|
|
||||||
string probeFile = Path.Combine(directory, $".write-probe-{Guid.NewGuid():N}");
|
|
||||||
File.WriteAllText(probeFile, "ok");
|
|
||||||
File.Delete(probeFile);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
AddIssue(result, $"Ziel '{target.Name}': Verzeichnis nicht beschreibbar ({ex.Message}).", target.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddIssue(PreflightValidationResult result, string message, int? targetId = null)
|
|
||||||
{
|
{
|
||||||
result.Issues.Add(new ValidationIssue
|
result.Issues.Add(new ValidationIssue
|
||||||
{
|
{
|
||||||
TargetId = targetId,
|
Message = $"Ziel '{target.Name}': SonicConnectionName fehlt.",
|
||||||
Message = message
|
TargetId = target.Id
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using ZA.CoreService.ESBCertificateManager.Models;
|
using ZA.CoreService.ESBCertificateManager.Models;
|
||||||
|
|
||||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||||
@@ -12,26 +11,13 @@ public sealed class RestartExecutor
|
|||||||
_sonicConnections = sonicConnections;
|
_sonicConnections = sonicConnections;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<(bool Success, string Status, string? Detail)> ExecuteAsync(
|
public async Task<(bool Success, string Status, string? Detail)> ExecuteAsync(
|
||||||
DeploymentTarget target,
|
DeploymentTarget target,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
=> target.RestartType switch
|
|
||||||
{
|
|
||||||
RestartType.None => Task.FromResult<(bool, string, string?)>((true, "Neustart übersprungen", "RestartType=None")),
|
|
||||||
RestartType.Command => ExecuteCommandAsync(target, cancellationToken),
|
|
||||||
RestartType.SonicContainer => ExecuteSonicRestartAsync(target, importXapi: false, cancellationToken),
|
|
||||||
RestartType.SonicContainerWithXapi => ExecuteSonicRestartAsync(target, importXapi: true, cancellationToken),
|
|
||||||
_ => Task.FromResult<(bool, string, string?)>((false, "Unbekannter RestartType", $"RestartType={target.RestartType}"))
|
|
||||||
};
|
|
||||||
|
|
||||||
private async Task<(bool Success, string Status, string? Detail)> ExecuteSonicRestartAsync(
|
|
||||||
DeploymentTarget target,
|
|
||||||
bool importXapi,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(target.ContainerName))
|
if (string.IsNullOrWhiteSpace(target.ContainerName))
|
||||||
{
|
{
|
||||||
return (false, "Sonic-Neustart fehlgeschlagen", $"Ziel '{target.Name}': ContainerName fehlt.");
|
return (false, "Neustart fehlgeschlagen", $"Ziel '{target.Name}': ContainerName fehlt.");
|
||||||
}
|
}
|
||||||
|
|
||||||
SonicConnection? connection = _sonicConnections
|
SonicConnection? connection = _sonicConnections
|
||||||
@@ -40,111 +26,10 @@ public sealed class RestartExecutor
|
|||||||
if (connection is null)
|
if (connection is null)
|
||||||
{
|
{
|
||||||
return (false, "Sonic-Verbindung nicht gefunden",
|
return (false, "Sonic-Verbindung nicht gefunden",
|
||||||
$"Ziel '{target.Name}': SonicConnection '{target.SonicConnectionName}' ist nicht in AppSettings konfiguriert.");
|
$"Ziel '{target.Name}': SonicConnection '{target.SonicConnectionName}' fehlt in appsettings.");
|
||||||
}
|
}
|
||||||
|
|
||||||
using SonicManagementClient client = new(connection);
|
using SonicManagementClient client = new(connection);
|
||||||
|
|
||||||
if (importXapi)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(target.XapiSourcePath))
|
|
||||||
{
|
|
||||||
return (false, "XApi-Import fehlgeschlagen",
|
|
||||||
$"Ziel '{target.Name}': XapiSourcePath fehlt bei RestartType=SonicContainerWithXapi.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return await client.ImportXapiAndRestartAsync(target.ContainerName, target.XapiSourcePath, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await client.RestartContainerAsync(target.ContainerName, cancellationToken);
|
return await client.RestartContainerAsync(target.ContainerName, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(bool Success, string Status, string? Detail)> ExecuteCommandAsync(
|
|
||||||
DeploymentTarget target,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(target.RestartCommand))
|
|
||||||
{
|
|
||||||
return (true, "Neustart übersprungen", "RestartType=None");
|
|
||||||
}
|
|
||||||
|
|
||||||
int timeoutSeconds = Math.Clamp(target.RestartTimeoutSeconds, 1, 600);
|
|
||||||
|
|
||||||
ProcessStartInfo startInfo = new()
|
|
||||||
{
|
|
||||||
FileName = target.RestartCommand,
|
|
||||||
Arguments = target.RestartArguments ?? string.Empty,
|
|
||||||
UseShellExecute = false,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
CreateNoWindow = true
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using Process process = new() { StartInfo = startInfo };
|
|
||||||
if (!process.Start())
|
|
||||||
{
|
|
||||||
return (false, "Neustart fehlgeschlagen", "Prozess konnte nicht gestartet werden.");
|
|
||||||
}
|
|
||||||
|
|
||||||
Task<string> stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
|
|
||||||
Task<string> stderrTask = process.StandardError.ReadToEndAsync(cancellationToken);
|
|
||||||
|
|
||||||
using CancellationTokenSource timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
||||||
timeoutCts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds));
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await process.WaitForExitAsync(timeoutCts.Token);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
try { process.Kill(entireProcessTree: true); } catch { /* ignore */ }
|
|
||||||
return (false, "Neustart Timeout", $"Timeout nach {timeoutSeconds}s.");
|
|
||||||
}
|
|
||||||
|
|
||||||
string detail = BuildDetail(process.ExitCode, await stdoutTask, await stderrTask);
|
|
||||||
return process.ExitCode == 0
|
|
||||||
? (true, "Neustart ok", detail)
|
|
||||||
: (false, "Neustart fehlgeschlagen", detail);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return (false, "Neustart fehlgeschlagen", ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildDetail(int exitCode, string stdout, string stderr)
|
|
||||||
{
|
|
||||||
string detail = $"ExitCode={exitCode}";
|
|
||||||
|
|
||||||
stdout = Truncate(stdout).Trim();
|
|
||||||
if (!string.IsNullOrWhiteSpace(stdout))
|
|
||||||
{
|
|
||||||
detail += "; out=" + stdout;
|
|
||||||
}
|
|
||||||
|
|
||||||
stderr = Truncate(stderr).Trim();
|
|
||||||
if (!string.IsNullOrWhiteSpace(stderr))
|
|
||||||
{
|
|
||||||
detail += "; err=" + stderr;
|
|
||||||
}
|
|
||||||
|
|
||||||
return detail;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Truncate(string value, int max = 400)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(value) || value.Length <= max)
|
|
||||||
{
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value[..max] + "…";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,245 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using System.Text;
|
|
||||||
using ZA.CoreService.ESBCertificateManager.Models;
|
|
||||||
|
|
||||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Neustart über Sonic-Server-Scripts:
|
|
||||||
/// stopcontainer.bat / startcontainer.bat
|
|
||||||
/// Diese liegen nur in einer vollen MQ/ESB-Server-Installation – nicht in einer
|
|
||||||
/// reinen Sonic Management Console (SMC/Client).
|
|
||||||
/// </summary>
|
|
||||||
public sealed class SonicBinRestartExecutor
|
|
||||||
{
|
|
||||||
private readonly SonicConnection _connection;
|
|
||||||
|
|
||||||
public SonicBinRestartExecutor(SonicConnection connection)
|
|
||||||
{
|
|
||||||
_connection = connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
public (bool Ok, string? Error, string? Detail) Probe()
|
|
||||||
{
|
|
||||||
string sonicHome = _connection.SonicHome?.Trim() ?? string.Empty;
|
|
||||||
if (string.IsNullOrWhiteSpace(sonicHome))
|
|
||||||
{
|
|
||||||
return (false, "SonicHome ist leer – in appsettings setzen.", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Directory.Exists(sonicHome))
|
|
||||||
{
|
|
||||||
return (false, $"SonicHome existiert nicht: '{sonicHome}'", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
(string? stop, string? start, string searchNote) = LocateContainerScripts(sonicHome);
|
|
||||||
if (stop is null || start is null)
|
|
||||||
{
|
|
||||||
string binDir = Path.Combine(sonicHome, "bin");
|
|
||||||
string binListing = DescribeDirectory(binDir);
|
|
||||||
bool looksLikeSmcOnly = Directory.Exists(Path.Combine(sonicHome, "lib"))
|
|
||||||
&& !File.Exists(Path.Combine(binDir, "stopcontainer.bat"));
|
|
||||||
|
|
||||||
string why = looksLikeSmcOnly
|
|
||||||
? "Das sieht nach einer Sonic Management Console / Client-Installation aus "
|
|
||||||
+ "(lib vorhanden, aber keine Server-Scripts). "
|
|
||||||
+ "stopcontainer.bat gibt es nur auf dem Sonic-SERVER, nicht in der reinen SMC."
|
|
||||||
: "Server-Scripts wurden unter SonicHome nicht gefunden.";
|
|
||||||
|
|
||||||
return (false,
|
|
||||||
why + $" Gesucht unter '{sonicHome}'.",
|
|
||||||
$"{searchNote}\nInhalt von bin: {binListing}\n"
|
|
||||||
+ "Lösung A: SonicHome auf den Server-Installationspfad setzen (dort wo stopcontainer.bat liegt).\n"
|
|
||||||
+ "Lösung B: App lässt automatisch MfApi/SMC-Verbindung versuchen (ConnectionUrl + Login).");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (true, null, $"stop={stop}; start={start}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<(bool Success, string Status, string? Detail)> RestartAsync(
|
|
||||||
string containerName,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
(bool probeOk, string? probeError, string? probeDetail) = Probe();
|
|
||||||
if (!probeOk)
|
|
||||||
{
|
|
||||||
return (false, "Neustart fehlgeschlagen (SonicBin)",
|
|
||||||
$"{probeError}\n{probeDetail}");
|
|
||||||
}
|
|
||||||
|
|
||||||
string sonicHome = _connection.SonicHome.Trim();
|
|
||||||
(string? stopBat, string? startBat, _) = LocateContainerScripts(sonicHome);
|
|
||||||
string bin = Path.GetDirectoryName(stopBat!)!;
|
|
||||||
|
|
||||||
string shortName = containerName.Contains('.')
|
|
||||||
? containerName[(containerName.IndexOf('.') + 1)..]
|
|
||||||
: containerName;
|
|
||||||
string fullName = containerName.Contains('.')
|
|
||||||
? containerName
|
|
||||||
: $"{_connection.DomainName}.{containerName}";
|
|
||||||
|
|
||||||
StringBuilder log = new();
|
|
||||||
log.AppendLine($"SonicHome={sonicHome}");
|
|
||||||
log.AppendLine($"stop={stopBat}");
|
|
||||||
log.AppendLine($"start={startBat}");
|
|
||||||
log.AppendLine($"Container={fullName} (kurz={shortName})");
|
|
||||||
|
|
||||||
(bool stopOk, string stopOut, string stopErr, int stopCode) =
|
|
||||||
await RunBatAsync(stopBat!, fullName, bin, cancellationToken);
|
|
||||||
log.AppendLine($"STOP ExitCode={stopCode}");
|
|
||||||
if (stopOut.Length > 0) log.AppendLine("STOP out: " + Truncate(stopOut));
|
|
||||||
if (stopErr.Length > 0) log.AppendLine("STOP err: " + Truncate(stopErr));
|
|
||||||
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
|
|
||||||
|
|
||||||
(bool startOk, string startOut, string startErr, int startCode) =
|
|
||||||
await RunBatAsync(startBat!, fullName, bin, cancellationToken);
|
|
||||||
log.AppendLine($"START ExitCode={startCode}");
|
|
||||||
if (startOut.Length > 0) log.AppendLine("START out: " + Truncate(startOut));
|
|
||||||
if (startErr.Length > 0) log.AppendLine("START err: " + Truncate(startErr));
|
|
||||||
|
|
||||||
if (startCode != 0
|
|
||||||
&& !string.Equals(shortName, fullName, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
log.AppendLine($"Retry START mit Kurzname '{shortName}'…");
|
|
||||||
(_, startOut, startErr, startCode) =
|
|
||||||
await RunBatAsync(startBat!, shortName, bin, cancellationToken);
|
|
||||||
log.AppendLine($"START(short) ExitCode={startCode}");
|
|
||||||
if (startOut.Length > 0) log.AppendLine("START out: " + Truncate(startOut));
|
|
||||||
if (startErr.Length > 0) log.AppendLine("START err: " + Truncate(startErr));
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(
|
|
||||||
TimeSpan.FromSeconds(Math.Clamp(_connection.PostRestartDelaySeconds, 0, 120)),
|
|
||||||
cancellationToken);
|
|
||||||
|
|
||||||
if (startCode != 0)
|
|
||||||
{
|
|
||||||
return (false, "Neustart fehlgeschlagen (SonicBin)",
|
|
||||||
$"startcontainer ExitCode={startCode} (stop ExitCode={stopCode}).\n{log}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (true, $"Container '{fullName}' neugestartet (SonicBin)", log.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sucht stop/startcontainer.bat unter SonicHome\bin und rekursiv (max. Tiefe 4).
|
|
||||||
/// </summary>
|
|
||||||
public static (string? StopBat, string? StartBat, string Note) LocateContainerScripts(string sonicHome)
|
|
||||||
{
|
|
||||||
List<string> candidates = [];
|
|
||||||
|
|
||||||
void AddDir(string? dir)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(dir) && Directory.Exists(dir) && !candidates.Contains(dir))
|
|
||||||
{
|
|
||||||
candidates.Add(dir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddDir(Path.Combine(sonicHome, "bin"));
|
|
||||||
AddDir(Path.Combine(sonicHome, "MQ_HOME", "bin"));
|
|
||||||
AddDir(Path.Combine(sonicHome, "MQ", "bin"));
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string? parent = Directory.GetParent(sonicHome)?.FullName;
|
|
||||||
if (parent is not null)
|
|
||||||
{
|
|
||||||
foreach (string child in Directory.EnumerateDirectories(parent))
|
|
||||||
{
|
|
||||||
AddDir(Path.Combine(child, "bin"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rekursiv nach Dateinamen suchen
|
|
||||||
try
|
|
||||||
{
|
|
||||||
foreach (string file in Directory.EnumerateFiles(sonicHome, "stopcontainer.bat", SearchOption.AllDirectories)
|
|
||||||
.Take(20))
|
|
||||||
{
|
|
||||||
AddDir(Path.GetDirectoryName(file));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// ignore permission issues
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (string dir in candidates)
|
|
||||||
{
|
|
||||||
string stop = Path.Combine(dir, "stopcontainer.bat");
|
|
||||||
string start = Path.Combine(dir, "startcontainer.bat");
|
|
||||||
if (File.Exists(stop) && File.Exists(start))
|
|
||||||
{
|
|
||||||
return (stop, start, $"Scripts gefunden in '{dir}'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (null, null, $"Keine Scripts in {candidates.Count} geprüften bin-Ordnern.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string DescribeDirectory(string dir)
|
|
||||||
{
|
|
||||||
if (!Directory.Exists(dir))
|
|
||||||
{
|
|
||||||
return "(Ordner existiert nicht)";
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string[] names = Directory.GetFileSystemEntries(dir)
|
|
||||||
.Select(Path.GetFileName)
|
|
||||||
.Where(n => n is not null)
|
|
||||||
.Cast<string>()
|
|
||||||
.OrderBy(n => n)
|
|
||||||
.Take(25)
|
|
||||||
.ToArray();
|
|
||||||
return names.Length == 0 ? "(leer)" : string.Join(", ", names);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return $"(nicht lesbar: {ex.Message})";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<(bool Started, string StdOut, string StdErr, int ExitCode)> RunBatAsync(
|
|
||||||
string batPath,
|
|
||||||
string argument,
|
|
||||||
string workingDirectory,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
ProcessStartInfo psi = new()
|
|
||||||
{
|
|
||||||
FileName = "cmd.exe",
|
|
||||||
Arguments = $"/c \"\"{batPath}\" \"{argument}\"\"",
|
|
||||||
WorkingDirectory = workingDirectory,
|
|
||||||
UseShellExecute = false,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
CreateNoWindow = true,
|
|
||||||
StandardOutputEncoding = Encoding.UTF8,
|
|
||||||
StandardErrorEncoding = Encoding.UTF8
|
|
||||||
};
|
|
||||||
|
|
||||||
using Process process = new() { StartInfo = psi };
|
|
||||||
if (!process.Start())
|
|
||||||
{
|
|
||||||
return (false, string.Empty, "cmd.exe konnte nicht gestartet werden.", -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
Task<string> stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
|
|
||||||
Task<string> stderrTask = process.StandardError.ReadToEndAsync(cancellationToken);
|
|
||||||
|
|
||||||
await process.WaitForExitAsync(cancellationToken);
|
|
||||||
return (true, (await stdoutTask).Trim(), (await stderrTask).Trim(), process.ExitCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Truncate(string value, int max = 500)
|
|
||||||
=> value.Length <= max ? value : value[..max] + "…";
|
|
||||||
}
|
|
||||||
@@ -2,10 +2,6 @@ using ZA.CoreService.ESBCertificateManager.Models;
|
|||||||
|
|
||||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fragt konfigurierte Sonic-Verbindungen nach Containern ab
|
|
||||||
/// und merged Remote-Treffer mit KnownContainers / Sample-Zielen.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class SonicContainerDiscovery
|
public sealed class SonicContainerDiscovery
|
||||||
{
|
{
|
||||||
private readonly IReadOnlyList<SonicConnection> _connections;
|
private readonly IReadOnlyList<SonicConnection> _connections;
|
||||||
@@ -29,228 +25,84 @@ public sealed class SonicContainerDiscovery
|
|||||||
if (connection is null)
|
if (connection is null)
|
||||||
{
|
{
|
||||||
return SonicDiscoveryResult.Failed(connectionName,
|
return SonicDiscoveryResult.Failed(connectionName,
|
||||||
$"Sonic-Verbindung '{connectionName}' ist nicht in AppSettings konfiguriert.");
|
$"Sonic-Verbindung '{connectionName}' fehlt in appsettings.");
|
||||||
}
|
}
|
||||||
|
|
||||||
using SonicManagementClient client = new(connection);
|
using SonicManagementClient client = new(connection);
|
||||||
|
(bool reachable, string? pingError, _) = await client.CheckConnectionAsync(cancellationToken);
|
||||||
|
|
||||||
(bool reachable, string? pingError, string? resolvedPath) = await client.CheckConnectionAsync(cancellationToken);
|
|
||||||
|
|
||||||
List<string> diagnostics = [];
|
|
||||||
List<string> remoteContainers = [];
|
List<string> remoteContainers = [];
|
||||||
bool listOk = false;
|
List<string> diagnostics = [];
|
||||||
string? listError = null;
|
|
||||||
|
|
||||||
if (!reachable)
|
if (!reachable)
|
||||||
{
|
{
|
||||||
// Trotzdem KnownContainers nutzen – Neustart kann über SonicBin (stop/startcontainer) gehen.
|
diagnostics.Add($"Ping: {pingError}");
|
||||||
diagnostics.Add($"Management-Ping: {pingError}");
|
|
||||||
if (connection.KnownContainers.Count == 0
|
|
||||||
&& !knownTargets.Any(t =>
|
|
||||||
string.Equals(t.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& !string.IsNullOrWhiteSpace(t.ContainerName)))
|
|
||||||
{
|
|
||||||
return SonicDiscoveryResult.Failed(connectionName,
|
|
||||||
$"Management-Konsole nicht erreichbar: {pingError}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
(listOk, IReadOnlyList<string> remoteNames, listError) =
|
(bool listOk, IReadOnlyList<string> remoteNames, string? listError) =
|
||||||
await client.GetContainersAsync(cancellationToken);
|
await client.GetContainersAsync(cancellationToken);
|
||||||
|
if (listOk)
|
||||||
foreach (string line in remoteNames)
|
|
||||||
{
|
{
|
||||||
if (line.StartsWith("WARN:", StringComparison.OrdinalIgnoreCase)
|
remoteContainers.AddRange(remoteNames.Where(n =>
|
||||||
|| line.StartsWith("INFO:", StringComparison.OrdinalIgnoreCase))
|
!n.StartsWith("WARN:", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !n.StartsWith("INFO:", StringComparison.OrdinalIgnoreCase)));
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrWhiteSpace(listError))
|
||||||
{
|
{
|
||||||
diagnostics.Add(line);
|
diagnostics.Add(listError);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
remoteContainers.Add(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!listOk)
|
|
||||||
{
|
|
||||||
diagnostics.Add($"Listen-Fehler: {listError}");
|
|
||||||
remoteContainers = [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: explizit in appsettings hinterlegte Container
|
List<string> merged = [];
|
||||||
List<string> mergedNames = MergeContainerNames(
|
foreach (string name in remoteContainers
|
||||||
remoteContainers,
|
.Concat(connection.KnownContainers)
|
||||||
connection.KnownContainers,
|
.Concat(knownTargets
|
||||||
knownTargets
|
|
||||||
.Where(t => string.Equals(t.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase))
|
.Where(t => string.Equals(t.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase))
|
||||||
.Select(t => t.ContainerName)
|
.Select(t => t.ContainerName))
|
||||||
.Where(n => !string.IsNullOrWhiteSpace(n)));
|
.Where(n => !string.IsNullOrWhiteSpace(n)))
|
||||||
|
{
|
||||||
|
if (!merged.Any(m => string.Equals(m, name, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
merged.Add(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
List<DeploymentTarget> discovered = BuildTargets(connection, mergedNames, knownTargets);
|
if (merged.Count == 0)
|
||||||
|
{
|
||||||
|
return SonicDiscoveryResult.Failed(connectionName,
|
||||||
|
string.Join(" | ", diagnostics.DefaultIfEmpty("Keine Container gefunden.")));
|
||||||
|
}
|
||||||
|
|
||||||
string? hint = BuildHint(connection, remoteContainers.Count, connection.KnownContainers.Count, diagnostics, listOk, listError);
|
int id = 1;
|
||||||
|
List<DeploymentTarget> targets = merged.Select(name =>
|
||||||
|
{
|
||||||
|
DeploymentTarget? known = knownTargets.FirstOrDefault(t =>
|
||||||
|
string.Equals(t.ContainerName, name, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& string.Equals(t.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
return new DeploymentTarget
|
||||||
|
{
|
||||||
|
Id = known?.Id ?? id++,
|
||||||
|
Name = known?.Name ?? $"{connection.Name} / {name}",
|
||||||
|
Environment = known?.Environment ?? "TEST",
|
||||||
|
IsActive = true,
|
||||||
|
ContainerName = name,
|
||||||
|
SonicConnectionName = connection.Name,
|
||||||
|
SortOrder = known?.SortOrder ?? id * 10
|
||||||
|
};
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
return new SonicDiscoveryResult
|
return new SonicDiscoveryResult
|
||||||
{
|
{
|
||||||
ConnectionName = connectionName,
|
ConnectionName = connectionName,
|
||||||
DomainName = connection.DomainName,
|
DomainName = connection.DomainName,
|
||||||
Success = true,
|
Success = true,
|
||||||
ErrorMessage = hint,
|
ErrorMessage = diagnostics.Count == 0 ? null : string.Join(" | ", diagnostics),
|
||||||
DiscoveredTargets = discovered,
|
DiscoveredTargets = targets,
|
||||||
RawContainerNames = remoteContainers,
|
RawContainerNames = remoteContainers
|
||||||
ResolvedPath = resolvedPath
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<string> MergeContainerNames(
|
|
||||||
IEnumerable<string> remote,
|
|
||||||
IEnumerable<string> knownConfigured,
|
|
||||||
IEnumerable<string> knownFromTargets)
|
|
||||||
{
|
|
||||||
List<string> result = [];
|
|
||||||
|
|
||||||
foreach (string name in remote.Concat(knownConfigured).Concat(knownFromTargets))
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.Any(existing => ContainerNamesMatch(existing, name, domain: null)))
|
|
||||||
{
|
|
||||||
result.Add(name.Trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string? BuildHint(
|
|
||||||
SonicConnection connection,
|
|
||||||
int remoteCount,
|
|
||||||
int knownConfigCount,
|
|
||||||
List<string> diagnostics,
|
|
||||||
bool listOk,
|
|
||||||
string? listError)
|
|
||||||
{
|
|
||||||
List<string> parts = [];
|
|
||||||
|
|
||||||
if (!listOk && !string.IsNullOrWhiteSpace(listError))
|
|
||||||
{
|
|
||||||
parts.Add(listError!);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remoteCount == 0)
|
|
||||||
{
|
|
||||||
parts.Add(
|
|
||||||
$"Remote hat 0 Container geliefert (Domain '{connection.DomainName}', SonicHome='{connection.SonicHome}').");
|
|
||||||
|
|
||||||
if (knownConfigCount > 0)
|
|
||||||
{
|
|
||||||
parts.Add($"Fallback: {knownConfigCount} KnownContainers aus appsettings.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
parts.Add("Tipp: KnownContainers in appsettings setzen oder SonicHome korrigieren.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (string d in diagnostics.Take(3))
|
|
||||||
{
|
|
||||||
parts.Add(d);
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts.Count == 0 ? null : string.Join(" ", parts);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<DeploymentTarget> BuildTargets(
|
|
||||||
SonicConnection connection,
|
|
||||||
IReadOnlyList<string> containerNames,
|
|
||||||
IReadOnlyList<DeploymentTarget> knownTargets)
|
|
||||||
{
|
|
||||||
List<DeploymentTarget> result = [];
|
|
||||||
int syntheticId = -1;
|
|
||||||
|
|
||||||
foreach (string containerName in containerNames)
|
|
||||||
{
|
|
||||||
DeploymentTarget? existing = knownTargets.FirstOrDefault(t =>
|
|
||||||
string.Equals(t.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase)
|
|
||||||
&& ContainerNamesMatch(t.ContainerName, containerName, connection.DomainName));
|
|
||||||
|
|
||||||
if (existing is not null)
|
|
||||||
{
|
|
||||||
if (!result.Any(r => r.Id == existing.Id))
|
|
||||||
{
|
|
||||||
result.Add(existing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result.Add(new DeploymentTarget
|
|
||||||
{
|
|
||||||
Id = syntheticId--,
|
|
||||||
Name = $"{connection.Name} / {containerName}",
|
|
||||||
Environment = connection.DomainName,
|
|
||||||
IsActive = true,
|
|
||||||
TargetDirectory = string.Empty,
|
|
||||||
CertificateFileName = string.Empty,
|
|
||||||
ContainerName = containerName,
|
|
||||||
RestartType = RestartType.SonicContainer,
|
|
||||||
SonicConnectionName = connection.Name,
|
|
||||||
SortOrder = result.Count * 10
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (DeploymentTarget known in knownTargets)
|
|
||||||
{
|
|
||||||
if (!string.Equals(known.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool alreadyAdded = result.Any(r =>
|
|
||||||
ContainerNamesMatch(r.ContainerName, known.ContainerName, connection.DomainName)
|
|
||||||
|| r.Id == known.Id);
|
|
||||||
|
|
||||||
if (!alreadyAdded)
|
|
||||||
{
|
|
||||||
result.Add(known);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool ContainerNamesMatch(string? a, string? b, string? domain)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(a, b, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
string aShort = StripDomainPrefix(a, domain);
|
|
||||||
string bShort = StripDomainPrefix(b, domain);
|
|
||||||
return string.Equals(aShort, bShort, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string StripDomainPrefix(string name, string? domain)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(domain)
|
|
||||||
&& name.StartsWith(domain + ".", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return name[(domain.Length + 1)..];
|
|
||||||
}
|
|
||||||
|
|
||||||
int dot = name.IndexOf('.');
|
|
||||||
return dot > 0 ? name[(dot + 1)..] : name;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class SonicDiscoveryResult
|
public sealed class SonicDiscoveryResult
|
||||||
@@ -259,10 +111,14 @@ public sealed class SonicDiscoveryResult
|
|||||||
public string DomainName { get; init; } = string.Empty;
|
public string DomainName { get; init; } = string.Empty;
|
||||||
public bool Success { get; init; }
|
public bool Success { get; init; }
|
||||||
public string? ErrorMessage { get; init; }
|
public string? ErrorMessage { get; init; }
|
||||||
public string? ResolvedPath { get; init; }
|
|
||||||
public IReadOnlyList<DeploymentTarget> DiscoveredTargets { get; init; } = [];
|
public IReadOnlyList<DeploymentTarget> DiscoveredTargets { get; init; } = [];
|
||||||
public IReadOnlyList<string> RawContainerNames { get; init; } = [];
|
public IReadOnlyList<string> RawContainerNames { get; init; } = [];
|
||||||
|
|
||||||
public static SonicDiscoveryResult Failed(string connectionName, string error)
|
public static SonicDiscoveryResult Failed(string connectionName, string error)
|
||||||
=> new() { ConnectionName = connectionName, Success = false, ErrorMessage = error };
|
=> new()
|
||||||
|
{
|
||||||
|
ConnectionName = connectionName,
|
||||||
|
Success = false,
|
||||||
|
ErrorMessage = error
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,339 +1,67 @@
|
|||||||
using System.Net;
|
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
|
||||||
using ZA.CoreService.ESBCertificateManager.Models;
|
using ZA.CoreService.ESBCertificateManager.Models;
|
||||||
|
|
||||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Verwaltet Sonic ESB Container über die Management Console.
|
/// Dünne Fassade über die Sonic MfApi (wie SMC-Neustart).
|
||||||
///
|
|
||||||
/// Modus = MfApi (Standard, laut Aurea CX Messenger Doku-Index → Management Application API):
|
|
||||||
/// Dieselbe Aktion wie „Restart“ in der Sonic Management Console:
|
|
||||||
/// JMSConnectorClient → MFProxyFactory.createAgentProxy → IAgentProxy.restart
|
|
||||||
/// über ConnectionUrl + SMC-User/Pass aus appsettings.
|
|
||||||
/// stopcontainer.bat wird NICHT verwendet (existiert in vielen SMC-only Installationen nicht).
|
|
||||||
///
|
|
||||||
/// Modus = LocalCmd / WinRm: nur optional, wenn Server-Scripts vorhanden sind.
|
|
||||||
/// Modus = HttpApi: REST falls vorhanden.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class SonicManagementClient : IDisposable
|
public sealed class SonicManagementClient : IDisposable
|
||||||
{
|
{
|
||||||
private readonly SonicConnection _connection;
|
private readonly SonicConnection _connection;
|
||||||
private readonly HttpClient? _http;
|
private readonly SonicMfApiExecutor _mfApi;
|
||||||
private readonly WinRmExecutor? _scriptRunner;
|
|
||||||
private readonly SonicMfApiExecutor? _mfApi;
|
|
||||||
|
|
||||||
// Gecachter HTTP-Basis-Pfad (nur im HttpApi-Modus)
|
|
||||||
private string? _resolvedContainerBasePath;
|
|
||||||
|
|
||||||
private static readonly string[] CandidateContainerPaths =
|
|
||||||
[
|
|
||||||
"/mf/rest/v1/domains/{domain}/containers",
|
|
||||||
"/api/v1/domains/{domain}/containers",
|
|
||||||
"/sonic/management/domains/{domain}/containers",
|
|
||||||
"/containers"
|
|
||||||
];
|
|
||||||
|
|
||||||
private SonicManagementMode Mode => _connection.EffectiveManagementMode;
|
|
||||||
|
|
||||||
private bool UsesMfApi => Mode == SonicManagementMode.MfApi;
|
|
||||||
|
|
||||||
private bool UsesScripts =>
|
|
||||||
Mode is SonicManagementMode.WinRm or SonicManagementMode.LocalCmd;
|
|
||||||
|
|
||||||
public SonicManagementClient(SonicConnection connection)
|
public SonicManagementClient(SonicConnection connection)
|
||||||
{
|
{
|
||||||
_connection = connection;
|
_connection = connection;
|
||||||
|
|
||||||
if (UsesMfApi)
|
|
||||||
{
|
|
||||||
_mfApi = new SonicMfApiExecutor(connection);
|
_mfApi = new SonicMfApiExecutor(connection);
|
||||||
}
|
}
|
||||||
else if (UsesScripts)
|
|
||||||
{
|
|
||||||
_scriptRunner = new WinRmExecutor(connection);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
HttpClientHandler handler = new()
|
|
||||||
{
|
|
||||||
ServerCertificateCustomValidationCallback =
|
|
||||||
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
|
||||||
};
|
|
||||||
|
|
||||||
_http = new HttpClient(handler)
|
public async Task<(bool Reachable, string? Error, string? Detail)> CheckConnectionAsync(
|
||||||
{
|
|
||||||
BaseAddress = BuildHttpBaseUri(connection.ConnectionUrl, connection.ManagementHttpPort),
|
|
||||||
Timeout = TimeSpan.FromSeconds(Math.Clamp(connection.TimeoutSeconds, 5, 300))
|
|
||||||
};
|
|
||||||
|
|
||||||
string credentials = Convert.ToBase64String(
|
|
||||||
Encoding.UTF8.GetBytes($"{connection.Username}:{connection.Password}"));
|
|
||||||
_http.DefaultRequestHeaders.Authorization =
|
|
||||||
new AuthenticationHeaderValue("Basic", credentials);
|
|
||||||
_http.DefaultRequestHeaders.Accept.Add(
|
|
||||||
new MediaTypeWithQualityHeaderValue("application/json"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
// Öffentliche API
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prüft die Verbindung zur Management Console.
|
|
||||||
/// MfApi: Domain-Manager über ConnectionUrl + SMC-Credentials.
|
|
||||||
/// WinRm/LocalCmd: Script-Laufzeit.
|
|
||||||
/// Http: Probe gegen bekannte API-Pfade.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<(bool Success, string? Error, string? ResolvedPath)> CheckConnectionAsync(
|
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (UsesMfApi)
|
(bool ok, string? error) = await _mfApi.TestConnectionAsync(cancellationToken);
|
||||||
{
|
|
||||||
(bool ok, string? error) = await _mfApi!.TestConnectionAsync(cancellationToken);
|
|
||||||
if (ok)
|
if (ok)
|
||||||
{
|
{
|
||||||
return (true, null,
|
return (true, null, $"MfApi {_connection.ConnectionUrl} Domain={_connection.DomainName}");
|
||||||
$"MfApi {_connection.ConnectionUrl} Domain={_connection.DomainName}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ohne lokales Java: Domain-Manager-TCP + KnownContainers reichen für Discovery.
|
|
||||||
bool javaMissing = error?.Contains("Java nicht gefunden", StringComparison.OrdinalIgnoreCase) == true;
|
|
||||||
if (javaMissing)
|
|
||||||
{
|
|
||||||
SonicBinRestartExecutor bin = new(_connection);
|
|
||||||
(bool binOk, _, string? binDetail) = bin.Probe();
|
|
||||||
if (binOk)
|
|
||||||
{
|
|
||||||
return (true, null,
|
|
||||||
$"SonicBin Fallback (kein Java für MfApi) {binDetail}; {_connection.ConnectionUrl}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Soft-Fallback: KnownContainers + TCP reichen für Discovery
|
||||||
if (_connection.KnownContainers.Count > 0
|
if (_connection.KnownContainers.Count > 0
|
||||||
|| await IsTcpReachableAsync(_connection.ConnectionUrl, cancellationToken))
|
&& await IsTcpReachableAsync(_connection.ConnectionUrl, cancellationToken))
|
||||||
{
|
{
|
||||||
return (true, null,
|
return (true, null,
|
||||||
$"MfApi ohne Java – KnownContainers/TCP; Hinweis: {Truncate(error ?? string.Empty, 180)}");
|
$"MfApi-Ping fehlgeschlagen, KnownContainers/TCP ok. Hinweis: {Truncate(error)}");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (false, error, null);
|
return (false, error ?? "MfApi-Verbindung fehlgeschlagen", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (UsesScripts)
|
|
||||||
{
|
|
||||||
if (Mode == SonicManagementMode.LocalCmd)
|
|
||||||
{
|
|
||||||
(bool binOk, string? binError, string? binDetail) = new SonicBinRestartExecutor(_connection).Probe();
|
|
||||||
return binOk
|
|
||||||
? (true, null, $"LocalCmd/SonicBin {binDetail}")
|
|
||||||
: (false, binError, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
(bool ok, string? error) = await _scriptRunner!.TestConnectionAsync(cancellationToken);
|
|
||||||
return ok
|
|
||||||
? (true, null, $"WinRM auf {ExtractHost(_connection.ConnectionUrl)}:{_connection.WinRmPort}")
|
|
||||||
: (false, error, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string? path = await ResolveContainerBasePathAsync(cancellationToken);
|
|
||||||
if (path is null)
|
|
||||||
{
|
|
||||||
return (false,
|
|
||||||
$"Keine Sonic HTTP-API unter {_http!.BaseAddress} gefunden.\n" +
|
|
||||||
$"Geprüfte Pfade: {string.Join(", ", GetCandidatePaths())}\n" +
|
|
||||||
$"Tipp: ManagementMode auf 'MfApi' setzen (ConnectionUrl + SMC-Logins).",
|
|
||||||
null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (true, null, path);
|
|
||||||
}
|
|
||||||
catch (TaskCanceledException) when (!cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
return (false,
|
|
||||||
$"HTTP-Timeout nach {_connection.TimeoutSeconds}s – Port {_connection.ManagementHttpPort} nicht erreichbar.\n" +
|
|
||||||
$"Tipp: ManagementMode auf 'MfApi' setzen.",
|
|
||||||
null);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return (false, ex.Message, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Listet alle Container der Domain auf.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<(bool Success, IReadOnlyList<string> ContainerNames, string? Error)> GetContainersAsync(
|
public async Task<(bool Success, IReadOnlyList<string> ContainerNames, string? Error)> GetContainersAsync(
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (UsesMfApi)
|
(bool ok, IReadOnlyList<string> names, string? error) =
|
||||||
|
await _mfApi.ListContainersAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (ok && names.Count > 0)
|
||||||
{
|
{
|
||||||
return await GetContainersViaMfApiAsync(cancellationToken);
|
return (true, names, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (UsesScripts)
|
if (_connection.KnownContainers.Count > 0)
|
||||||
{
|
{
|
||||||
return await GetContainersViaScriptAsync(cancellationToken);
|
return (true, _connection.KnownContainers,
|
||||||
|
ok ? null : $"Liste leer/fehlerhaft – KnownContainers. {error}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return await GetContainersViaHttpAsync(cancellationToken);
|
return (false, [], error ?? "Keine Container gefunden.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Startet den Container neu.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<(bool Success, string Status, string? Detail)> RestartContainerAsync(
|
public async Task<(bool Success, string Status, string? Detail)> RestartContainerAsync(
|
||||||
string containerName,
|
string containerName,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
|
||||||
if (UsesMfApi)
|
|
||||||
{
|
|
||||||
return await RestartViaMfApiAsync(containerName, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (UsesScripts)
|
|
||||||
{
|
|
||||||
if (Mode == SonicManagementMode.LocalCmd)
|
|
||||||
{
|
|
||||||
(bool binOk, string binStatus, string? binDetail) =
|
|
||||||
await new SonicBinRestartExecutor(_connection)
|
|
||||||
.RestartAsync(containerName, cancellationToken);
|
|
||||||
|
|
||||||
if (binOk)
|
|
||||||
{
|
|
||||||
return (true, binStatus, binDetail);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reine SMC/Client-Installation ohne Server-bin → wie die Console selbst
|
|
||||||
// über ConnectionUrl + SMC-Login (MfApi) neu starten.
|
|
||||||
bool batsMissing = binDetail?.Contains("stopcontainer", StringComparison.OrdinalIgnoreCase) == true
|
|
||||||
|| binStatus.Contains("SonicBin", StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
if (batsMissing)
|
|
||||||
{
|
|
||||||
SonicMfApiExecutor mf = new(_connection);
|
|
||||||
(bool mfOk, string? mfOut, string? mfErr) =
|
|
||||||
await mf.RestartAsync(containerName, cancellationToken);
|
|
||||||
if (mfOk)
|
|
||||||
{
|
|
||||||
await Task.Delay(
|
|
||||||
TimeSpan.FromSeconds(Math.Clamp(_connection.PostRestartDelaySeconds, 0, 120)),
|
|
||||||
cancellationToken);
|
|
||||||
return (true, $"Container '{containerName}' neugestartet (SMC/MfApi)",
|
|
||||||
$"Kein stopcontainer.bat unter SonicHome – SMC-Verbindung genutzt.\n{mfOut}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (false, "Neustart fehlgeschlagen",
|
|
||||||
"Ursache: Unter SonicHome fehlen stopcontainer.bat/startcontainer.bat.\n" +
|
|
||||||
"Das ist typisch, wenn nur die Sonic Management Console (Client) installiert ist –\n" +
|
|
||||||
"die Scripts liegen auf dem Sonic-SERVER.\n\n" +
|
|
||||||
$"SonicBin: {binDetail}\n\n" +
|
|
||||||
$"SMC/MfApi-Fallback: {mfErr}\n{mfOut}\n\n" +
|
|
||||||
"Was tun:\n" +
|
|
||||||
"1) SonicHome auf den Server-Pfad setzen (Ordner mit bin\\stopcontainer.bat), ODER\n" +
|
|
||||||
"2) Java + Client-JARs unter SonicHome\\lib bereitstellen (wie SMC),\n" +
|
|
||||||
" ConnectionUrl/User/Pass = dieselben Werte wie beim SMC-Login.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (false, binStatus, binDetail);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await RestartViaScriptAsync(containerName, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await RestartViaHttpAsync(containerName, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Importiert XApi-Ressourcen und startet den Container neu.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<(bool Success, string Status, string? Detail)> ImportXapiAndRestartAsync(
|
|
||||||
string containerName,
|
|
||||||
string xapiSourcePath,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (UsesMfApi)
|
|
||||||
{
|
|
||||||
// XApi-Import bleibt script/HTTP; Neustart danach über MfApi.
|
|
||||||
if (!string.IsNullOrWhiteSpace(_connection.WinRmXapiImportScript))
|
|
||||||
{
|
|
||||||
WinRmExecutor local = new(CloneAsLocalCmd(_connection));
|
|
||||||
string script = WinRmExecutor.ApplyScriptTemplate(
|
|
||||||
_connection.WinRmXapiImportScript,
|
|
||||||
containerName,
|
|
||||||
_connection.DomainName,
|
|
||||||
_connection.SonicHome,
|
|
||||||
xapiSourcePath,
|
|
||||||
_connection.ConnectionUrl,
|
|
||||||
_connection.Username,
|
|
||||||
_connection.Password);
|
|
||||||
(bool importOk, string? importOut, string? importErr) =
|
|
||||||
await local.RunScriptAsync(script, cancellationToken);
|
|
||||||
if (!importOk)
|
|
||||||
{
|
|
||||||
return (false, "XApi-Import fehlgeschlagen", importErr ?? importOut);
|
|
||||||
}
|
|
||||||
|
|
||||||
(bool restartOk, string restartStatus, string? restartDetail) =
|
|
||||||
await RestartViaMfApiAsync(containerName, cancellationToken);
|
|
||||||
return restartOk
|
|
||||||
? (true, "XApi importiert + Container neugestartet (MfApi)",
|
|
||||||
$"Import: {importOut} | Restart: {restartDetail}")
|
|
||||||
: (false, restartStatus, restartDetail);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (false, "XApi-Import fehlgeschlagen",
|
|
||||||
"Im MfApi-Modus ist WinRmXapiImportScript für den Import nötig, " +
|
|
||||||
"oder ManagementMode vorübergehend auf LocalCmd/WinRm setzen.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (UsesScripts)
|
|
||||||
{
|
|
||||||
return await ImportXapiViaScriptAsync(containerName, xapiSourcePath, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await ImportXapiViaHttpAsync(containerName, xapiSourcePath, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
// MfApi (Sonic Domain Manager / IAgentProxy.restart)
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
|
|
||||||
private async Task<(bool, IReadOnlyList<string>, string?)> GetContainersViaMfApiAsync(
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
(bool ok, IReadOnlyList<string> names, string? error) =
|
|
||||||
await _mfApi!.ListContainersAsync(cancellationToken);
|
|
||||||
|
|
||||||
if (!ok)
|
|
||||||
{
|
|
||||||
return (false, [], $"Container-Liste fehlgeschlagen (MfApi): {error}");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (names.Count == 0 && _connection.KnownContainers.Count > 0)
|
|
||||||
{
|
|
||||||
List<string> withHint =
|
|
||||||
[
|
|
||||||
..names,
|
|
||||||
$"INFO:MfApiListeLeer FallbackKnownContainers={_connection.KnownContainers.Count}"
|
|
||||||
];
|
|
||||||
return (true, withHint, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (true, names, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<(bool, string, string?)> RestartViaMfApiAsync(
|
|
||||||
string containerName, CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
(bool ok, string? output, string? error) =
|
(bool ok, string? output, string? error) =
|
||||||
await _mfApi!.RestartAsync(containerName, cancellationToken);
|
await _mfApi.RestartAsync(containerName, cancellationToken);
|
||||||
|
|
||||||
if (ok)
|
if (ok)
|
||||||
{
|
{
|
||||||
@@ -341,27 +69,13 @@ public sealed class SonicManagementClient : IDisposable
|
|||||||
TimeSpan.FromSeconds(Math.Clamp(_connection.PostRestartDelaySeconds, 0, 120)),
|
TimeSpan.FromSeconds(Math.Clamp(_connection.PostRestartDelaySeconds, 0, 120)),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
return (true, $"Container '{containerName}' neugestartet (MfApi)",
|
return (true, $"Container '{containerName}' neugestartet",
|
||||||
$"Domain={_connection.DomainName}; ConnectionUrl={_connection.ConnectionUrl}\n{output}");
|
$"Domain={_connection.DomainName}; URL={_connection.ConnectionUrl}\n{output}");
|
||||||
}
|
}
|
||||||
|
|
||||||
string libHint = string.IsNullOrWhiteSpace(_connection.MfClientLibPath)
|
return (false, "Neustart fehlgeschlagen",
|
||||||
? _connection.SonicHome
|
|
||||||
: _connection.MfClientLibPath;
|
|
||||||
|
|
||||||
return (false, "Neustart fehlgeschlagen (MfApi)",
|
|
||||||
"Laut Doku (Management Application API / wie SMC-Restart):\n" +
|
|
||||||
"JMSConnectorClient + MFProxyFactory.createAgentProxy + IAgentProxy.restart\n\n" +
|
|
||||||
$"Container '{containerName}', Domain '{_connection.DomainName}', URL {_connection.ConnectionUrl}\n" +
|
$"Container '{containerName}', Domain '{_connection.DomainName}', URL {_connection.ConnectionUrl}\n" +
|
||||||
$"Fehler: {error}\nAusgabe: {output}\n\n" +
|
$"Fehler: {error}\n\n{output}");
|
||||||
"Benötigt (SMC-Installation):\n" +
|
|
||||||
$"- MfClientLibPath/SonicHome={libHint} (mgmt_client.jar, mfcontext.jar, sonic_Client.jar)\n" +
|
|
||||||
$"- JavaPath={_connection.JavaPath}\n" +
|
|
||||||
"- Dieselben ConnectionUrl/User/Pass wie beim SMC-Login\n" +
|
|
||||||
"SMC-ObjectName Beispiel: proalpha-test.ct-ZADBService:ID=AGENT → ContainerName=ct-ZADBService\n" +
|
|
||||||
"Hinweis: Bei 'unbounded client connector' nutzt das Tool MBean stop/restart (wie SMC),\n" +
|
|
||||||
"nicht nur IAgentProxy.restart.\n" +
|
|
||||||
"Hinweis: stopcontainer.bat wird nicht verwendet (SMC-only).");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<bool> IsTcpReachableAsync(string connectionUrl, CancellationToken cancellationToken)
|
private static async Task<bool> IsTcpReachableAsync(string connectionUrl, CancellationToken cancellationToken)
|
||||||
@@ -383,390 +97,12 @@ public sealed class SonicManagementClient : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SonicConnection CloneAsLocalCmd(SonicConnection source)
|
private static string Truncate(string? s, int max = 180)
|
||||||
=> new()
|
=> string.IsNullOrWhiteSpace(s) ? string.Empty
|
||||||
{
|
: s.Length <= max ? s : s[..max] + "…";
|
||||||
Name = source.Name,
|
|
||||||
DomainName = source.DomainName,
|
|
||||||
ConnectionUrl = source.ConnectionUrl,
|
|
||||||
Username = source.Username,
|
|
||||||
Password = source.Password,
|
|
||||||
WinRmUsername = source.WinRmUsername,
|
|
||||||
WinRmPassword = source.WinRmPassword,
|
|
||||||
ManagementMode = SonicManagementMode.LocalCmd,
|
|
||||||
SonicHome = source.SonicHome,
|
|
||||||
JavaHome = source.JavaHome,
|
|
||||||
JavaPath = source.JavaPath,
|
|
||||||
MfClientLibPath = source.MfClientLibPath,
|
|
||||||
KnownContainers = source.KnownContainers,
|
|
||||||
ManagementHttpPort = source.ManagementHttpPort,
|
|
||||||
ApiBasePath = source.ApiBasePath,
|
|
||||||
ContainerListPath = source.ContainerListPath,
|
|
||||||
ContainerRestartPath = source.ContainerRestartPath,
|
|
||||||
ContainerStopPath = source.ContainerStopPath,
|
|
||||||
ContainerStartPath = source.ContainerStartPath,
|
|
||||||
WinRmPort = source.WinRmPort,
|
|
||||||
WinRmRestartScript = source.WinRmRestartScript,
|
|
||||||
WinRmContainerListScript = source.WinRmContainerListScript,
|
|
||||||
WinRmXapiImportScript = source.WinRmXapiImportScript,
|
|
||||||
TimeoutSeconds = source.TimeoutSeconds,
|
|
||||||
PostRestartDelaySeconds = source.PostRestartDelaySeconds
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
// Script-Implementierungen (WinRM / LocalCmd)
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
|
|
||||||
private async Task<(bool, IReadOnlyList<string>, string?)> GetContainersViaScriptAsync(
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
string script = WinRmExecutor.ApplyScriptTemplate(
|
|
||||||
_connection.ResolveContainerListScript(),
|
|
||||||
containerName: string.Empty,
|
|
||||||
domainName: _connection.DomainName,
|
|
||||||
sonicHome: _connection.SonicHome,
|
|
||||||
connectionUrl: _connection.ConnectionUrl,
|
|
||||||
username: _connection.Username,
|
|
||||||
password: _connection.Password);
|
|
||||||
|
|
||||||
(bool ok, string? output, string? error) =
|
|
||||||
await _scriptRunner!.RunScriptAsync(script, cancellationToken);
|
|
||||||
|
|
||||||
if (!ok)
|
|
||||||
{
|
|
||||||
return (false, [], $"Container-Liste fehlgeschlagen ({Mode}): {error}");
|
|
||||||
}
|
|
||||||
|
|
||||||
List<string> names = (output ?? string.Empty)
|
|
||||||
.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
||||||
.Where(l => l.Length > 0)
|
|
||||||
.Where(l => !l.StartsWith("WARNING:", StringComparison.OrdinalIgnoreCase))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
// INFO:/WARN:-Zeilen aus dem Script bewusst durchreichen (Diagnose in Discovery)
|
|
||||||
return (true, names, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<(bool, string, string?)> RestartViaScriptAsync(
|
|
||||||
string containerName, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
string script = WinRmExecutor.ApplyScriptTemplate(
|
|
||||||
_connection.ResolveRestartScript(),
|
|
||||||
containerName,
|
|
||||||
_connection.DomainName,
|
|
||||||
_connection.SonicHome,
|
|
||||||
connectionUrl: _connection.ConnectionUrl,
|
|
||||||
username: _connection.Username,
|
|
||||||
password: _connection.Password);
|
|
||||||
|
|
||||||
(bool ok, string? output, string? error) =
|
|
||||||
await _scriptRunner!.RunScriptAsync(script, cancellationToken);
|
|
||||||
|
|
||||||
string mode = Mode.ToString();
|
|
||||||
bool verified = (output ?? string.Empty)
|
|
||||||
.Contains("OK:ContainerRestartVerified", StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
if (!ok || !verified)
|
|
||||||
{
|
|
||||||
return (false, $"Neustart fehlgeschlagen ({mode})",
|
|
||||||
$"Kein verifizierter Prozess-Neustart für Container '{containerName}' " +
|
|
||||||
$"(Domain '{_connection.DomainName}').\n" +
|
|
||||||
$"Fehler: {error}\nAusgabe: {output}\nSonicHome={_connection.SonicHome}");
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(
|
|
||||||
TimeSpan.FromSeconds(Math.Clamp(_connection.PostRestartDelaySeconds, 0, 120)),
|
|
||||||
cancellationToken);
|
|
||||||
|
|
||||||
return (true, $"Container '{containerName}' neugestartet ({mode})",
|
|
||||||
$"Domain={_connection.DomainName}; verifiziert.\n{output}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<(bool, string, string?)> ImportXapiViaScriptAsync(
|
|
||||||
string containerName, string xapiSourcePath, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(_connection.WinRmXapiImportScript))
|
|
||||||
{
|
|
||||||
return (false, "XApi-Import fehlgeschlagen",
|
|
||||||
"WinRmXapiImportScript ist nicht konfiguriert.");
|
|
||||||
}
|
|
||||||
|
|
||||||
string script = WinRmExecutor.ApplyScriptTemplate(
|
|
||||||
_connection.WinRmXapiImportScript,
|
|
||||||
containerName,
|
|
||||||
_connection.DomainName,
|
|
||||||
_connection.SonicHome,
|
|
||||||
xapiSourcePath,
|
|
||||||
_connection.ConnectionUrl,
|
|
||||||
_connection.Username,
|
|
||||||
_connection.Password);
|
|
||||||
|
|
||||||
(bool importOk, string? importOut, string? importErr) =
|
|
||||||
await _scriptRunner!.RunScriptAsync(script, cancellationToken);
|
|
||||||
|
|
||||||
if (!importOk)
|
|
||||||
{
|
|
||||||
return (false, "XApi-Import fehlgeschlagen", importErr ?? importOut);
|
|
||||||
}
|
|
||||||
|
|
||||||
(bool restartOk, string restartStatus, string? restartDetail) =
|
|
||||||
await RestartViaScriptAsync(containerName, cancellationToken);
|
|
||||||
|
|
||||||
return restartOk
|
|
||||||
? (true, "XApi importiert + Container neugestartet",
|
|
||||||
$"Import: {importOut} | Restart: {restartDetail}")
|
|
||||||
: (false, restartStatus, restartDetail);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
// HTTP-Implementierungen
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
|
|
||||||
private async Task<(bool, IReadOnlyList<string>, string?)> GetContainersViaHttpAsync(
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string? basePath = await ResolveContainerBasePathAsync(cancellationToken);
|
|
||||||
if (basePath is null)
|
|
||||||
{
|
|
||||||
return (false, [], "Kein HTTP-API-Pfad gefunden. ManagementMode auf 'WinRm' setzen?");
|
|
||||||
}
|
|
||||||
|
|
||||||
using HttpResponseMessage response = await _http!.GetAsync(basePath, cancellationToken);
|
|
||||||
if (!response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
return (false, [], $"HTTP {(int)response.StatusCode}: {response.ReasonPhrase}");
|
|
||||||
}
|
|
||||||
|
|
||||||
string json = await response.Content.ReadAsStringAsync(cancellationToken);
|
|
||||||
return (true, ParseContainerNames(json), null);
|
|
||||||
}
|
|
||||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
|
||||||
{
|
|
||||||
return (false, [], ex.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<(bool, string, string?)> RestartViaHttpAsync(
|
|
||||||
string containerName, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
string encoded = Uri.EscapeDataString(containerName);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string? basePath = await ResolveContainerBasePathAsync(cancellationToken);
|
|
||||||
if (basePath is null)
|
|
||||||
{
|
|
||||||
return (false, "Neustart fehlgeschlagen",
|
|
||||||
"Kein HTTP-API-Pfad gefunden. ManagementMode auf 'WinRm' setzen.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(_connection.ContainerRestartPath))
|
|
||||||
{
|
|
||||||
string customPath = ApplyTemplate(_connection.ContainerRestartPath, encoded);
|
|
||||||
(bool ok, _, string? d) = await PostAsync(customPath, cancellationToken);
|
|
||||||
if (ok) { await DelayAsync(cancellationToken); return (true, "Container neugestartet", d); }
|
|
||||||
}
|
|
||||||
|
|
||||||
(bool r1Ok, _, string? r1d) = await PostAsync($"{basePath}/{encoded}/restart", cancellationToken);
|
|
||||||
if (r1Ok) { await DelayAsync(cancellationToken); return (true, "Container neugestartet", r1d); }
|
|
||||||
|
|
||||||
(bool r2Ok, _, string? r2d) = await PutStateAsync(basePath, encoded, "running", cancellationToken);
|
|
||||||
if (r2Ok) { await DelayAsync(cancellationToken); return (true, "Container neugestartet (State-API)", r2d); }
|
|
||||||
|
|
||||||
return await StopThenStartAsync(basePath, encoded, cancellationToken);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) { throw; }
|
|
||||||
catch (Exception ex) { return (false, "Neustart fehlgeschlagen", ex.Message); }
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<(bool, string, string?)> ImportXapiViaHttpAsync(
|
|
||||||
string containerName, string xapiSourcePath, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (!File.Exists(xapiSourcePath))
|
|
||||||
{
|
|
||||||
return (false, "XApi-Import fehlgeschlagen", $"Quelldatei nicht gefunden: {xapiSourcePath}");
|
|
||||||
}
|
|
||||||
|
|
||||||
string encoded = Uri.EscapeDataString(containerName);
|
|
||||||
string? basePath = await ResolveContainerBasePathAsync(cancellationToken);
|
|
||||||
if (basePath is null) return (false, "XApi-Import fehlgeschlagen", "Kein HTTP-API-Pfad.");
|
|
||||||
|
|
||||||
string path = $"{basePath}/{encoded}/xapi/import";
|
|
||||||
await using FileStream fs = File.OpenRead(xapiSourcePath);
|
|
||||||
string mt = xapiSourcePath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) ? "application/zip" : "application/xml";
|
|
||||||
using StreamContent content = new(fs);
|
|
||||||
content.Headers.ContentType = new MediaTypeHeaderValue(mt);
|
|
||||||
using HttpResponseMessage rsp = await _http!.PostAsync(path, content, cancellationToken);
|
|
||||||
|
|
||||||
if (!rsp.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
string body = await rsp.Content.ReadAsStringAsync(cancellationToken);
|
|
||||||
return (false, "XApi-Import fehlgeschlagen", $"HTTP {(int)rsp.StatusCode}: {Truncate(body)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
(bool restartOk, string rs, string? rd) = await RestartViaHttpAsync(containerName, cancellationToken);
|
|
||||||
return restartOk
|
|
||||||
? (true, "XApi importiert + Container neugestartet", rd)
|
|
||||||
: (false, rs, rd);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
// HTTP-Pfad-Erkennung
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
|
|
||||||
private async Task<string?> ResolveContainerBasePathAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (_resolvedContainerBasePath is not null) return _resolvedContainerBasePath;
|
|
||||||
|
|
||||||
foreach (string p in GetCandidatePaths())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using HttpResponseMessage r = await _http!.GetAsync(p, cancellationToken);
|
|
||||||
if (r.IsSuccessStatusCode || r.StatusCode == HttpStatusCode.Unauthorized)
|
|
||||||
{
|
|
||||||
_resolvedContainerBasePath = p;
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (HttpRequestException) { }
|
|
||||||
catch (TaskCanceledException) when (!cancellationToken.IsCancellationRequested) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<string> GetCandidatePaths()
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(_connection.ContainerListPath))
|
|
||||||
yield return ApplyTemplate(_connection.ContainerListPath, string.Empty).TrimEnd('/');
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(_connection.ApiBasePath))
|
|
||||||
{
|
|
||||||
string d = Uri.EscapeDataString(_connection.DomainName);
|
|
||||||
yield return $"{_connection.ApiBasePath.TrimEnd('/')}/domains/{d}/containers";
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (string pattern in CandidateContainerPaths)
|
|
||||||
yield return pattern.Replace("{domain}", Uri.EscapeDataString(_connection.DomainName), StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
// HTTP-Aktions-Helfer
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
|
|
||||||
private async Task<(bool, string, string?)> PostAsync(string path, CancellationToken ct)
|
|
||||||
{
|
|
||||||
using HttpResponseMessage r = await _http!.PostAsync(path, null, ct);
|
|
||||||
if (r.IsSuccessStatusCode)
|
|
||||||
return (true, $"OK ({(int)r.StatusCode})", $"POST {path} → {(int)r.StatusCode}");
|
|
||||||
string body = await r.Content.ReadAsStringAsync(ct);
|
|
||||||
return (false, $"Fehler ({(int)r.StatusCode})", $"POST {path} → {(int)r.StatusCode}: {Truncate(body)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<(bool, string, string?)> PutStateAsync(
|
|
||||||
string basePath, string encoded, string state, CancellationToken ct)
|
|
||||||
{
|
|
||||||
string path = $"{basePath}/{encoded}";
|
|
||||||
using StringContent body = new($"{{\"state\":\"{state}\"}}", Encoding.UTF8, "application/json");
|
|
||||||
using HttpResponseMessage r = await _http!.PutAsync(path, body, ct);
|
|
||||||
if (r.IsSuccessStatusCode)
|
|
||||||
return (true, $"State={state}", $"PUT {path} state={state} → {(int)r.StatusCode}");
|
|
||||||
string b = await r.Content.ReadAsStringAsync(ct);
|
|
||||||
return (false, "State fehlgeschlagen", $"PUT {path} → {(int)r.StatusCode}: {Truncate(b)}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<(bool, string, string?)> StopThenStartAsync(
|
|
||||||
string basePath, string encoded, CancellationToken ct)
|
|
||||||
{
|
|
||||||
string stopPath = string.IsNullOrWhiteSpace(_connection.ContainerStopPath)
|
|
||||||
? $"{basePath}/{encoded}/stop"
|
|
||||||
: ApplyTemplate(_connection.ContainerStopPath, encoded);
|
|
||||||
|
|
||||||
string startPath = string.IsNullOrWhiteSpace(_connection.ContainerStartPath)
|
|
||||||
? $"{basePath}/{encoded}/start"
|
|
||||||
: ApplyTemplate(_connection.ContainerStartPath, encoded);
|
|
||||||
|
|
||||||
(bool sOk, _, string? sd) = await PostAsync(stopPath, ct);
|
|
||||||
if (!sOk) return (false, "Container-Stop fehlgeschlagen", sd);
|
|
||||||
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(3), ct);
|
|
||||||
|
|
||||||
(bool stOk, _, string? std) = await PostAsync(startPath, ct);
|
|
||||||
if (!stOk) return (false, "Container-Start fehlgeschlagen", std);
|
|
||||||
|
|
||||||
await DelayAsync(ct);
|
|
||||||
return (true, "Container neugestartet (Stop+Start)", $"{sd} | {std}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DelayAsync(CancellationToken ct)
|
|
||||||
{
|
|
||||||
int d = Math.Clamp(_connection.PostRestartDelaySeconds, 0, 120);
|
|
||||||
if (d > 0) await Task.Delay(TimeSpan.FromSeconds(d), ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string ApplyTemplate(string template, string encodedName)
|
|
||||||
=> template
|
|
||||||
.Replace("{domain}", Uri.EscapeDataString(_connection.DomainName), StringComparison.OrdinalIgnoreCase)
|
|
||||||
.Replace("{container}", encodedName, StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
private static Uri BuildHttpBaseUri(string connectionUrl, int httpPort)
|
|
||||||
{
|
|
||||||
try { return new Uri($"http://{new Uri(connectionUrl).Host}:{httpPort}"); }
|
|
||||||
catch { return new Uri(connectionUrl); }
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ExtractHost(string connectionUrl)
|
|
||||||
{
|
|
||||||
try { return new Uri(connectionUrl).Host; }
|
|
||||||
catch { return connectionUrl; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<string> ParseContainerNames(string json)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using JsonDocument doc = JsonDocument.Parse(json);
|
|
||||||
JsonElement root = doc.RootElement;
|
|
||||||
List<string> names = [];
|
|
||||||
|
|
||||||
IEnumerable<JsonElement> elements = root.ValueKind == JsonValueKind.Array
|
|
||||||
? root.EnumerateArray()
|
|
||||||
: root.ValueKind == JsonValueKind.Object
|
|
||||||
? new[] { "containers", "data", "items", "result" }
|
|
||||||
.Where(k => root.TryGetProperty(k, out _))
|
|
||||||
.SelectMany(k =>
|
|
||||||
{
|
|
||||||
root.TryGetProperty(k, out JsonElement a);
|
|
||||||
return a.ValueKind == JsonValueKind.Array
|
|
||||||
? a.EnumerateArray()
|
|
||||||
: Enumerable.Empty<JsonElement>();
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
foreach (JsonElement el in elements)
|
|
||||||
{
|
|
||||||
string? name = el.ValueKind == JsonValueKind.String
|
|
||||||
? el.GetString()
|
|
||||||
: new[] { "name", "containerName", "id", "configId" }
|
|
||||||
.Where(k => el.TryGetProperty(k, out _))
|
|
||||||
.Select(k => { el.TryGetProperty(k, out JsonElement p); return p.GetString(); })
|
|
||||||
.FirstOrDefault();
|
|
||||||
if (name is not null) names.Add(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return names;
|
|
||||||
}
|
|
||||||
catch { return []; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Truncate(string v, int max = 400)
|
|
||||||
=> v.Length <= max ? v : v[..max] + "…";
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_http?.Dispose();
|
// nichts zu dispose'n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
using System.Net.Security;
|
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Security.Cryptography.X509Certificates;
|
|
||||||
using ZA.CoreService.ESBCertificateManager.Models;
|
|
||||||
|
|
||||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
|
||||||
|
|
||||||
public sealed class TlsCertificateProbe
|
|
||||||
{
|
|
||||||
private readonly int _timeoutSeconds;
|
|
||||||
private readonly int _retryCount;
|
|
||||||
|
|
||||||
public TlsCertificateProbe(int timeoutSeconds = 8, int retryCount = 2)
|
|
||||||
{
|
|
||||||
_timeoutSeconds = Math.Clamp(timeoutSeconds, 1, 60);
|
|
||||||
_retryCount = Math.Clamp(retryCount, 0, 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<(bool Success, string Status, string? Detail, string? ObservedFingerprint)> ProbeAsync(
|
|
||||||
DeploymentTarget target,
|
|
||||||
string expectedFingerprintSha256,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(target.TlsHost))
|
|
||||||
{
|
|
||||||
return (true, "TLS übersprungen", "Kein TlsHost konfiguriert.", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
int port = target.TlsPort is > 0 and <= 65535 ? target.TlsPort.Value : 443;
|
|
||||||
|
|
||||||
// TlsServerName überschreibt den SNI-Hostnamen wenn gesetzt (wichtig bei IP-Adressen)
|
|
||||||
string serverName = string.IsNullOrWhiteSpace(target.TlsServerName)
|
|
||||||
? target.TlsHost
|
|
||||||
: target.TlsServerName;
|
|
||||||
|
|
||||||
string expected = NormalizeFingerprint(expectedFingerprintSha256);
|
|
||||||
|
|
||||||
Exception? lastError = null;
|
|
||||||
|
|
||||||
for (int attempt = 0; attempt <= _retryCount; attempt++)
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await ProbeOnceAsync(
|
|
||||||
target.TlsHost,
|
|
||||||
port,
|
|
||||||
serverName,
|
|
||||||
expected,
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
lastError = ex;
|
|
||||||
if (attempt < _retryCount)
|
|
||||||
{
|
|
||||||
await Task.Delay(400, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
false,
|
|
||||||
"TLS nicht erreichbar",
|
|
||||||
lastError?.Message ?? $"Keine Verbindung zu {target.TlsHost}:{port}",
|
|
||||||
null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<(bool Success, string Status, string? Detail, string? ObservedFingerprint)> ProbeOnceAsync(
|
|
||||||
string host,
|
|
||||||
int port,
|
|
||||||
string serverName,
|
|
||||||
string expectedFingerprint,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
using TcpClient client = new();
|
|
||||||
using CancellationTokenSource timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
||||||
timeoutCts.CancelAfter(TimeSpan.FromSeconds(_timeoutSeconds));
|
|
||||||
|
|
||||||
await client.ConnectAsync(host, port, timeoutCts.Token);
|
|
||||||
|
|
||||||
await using SslStream sslStream = new(
|
|
||||||
client.GetStream(),
|
|
||||||
leaveInnerStreamOpen: false,
|
|
||||||
userCertificateValidationCallback: static (_, _, _, _) => true);
|
|
||||||
|
|
||||||
await sslStream.AuthenticateAsClientAsync(
|
|
||||||
new SslClientAuthenticationOptions
|
|
||||||
{
|
|
||||||
TargetHost = serverName, // SNI: muss zum CN/SAN im Zertifikat passen
|
|
||||||
EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls12
|
|
||||||
| System.Security.Authentication.SslProtocols.Tls13
|
|
||||||
},
|
|
||||||
timeoutCts.Token);
|
|
||||||
|
|
||||||
if (sslStream.RemoteCertificate is null)
|
|
||||||
{
|
|
||||||
return (false, "TLS Fail", "Kein Remote-Zertifikat erhalten.", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
using X509Certificate2 remote = new(sslStream.RemoteCertificate);
|
|
||||||
string actual = Convert.ToHexString(SHA256.HashData(remote.RawData));
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(expectedFingerprint))
|
|
||||||
{
|
|
||||||
// Kein erwarteter Fingerprint konfiguriert – nur Konnektivität prüfen
|
|
||||||
return (true, "TLS Pass (kein Fingerprint-Vergleich)", $"{host}:{port} erreichbar. Fingerprint={actual}", actual);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(actual, expectedFingerprint, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return (true, "TLS Pass", $"{host}:{port} Fingerprint stimmt überein.", actual);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
false,
|
|
||||||
"TLS Fail",
|
|
||||||
$"{host}:{port} Fingerprint weicht ab. Erwartet={expectedFingerprint}, Ist={actual}",
|
|
||||||
actual);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string NormalizeFingerprint(string fingerprint)
|
|
||||||
{
|
|
||||||
return fingerprint
|
|
||||||
.Replace(":", string.Empty, StringComparison.Ordinal)
|
|
||||||
.Replace(" ", string.Empty, StringComparison.Ordinal)
|
|
||||||
.ToUpperInvariant();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,268 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using System.Text;
|
|
||||||
using ZA.CoreService.ESBCertificateManager.Models;
|
|
||||||
|
|
||||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Führt PowerShell-Befehle lokal oder via WinRM (Invoke-Command) auf dem Sonic-Server aus.
|
|
||||||
/// Schreibt Skripte als .ps1 (UTF-8 ohne BOM) und startet sie mit -File,
|
|
||||||
/// um den bekannten stdin/BOM-Fehler zu vermeiden
|
|
||||||
/// ("$ErrorActionPreference wurde nicht als Name eines Cmdlet erkannt").
|
|
||||||
/// </summary>
|
|
||||||
public sealed class WinRmExecutor
|
|
||||||
{
|
|
||||||
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
|
|
||||||
|
|
||||||
private readonly SonicConnection _connection;
|
|
||||||
|
|
||||||
public WinRmExecutor(SonicConnection connection)
|
|
||||||
{
|
|
||||||
_connection = connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool HasExplicitWinRmCredentials
|
|
||||||
=> !string.IsNullOrWhiteSpace(_connection.WinRmUsername);
|
|
||||||
|
|
||||||
public async Task<(bool Success, string? Error)> TestConnectionAsync(
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (_connection.EffectiveManagementMode == SonicManagementMode.LocalCmd)
|
|
||||||
{
|
|
||||||
(bool ok, string? output, string? error) = await RunScriptAsync(
|
|
||||||
"$env:COMPUTERNAME", cancellationToken);
|
|
||||||
return ok
|
|
||||||
? (true, null)
|
|
||||||
: (false, $"LocalCmd fehlgeschlagen: {error ?? output}");
|
|
||||||
}
|
|
||||||
|
|
||||||
string host = ExtractHost(_connection.ConnectionUrl);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using System.Net.Sockets.TcpClient tcp = new();
|
|
||||||
using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
||||||
cts.CancelAfter(TimeSpan.FromSeconds(Math.Clamp(_connection.TimeoutSeconds, 5, 30)));
|
|
||||||
|
|
||||||
await tcp.ConnectAsync(host, _connection.WinRmPort, cts.Token);
|
|
||||||
}
|
|
||||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
|
||||||
{
|
|
||||||
return (false,
|
|
||||||
$"WinRM-Port {_connection.WinRmPort} auf '{host}' nicht erreichbar: {ex.Message}\n" +
|
|
||||||
"Auf dem Zielrechner: Enable-PSRemoting -Force\n" +
|
|
||||||
"Oder ManagementMode=LocalCmd setzen und die App auf dem Sonic-PC starten.");
|
|
||||||
}
|
|
||||||
|
|
||||||
(bool sessionOk, _, string? sessionError) = await RunScriptAsync(
|
|
||||||
"$env:COMPUTERNAME", cancellationToken);
|
|
||||||
|
|
||||||
return sessionOk
|
|
||||||
? (true, null)
|
|
||||||
: (false, sessionError ?? "WinRM-Verbindung fehlgeschlagen.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<(bool Success, string? Output, string? Error)> RunScriptAsync(
|
|
||||||
string scriptBlock,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
string fullScript = _connection.EffectiveManagementMode == SonicManagementMode.LocalCmd
|
|
||||||
? BuildLocalScript(scriptBlock)
|
|
||||||
: BuildRemoteScript(ExtractHost(_connection.ConnectionUrl), scriptBlock);
|
|
||||||
|
|
||||||
string tempFile = Path.Combine(
|
|
||||||
Path.GetTempPath(),
|
|
||||||
$"esb-winrm-{Guid.NewGuid():N}.ps1");
|
|
||||||
|
|
||||||
await File.WriteAllTextAsync(tempFile, fullScript, Utf8NoBom, cancellationToken);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ProcessStartInfo psi = new()
|
|
||||||
{
|
|
||||||
FileName = "powershell.exe",
|
|
||||||
Arguments = "-NonInteractive -NoProfile -ExecutionPolicy Bypass -File \"" + tempFile + "\"",
|
|
||||||
UseShellExecute = false,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
CreateNoWindow = true,
|
|
||||||
StandardOutputEncoding = Encoding.UTF8,
|
|
||||||
StandardErrorEncoding = Encoding.UTF8
|
|
||||||
};
|
|
||||||
|
|
||||||
using Process process = new() { StartInfo = psi };
|
|
||||||
|
|
||||||
if (!process.Start())
|
|
||||||
{
|
|
||||||
return (false, null, "PowerShell-Prozess konnte nicht gestartet werden.");
|
|
||||||
}
|
|
||||||
|
|
||||||
using CancellationTokenSource timeoutCts =
|
|
||||||
CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
||||||
timeoutCts.CancelAfter(TimeSpan.FromSeconds(Math.Clamp(_connection.TimeoutSeconds, 10, 600)));
|
|
||||||
|
|
||||||
Task<string> stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
|
|
||||||
Task<string> stderrTask = process.StandardError.ReadToEndAsync(cancellationToken);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await process.WaitForExitAsync(timeoutCts.Token);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
try { process.Kill(entireProcessTree: true); } catch { /* ignore */ }
|
|
||||||
return (false, null, $"Ausführung Timeout nach {_connection.TimeoutSeconds}s.");
|
|
||||||
}
|
|
||||||
|
|
||||||
string stdout = (await stdoutTask).Trim();
|
|
||||||
string stderr = (await stderrTask).Trim();
|
|
||||||
|
|
||||||
if (process.ExitCode == 0)
|
|
||||||
{
|
|
||||||
return (true, stdout, stderr.Length > 0 ? stderr : null);
|
|
||||||
}
|
|
||||||
|
|
||||||
string rawError = stderr.Length > 0
|
|
||||||
? stderr
|
|
||||||
: $"PowerShell ExitCode={process.ExitCode}";
|
|
||||||
|
|
||||||
string error = _connection.EffectiveManagementMode == SonicManagementMode.WinRm
|
|
||||||
? FormatWinRmFailure(rawError)
|
|
||||||
: Truncate(rawError);
|
|
||||||
|
|
||||||
return (false, stdout.Length > 0 ? stdout : null, error);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
try { File.Delete(tempFile); } catch { /* ignore */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildLocalScript(string scriptBlock)
|
|
||||||
=> "$ErrorActionPreference = 'Stop'\n" + scriptBlock;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Remote-Payload als Base64 einbetten und per [scriptblock]::Create ausführen.
|
|
||||||
/// Vermeidet verschachteltes ScriptBlock-Brace-Nesting und lokale $-Expansion.
|
|
||||||
/// WinRM-Credentials: nur <see cref="SonicConnection.WinRmUsername"/> / WinRmPassword.
|
|
||||||
/// Leer = aktueller Windows-Benutzer (ohne -Credential).
|
|
||||||
/// </summary>
|
|
||||||
private string BuildRemoteScript(string host, string scriptBlock)
|
|
||||||
{
|
|
||||||
string escapedHost = host.Replace("'", "''");
|
|
||||||
string remoteB64 = Convert.ToBase64String(Encoding.Unicode.GetBytes(scriptBlock));
|
|
||||||
|
|
||||||
StringBuilder sb = new();
|
|
||||||
sb.Append("$ErrorActionPreference = 'Stop'\n");
|
|
||||||
|
|
||||||
if (HasExplicitWinRmCredentials)
|
|
||||||
{
|
|
||||||
string escapedPwd = (_connection.WinRmPassword ?? string.Empty).Replace("'", "''");
|
|
||||||
string escapedUser = _connection.WinRmUsername.Replace("'", "''");
|
|
||||||
sb.Append($"$secPwd = ConvertTo-SecureString '{escapedPwd}' -AsPlainText -Force\n");
|
|
||||||
sb.Append($"$cred = New-Object System.Management.Automation.PSCredential('{escapedUser}', $secPwd)\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.Append($"$remoteB64 = '{remoteB64}'\n");
|
|
||||||
sb.Append("$remoteScript = [System.Text.Encoding]::Unicode.GetString(");
|
|
||||||
sb.Append("[System.Convert]::FromBase64String($remoteB64))\n");
|
|
||||||
sb.Append("$sb = [scriptblock]::Create($remoteScript)\n");
|
|
||||||
sb.Append($"Invoke-Command -ComputerName '{escapedHost}' -Port {_connection.WinRmPort} ");
|
|
||||||
|
|
||||||
if (HasExplicitWinRmCredentials)
|
|
||||||
{
|
|
||||||
sb.Append("-Credential $cred ");
|
|
||||||
}
|
|
||||||
|
|
||||||
sb.Append("-ScriptBlock $sb -ErrorAction Stop");
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Erkennt typische WinRM-/Windows-Auth-Fehler und liefert eine klare Handlungsanweisung.
|
|
||||||
/// Username/Password in appsettings sind Sonic-SMC – nicht Windows/WinRM.
|
|
||||||
/// </summary>
|
|
||||||
private string FormatWinRmFailure(string rawError)
|
|
||||||
{
|
|
||||||
string truncated = Truncate(rawError);
|
|
||||||
if (!LooksLikeWinRmAuthFailure(rawError))
|
|
||||||
{
|
|
||||||
return truncated;
|
|
||||||
}
|
|
||||||
|
|
||||||
string authHint = HasExplicitWinRmCredentials
|
|
||||||
? "WinRmUsername/WinRmPassword prüfen (Windows-Konto mit WinRM-Rechten auf dem Zielrechner)."
|
|
||||||
: "Aktueller Windows-Benutzer hat keine WinRM-Berechtigung auf dem Zielrechner " +
|
|
||||||
"(oder Kerberos/CredSSP fehlt). WinRmUsername/WinRmPassword setzen " +
|
|
||||||
"oder App unter einem berechtigten Windows-Konto starten.";
|
|
||||||
|
|
||||||
return
|
|
||||||
"WinRM-Authentifizierung fehlgeschlagen: Windows-Anmeldedaten falsch oder fehlend.\n" +
|
|
||||||
"Hinweis: Username/Password in appsettings sind Sonic-SMC-/Domain-Manager-Logins – " +
|
|
||||||
"NICHT für WinRM/Windows.\n" +
|
|
||||||
authHint + "\n" +
|
|
||||||
"Alternativen:\n" +
|
|
||||||
" • ManagementMode=LocalCmd setzen und die App direkt auf dem Sonic-Server starten (kein WinRM).\n" +
|
|
||||||
" • WinRmUsername/WinRmPassword mit gültigem Windows-Konto befüllen.\n" +
|
|
||||||
$"Details: {truncated}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool LooksLikeWinRmAuthFailure(string error)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(error))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ReadOnlySpan<string> markers =
|
|
||||||
[
|
|
||||||
"Benutzername oder das Kennwort ist falsch",
|
|
||||||
"username or password is incorrect",
|
|
||||||
"Access is denied",
|
|
||||||
"Zugriff verweigert",
|
|
||||||
"Logon failure",
|
|
||||||
"Anmeldefehler",
|
|
||||||
"PSRemotingTransportException",
|
|
||||||
"UnauthorizedAccess",
|
|
||||||
"WinRM cannot process the request",
|
|
||||||
"der remotecomputer hat den netzwerkdatenverkehr verweigert"
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach (string marker in markers)
|
|
||||||
{
|
|
||||||
if (error.Contains(marker, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string ApplyScriptTemplate(
|
|
||||||
string template,
|
|
||||||
string containerName,
|
|
||||||
string domainName = "",
|
|
||||||
string sonicHome = "",
|
|
||||||
string xapiPath = "",
|
|
||||||
string connectionUrl = "",
|
|
||||||
string username = "",
|
|
||||||
string password = "")
|
|
||||||
=> template
|
|
||||||
.Replace("{container}", containerName.Replace("'", "''"), StringComparison.OrdinalIgnoreCase)
|
|
||||||
.Replace("{domain}", domainName.Replace("'", "''"), StringComparison.OrdinalIgnoreCase)
|
|
||||||
.Replace("{sonicHome}", sonicHome.Replace("'", "''"), StringComparison.OrdinalIgnoreCase)
|
|
||||||
.Replace("{xapiPath}", xapiPath.Replace("'", "''"), StringComparison.OrdinalIgnoreCase)
|
|
||||||
.Replace("{connectionUrl}", connectionUrl.Replace("'", "''"), StringComparison.OrdinalIgnoreCase)
|
|
||||||
.Replace("{username}", username.Replace("'", "''"), StringComparison.OrdinalIgnoreCase)
|
|
||||||
.Replace("{password}", password.Replace("'", "''"), StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
private static string ExtractHost(string connectionUrl)
|
|
||||||
{
|
|
||||||
try { return new Uri(connectionUrl).Host; }
|
|
||||||
catch { return connectionUrl; }
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Truncate(string s, int max = 600)
|
|
||||||
=> s.Length <= max ? s : s[..max] + "…";
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
/*
|
|
||||||
ESB Certificate Manager – Zielkonfiguration (SQL Server)
|
|
||||||
Offline vorbereitet; zur späteren Nutzung auf dem Ziel-SQL-Server ausführen.
|
|
||||||
*/
|
|
||||||
|
|
||||||
IF OBJECT_ID(N'dbo.SonicConnection', N'U') IS NULL
|
|
||||||
BEGIN
|
|
||||||
CREATE TABLE dbo.SonicConnection
|
|
||||||
(
|
|
||||||
Id INT NOT NULL IDENTITY(1, 1),
|
|
||||||
Name NVARCHAR(128) NOT NULL, -- Eindeutiger Bezeichner, referenziert von DeploymentTarget
|
|
||||||
DomainName NVARCHAR(128) NOT NULL, -- Sonic-Domain-Name, z.B. "proalpha-test"
|
|
||||||
ManagementUrl NVARCHAR(512) NOT NULL, -- HTTP-URL der Management Console, z.B. "http://host:8080"
|
|
||||||
ApiBasePath NVARCHAR(128) NOT NULL CONSTRAINT DF_SonicConnection_ApiBasePath DEFAULT (N'/api/v1'),
|
|
||||||
Username NVARCHAR(128) NOT NULL,
|
|
||||||
-- Passwort wird in der Anwendung verschlüsselt gespeichert; hier nur als Verweis
|
|
||||||
PasswordHash NVARCHAR(512) NOT NULL CONSTRAINT DF_SonicConnection_PwdHash DEFAULT (N''),
|
|
||||||
TimeoutSeconds INT NOT NULL CONSTRAINT DF_SonicConnection_Timeout DEFAULT (30),
|
|
||||||
PostRestartDelaySecs INT NOT NULL CONSTRAINT DF_SonicConnection_Delay DEFAULT (15),
|
|
||||||
IsActive BIT NOT NULL CONSTRAINT DF_SonicConnection_IsActive DEFAULT (1),
|
|
||||||
CONSTRAINT PK_SonicConnection PRIMARY KEY CLUSTERED (Id),
|
|
||||||
CONSTRAINT UQ_SonicConnection_Name UNIQUE (Name)
|
|
||||||
);
|
|
||||||
END
|
|
||||||
GO
|
|
||||||
|
|
||||||
IF OBJECT_ID(N'dbo.DeploymentTarget', N'U') IS NULL
|
|
||||||
BEGIN
|
|
||||||
CREATE TABLE dbo.DeploymentTarget
|
|
||||||
(
|
|
||||||
Id INT NOT NULL IDENTITY(1, 1),
|
|
||||||
Name NVARCHAR(128) NOT NULL,
|
|
||||||
Environment NVARCHAR(64) NOT NULL,
|
|
||||||
IsActive BIT NOT NULL CONSTRAINT DF_DeploymentTarget_IsActive DEFAULT (1),
|
|
||||||
TargetDirectory NVARCHAR(512) NOT NULL,
|
|
||||||
CertificateFileName NVARCHAR(260) NOT NULL,
|
|
||||||
ContainerName NVARCHAR(128) NOT NULL CONSTRAINT DF_DeploymentTarget_Container DEFAULT (N''),
|
|
||||||
RestartType NVARCHAR(32) NOT NULL CONSTRAINT DF_DeploymentTarget_RestartType DEFAULT (N'None'),
|
|
||||||
RestartCommand NVARCHAR(512) NOT NULL CONSTRAINT DF_DeploymentTarget_RestartCmd DEFAULT (N''),
|
|
||||||
RestartArguments NVARCHAR(1024) NOT NULL CONSTRAINT DF_DeploymentTarget_RestartArgs DEFAULT (N''),
|
|
||||||
RestartTimeoutSeconds INT NOT NULL CONSTRAINT DF_DeploymentTarget_RestartTimeout DEFAULT (60),
|
|
||||||
-- Sonic ESB Management
|
|
||||||
SonicConnectionName NVARCHAR(128) NOT NULL CONSTRAINT DF_DeploymentTarget_SonicConn DEFAULT (N''),
|
|
||||||
XapiSourcePath NVARCHAR(512) NOT NULL CONSTRAINT DF_DeploymentTarget_XapiSrc DEFAULT (N''),
|
|
||||||
-- TLS-Probe
|
|
||||||
TlsHost NVARCHAR(255) NOT NULL CONSTRAINT DF_DeploymentTarget_TlsHost DEFAULT (N''),
|
|
||||||
TlsPort INT NULL,
|
|
||||||
SortOrder INT NOT NULL CONSTRAINT DF_DeploymentTarget_SortOrder DEFAULT (0),
|
|
||||||
CONSTRAINT PK_DeploymentTarget PRIMARY KEY CLUSTERED (Id),
|
|
||||||
CONSTRAINT CK_DeploymentTarget_RestartType CHECK (
|
|
||||||
RestartType IN (N'None', N'Command', N'SonicContainer', N'SonicContainerWithXapi')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
END
|
|
||||||
ELSE
|
|
||||||
BEGIN
|
|
||||||
-- Neue Spalten zu bestehender Tabelle hinzufügen (idempotent)
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM sys.columns
|
|
||||||
WHERE object_id = OBJECT_ID(N'dbo.DeploymentTarget')
|
|
||||||
AND name = N'SonicConnectionName')
|
|
||||||
BEGIN
|
|
||||||
ALTER TABLE dbo.DeploymentTarget
|
|
||||||
ADD SonicConnectionName NVARCHAR(128) NOT NULL
|
|
||||||
CONSTRAINT DF_DeploymentTarget_SonicConn DEFAULT (N'');
|
|
||||||
END
|
|
||||||
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM sys.columns
|
|
||||||
WHERE object_id = OBJECT_ID(N'dbo.DeploymentTarget')
|
|
||||||
AND name = N'XapiSourcePath')
|
|
||||||
BEGIN
|
|
||||||
ALTER TABLE dbo.DeploymentTarget
|
|
||||||
ADD XapiSourcePath NVARCHAR(512) NOT NULL
|
|
||||||
CONSTRAINT DF_DeploymentTarget_XapiSrc DEFAULT (N'');
|
|
||||||
END
|
|
||||||
|
|
||||||
-- CHECK-Constraint um neue RestartType-Werte erweitern
|
|
||||||
IF EXISTS (SELECT 1 FROM sys.check_constraints
|
|
||||||
WHERE parent_object_id = OBJECT_ID(N'dbo.DeploymentTarget')
|
|
||||||
AND name = N'CK_DeploymentTarget_RestartType')
|
|
||||||
BEGIN
|
|
||||||
ALTER TABLE dbo.DeploymentTarget DROP CONSTRAINT CK_DeploymentTarget_RestartType;
|
|
||||||
ALTER TABLE dbo.DeploymentTarget ADD CONSTRAINT CK_DeploymentTarget_RestartType
|
|
||||||
CHECK (RestartType IN (N'None', N'Command', N'SonicContainer', N'SonicContainerWithXapi'));
|
|
||||||
END
|
|
||||||
END
|
|
||||||
GO
|
|
||||||
|
|
||||||
IF OBJECT_ID(N'dbo.DeploymentRunHistory', N'U') IS NULL
|
|
||||||
BEGIN
|
|
||||||
CREATE TABLE dbo.DeploymentRunHistory
|
|
||||||
(
|
|
||||||
Id BIGINT NOT NULL IDENTITY(1, 1),
|
|
||||||
StartedAtUtc DATETIME2(3) NOT NULL,
|
|
||||||
FinishedAtUtc DATETIME2(3) NULL,
|
|
||||||
UserName NVARCHAR(128) NOT NULL,
|
|
||||||
MachineName NVARCHAR(128) NOT NULL CONSTRAINT DF_DeploymentRunHistory_Machine DEFAULT (N''),
|
|
||||||
OverallSuccess BIT NULL,
|
|
||||||
Summary NVARCHAR(2000) NULL,
|
|
||||||
CONSTRAINT PK_DeploymentRunHistory PRIMARY KEY CLUSTERED (Id)
|
|
||||||
);
|
|
||||||
END
|
|
||||||
ELSE
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM sys.columns
|
|
||||||
WHERE object_id = OBJECT_ID(N'dbo.DeploymentRunHistory')
|
|
||||||
AND name = N'MachineName')
|
|
||||||
BEGIN
|
|
||||||
ALTER TABLE dbo.DeploymentRunHistory
|
|
||||||
ADD MachineName NVARCHAR(128) NOT NULL
|
|
||||||
CONSTRAINT DF_DeploymentRunHistory_Machine DEFAULT (N'');
|
|
||||||
END
|
|
||||||
END
|
|
||||||
GO
|
|
||||||
|
|
||||||
IF OBJECT_ID(N'dbo.DeploymentRunDetail', N'U') IS NULL
|
|
||||||
BEGIN
|
|
||||||
CREATE TABLE dbo.DeploymentRunDetail
|
|
||||||
(
|
|
||||||
Id BIGINT NOT NULL IDENTITY(1, 1),
|
|
||||||
RunId BIGINT NOT NULL,
|
|
||||||
TargetId INT NOT NULL,
|
|
||||||
TargetName NVARCHAR(128) NOT NULL,
|
|
||||||
Success BIT NOT NULL,
|
|
||||||
StatusText NVARCHAR(256) NOT NULL,
|
|
||||||
Detail NVARCHAR(2000) NULL,
|
|
||||||
Steps NVARCHAR(MAX) NULL,
|
|
||||||
CONSTRAINT PK_DeploymentRunDetail PRIMARY KEY CLUSTERED (Id),
|
|
||||||
CONSTRAINT FK_DeploymentRunDetail_Run
|
|
||||||
FOREIGN KEY (RunId) REFERENCES dbo.DeploymentRunHistory (Id)
|
|
||||||
);
|
|
||||||
END
|
|
||||||
GO
|
|
||||||
|
|
||||||
/*
|
|
||||||
Beispiel-Insert:
|
|
||||||
|
|
||||||
INSERT INTO dbo.SonicConnection (Name, DomainName, ManagementUrl, Username, PasswordHash)
|
|
||||||
VALUES (N'DE-Test', N'proalpha-test', N'http://dekun-painwbdet:8080', N'Administrator', N'<encrypted>');
|
|
||||||
|
|
||||||
INSERT INTO dbo.DeploymentTarget
|
|
||||||
(
|
|
||||||
Name, Environment, IsActive, TargetDirectory, CertificateFileName,
|
|
||||||
ContainerName, RestartType, SonicConnectionName,
|
|
||||||
TlsHost, TlsPort, SortOrder
|
|
||||||
)
|
|
||||||
VALUES
|
|
||||||
(
|
|
||||||
N'DE-Test Container A', N'TEST', 1, N'\\share\esb\certs\a', N'esb-cert.cer',
|
|
||||||
N'sonic-container-a', N'SonicContainer', N'DE-Test',
|
|
||||||
N'dekun-painwbdet', 13070, 10
|
|
||||||
),
|
|
||||||
(
|
|
||||||
N'DE-Test Container B (XApi)', N'TEST', 1, N'\\share\esb\certs\b', N'esb-cert.cer',
|
|
||||||
N'sonic-container-b', N'SonicContainerWithXapi', N'DE-Test',
|
|
||||||
N'dekun-painwbdet', 13070, 20
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
/*
|
|
||||||
ESB Certificate Manager – vollständiges Datenbankschema v2
|
|
||||||
Idempotent; kann auf einem leeren Schema oder nach 001_CreateSchema.sql ausgeführt werden.
|
|
||||||
|
|
||||||
Tabellen:
|
|
||||||
dbo.SonicConnection – Sonic-ESB-Management-Instanzen
|
|
||||||
dbo.DeploymentTargets – Deployment-Ziele mit allen Konfigurationsfeldern
|
|
||||||
dbo.DeploymentRuns – Ein Eintrag pro Deployment-Lauf
|
|
||||||
dbo.DeploymentTargetResults – Detailergebnis pro Ziel und Lauf
|
|
||||||
|
|
||||||
Always Encrypted (optional):
|
|
||||||
Zur Nutzung von Always Encrypted auf der Spalte PasswordHash in SonicConnection
|
|
||||||
die Blöcke unterhalb des Kommentars "-- ALWAYS ENCRYPTED" auskommentieren
|
|
||||||
und den Schlüsselnamen anpassen.
|
|
||||||
*/
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- SonicConnection
|
|
||||||
-- ============================================================
|
|
||||||
IF OBJECT_ID(N'dbo.SonicConnection', N'U') IS NULL
|
|
||||||
BEGIN
|
|
||||||
CREATE TABLE dbo.SonicConnection
|
|
||||||
(
|
|
||||||
Id INT NOT NULL IDENTITY(1, 1),
|
|
||||||
Name NVARCHAR(128) NOT NULL, -- Eindeutiger Bezeichner, referenziert von DeploymentTargets
|
|
||||||
DomainName NVARCHAR(128) NOT NULL, -- Sonic-Domain, z.B. "proalpha-test"
|
|
||||||
ConnectionUrl NVARCHAR(512) NOT NULL, -- Sonic-Broker-URL, z.B. "tcp://dekun-painwbdet:13070"
|
|
||||||
ManagementHttpPort INT NOT NULL CONSTRAINT DF_SonicConnection_HttpPort DEFAULT (8080),
|
|
||||||
ApiBasePath NVARCHAR(128) NOT NULL CONSTRAINT DF_SonicConnection_ApiBase DEFAULT (N'/api/v1'),
|
|
||||||
Username NVARCHAR(128) NOT NULL,
|
|
||||||
PasswordHash NVARCHAR(512) NOT NULL CONSTRAINT DF_SonicConnection_PwdHash DEFAULT (N''),
|
|
||||||
-- ALWAYS ENCRYPTED: PasswordEncrypted NVARCHAR(512) ENCRYPTED WITH (
|
|
||||||
-- COLUMN_ENCRYPTION_KEY = CEK_SonicPwd,
|
|
||||||
-- ENCRYPTION_TYPE = DETERMINISTIC,
|
|
||||||
-- ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256'
|
|
||||||
-- ) NULL,
|
|
||||||
TimeoutSeconds INT NOT NULL CONSTRAINT DF_SonicConnection_Timeout DEFAULT (30),
|
|
||||||
PostRestartDelaySecs INT NOT NULL CONSTRAINT DF_SonicConnection_Delay DEFAULT (15),
|
|
||||||
IsActive BIT NOT NULL CONSTRAINT DF_SonicConnection_Active DEFAULT (1),
|
|
||||||
CONSTRAINT PK_SonicConnection PRIMARY KEY CLUSTERED (Id),
|
|
||||||
CONSTRAINT UQ_SonicConnection_Name UNIQUE (Name)
|
|
||||||
);
|
|
||||||
END
|
|
||||||
ELSE
|
|
||||||
BEGIN
|
|
||||||
-- ConnectionUrl-Spalte nachrüsten (Migration von ManagementUrl)
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM sys.columns
|
|
||||||
WHERE object_id = OBJECT_ID(N'dbo.SonicConnection') AND name = N'ConnectionUrl')
|
|
||||||
BEGIN
|
|
||||||
ALTER TABLE dbo.SonicConnection ADD ConnectionUrl NVARCHAR(512) NOT NULL
|
|
||||||
CONSTRAINT DF_SonicConnection_ConnUrl DEFAULT (N'');
|
|
||||||
END
|
|
||||||
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM sys.columns
|
|
||||||
WHERE object_id = OBJECT_ID(N'dbo.SonicConnection') AND name = N'ManagementHttpPort')
|
|
||||||
BEGIN
|
|
||||||
ALTER TABLE dbo.SonicConnection ADD ManagementHttpPort INT NOT NULL
|
|
||||||
CONSTRAINT DF_SonicConnection_HttpPort DEFAULT (8080);
|
|
||||||
END
|
|
||||||
END
|
|
||||||
GO
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- DeploymentTargets
|
|
||||||
-- ============================================================
|
|
||||||
IF OBJECT_ID(N'dbo.DeploymentTargets', N'U') IS NULL
|
|
||||||
BEGIN
|
|
||||||
CREATE TABLE dbo.DeploymentTargets
|
|
||||||
(
|
|
||||||
Id INT NOT NULL IDENTITY(1, 1),
|
|
||||||
Name NVARCHAR(100) NOT NULL,
|
|
||||||
Environment NVARCHAR(64) NOT NULL CONSTRAINT DF_DT_Env DEFAULT (N''),
|
|
||||||
IsActive BIT NOT NULL CONSTRAINT DF_DT_IsActive DEFAULT (1),
|
|
||||||
|
|
||||||
-- Zertifikat-Ablage
|
|
||||||
CertificateTargetPath NVARCHAR(500) NOT NULL,
|
|
||||||
CertificateFileName NVARCHAR(260) NOT NULL CONSTRAINT DF_DT_CertFile DEFAULT (N''),
|
|
||||||
|
|
||||||
-- Neustart-Konfiguration
|
|
||||||
RestartType NVARCHAR(30) NOT NULL CONSTRAINT DF_DT_RestartType DEFAULT (N'None'),
|
|
||||||
RestartHost NVARCHAR(255) NULL,
|
|
||||||
RestartCommand NVARCHAR(2000) NULL,
|
|
||||||
RestartArguments NVARCHAR(1024) NOT NULL CONSTRAINT DF_DT_RestartArgs DEFAULT (N''),
|
|
||||||
RestartTimeoutSeconds INT NOT NULL CONSTRAINT DF_DT_RestartTimeout DEFAULT (60),
|
|
||||||
|
|
||||||
-- Sonic ESB
|
|
||||||
SonicConnectionName NVARCHAR(128) NOT NULL CONSTRAINT DF_DT_SonicConn DEFAULT (N''),
|
|
||||||
ContainerName NVARCHAR(128) NOT NULL CONSTRAINT DF_DT_Container DEFAULT (N''),
|
|
||||||
XapiSourcePath NVARCHAR(512) NOT NULL CONSTRAINT DF_DT_XapiSrc DEFAULT (N''),
|
|
||||||
|
|
||||||
-- TLS-Probe
|
|
||||||
TlsHost NVARCHAR(255) NOT NULL CONSTRAINT DF_DT_TlsHost DEFAULT (N''),
|
|
||||||
TlsPort INT NOT NULL CONSTRAINT DF_DT_TlsPort DEFAULT (443),
|
|
||||||
TlsServerName NVARCHAR(255) NOT NULL CONSTRAINT DF_DT_TlsServerName DEFAULT (N''),
|
|
||||||
ExpectedFingerprint NVARCHAR(128) NULL,
|
|
||||||
|
|
||||||
SortOrder INT NOT NULL CONSTRAINT DF_DT_SortOrder DEFAULT (0),
|
|
||||||
|
|
||||||
CONSTRAINT PK_DeploymentTargets PRIMARY KEY CLUSTERED (Id),
|
|
||||||
CONSTRAINT CK_DeploymentTargets_RestartType CHECK (
|
|
||||||
RestartType IN (N'None', N'Command', N'SonicContainer', N'SonicContainerWithXapi')
|
|
||||||
)
|
|
||||||
);
|
|
||||||
END
|
|
||||||
ELSE
|
|
||||||
BEGIN
|
|
||||||
-- Neue Spalten idempotent nachrüsten
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'dbo.DeploymentTargets') AND name = N'TlsServerName')
|
|
||||||
ALTER TABLE dbo.DeploymentTargets ADD TlsServerName NVARCHAR(255) NOT NULL CONSTRAINT DF_DT_TlsServerName DEFAULT (N'');
|
|
||||||
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'dbo.DeploymentTargets') AND name = N'ExpectedFingerprint')
|
|
||||||
ALTER TABLE dbo.DeploymentTargets ADD ExpectedFingerprint NVARCHAR(128) NULL;
|
|
||||||
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'dbo.DeploymentTargets') AND name = N'SonicConnectionName')
|
|
||||||
ALTER TABLE dbo.DeploymentTargets ADD SonicConnectionName NVARCHAR(128) NOT NULL CONSTRAINT DF_DT_SonicConn DEFAULT (N'');
|
|
||||||
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'dbo.DeploymentTargets') AND name = N'ContainerName')
|
|
||||||
ALTER TABLE dbo.DeploymentTargets ADD ContainerName NVARCHAR(128) NOT NULL CONSTRAINT DF_DT_Container DEFAULT (N'');
|
|
||||||
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'dbo.DeploymentTargets') AND name = N'XapiSourcePath')
|
|
||||||
ALTER TABLE dbo.DeploymentTargets ADD XapiSourcePath NVARCHAR(512) NOT NULL CONSTRAINT DF_DT_XapiSrc DEFAULT (N'');
|
|
||||||
END
|
|
||||||
GO
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- DeploymentRuns (ein Datensatz pro Deployment-Lauf)
|
|
||||||
-- ============================================================
|
|
||||||
IF OBJECT_ID(N'dbo.DeploymentRuns', N'U') IS NULL
|
|
||||||
BEGIN
|
|
||||||
CREATE TABLE dbo.DeploymentRuns
|
|
||||||
(
|
|
||||||
Id UNIQUEIDENTIFIER NOT NULL,
|
|
||||||
StartedAtUtc DATETIME2(3) NOT NULL,
|
|
||||||
FinishedAtUtc DATETIME2(3) NULL,
|
|
||||||
SourceFile NVARCHAR(500) NOT NULL,
|
|
||||||
SourceFingerprint NVARCHAR(128) NOT NULL,
|
|
||||||
StartedBy NVARCHAR(255) NOT NULL,
|
|
||||||
MachineName NVARCHAR(255) NOT NULL CONSTRAINT DF_DR_Machine DEFAULT (N''),
|
|
||||||
OverallStatus NVARCHAR(30) NOT NULL, -- 'Running' | 'Success' | 'PartialFailure' | 'Failure'
|
|
||||||
ErrorMessage NVARCHAR(MAX) NULL,
|
|
||||||
CONSTRAINT PK_DeploymentRuns PRIMARY KEY CLUSTERED (Id)
|
|
||||||
);
|
|
||||||
END
|
|
||||||
GO
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- DeploymentTargetResults (ein Datensatz pro Ziel und Lauf)
|
|
||||||
-- ============================================================
|
|
||||||
IF OBJECT_ID(N'dbo.DeploymentTargetResults', N'U') IS NULL
|
|
||||||
BEGIN
|
|
||||||
CREATE TABLE dbo.DeploymentTargetResults
|
|
||||||
(
|
|
||||||
Id INT NOT NULL IDENTITY(1, 1),
|
|
||||||
DeploymentRunId UNIQUEIDENTIFIER NOT NULL,
|
|
||||||
TargetId INT NOT NULL,
|
|
||||||
TargetName NVARCHAR(128) NOT NULL,
|
|
||||||
StartedAtUtc DATETIME2(3) NOT NULL,
|
|
||||||
FinishedAtUtc DATETIME2(3) NULL,
|
|
||||||
CopySucceeded BIT NOT NULL CONSTRAINT DF_DTR_Copy DEFAULT (0),
|
|
||||||
RestartSucceeded BIT NOT NULL CONSTRAINT DF_DTR_Restart DEFAULT (0),
|
|
||||||
TlsSucceeded BIT NOT NULL CONSTRAINT DF_DTR_Tls DEFAULT (0),
|
|
||||||
ObservedFingerprint NVARCHAR(128) NULL,
|
|
||||||
Status NVARCHAR(30) NOT NULL,
|
|
||||||
ErrorMessage NVARCHAR(MAX) NULL,
|
|
||||||
CONSTRAINT PK_DeploymentTargetResults PRIMARY KEY CLUSTERED (Id),
|
|
||||||
CONSTRAINT FK_DTR_Run FOREIGN KEY (DeploymentRunId) REFERENCES dbo.DeploymentRuns (Id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE NONCLUSTERED INDEX IX_DTR_RunId ON dbo.DeploymentTargetResults (DeploymentRunId);
|
|
||||||
END
|
|
||||||
GO
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- Beispieldaten
|
|
||||||
-- ============================================================
|
|
||||||
/*
|
|
||||||
INSERT INTO dbo.SonicConnection (Name, DomainName, ConnectionUrl, Username, PasswordHash)
|
|
||||||
VALUES (N'DE-Test', N'proalpha-test', N'tcp://dekun-painwbdet:13070', N'Administrator', N'<encrypted>');
|
|
||||||
|
|
||||||
INSERT INTO dbo.DeploymentTargets
|
|
||||||
(Name, Environment, IsActive, CertificateTargetPath, CertificateFileName,
|
|
||||||
RestartType, SonicConnectionName, ContainerName,
|
|
||||||
TlsHost, TlsPort, TlsServerName, SortOrder)
|
|
||||||
VALUES
|
|
||||||
(N'DE-Test Container A', N'TEST', 1,
|
|
||||||
N'\\dekun-painwbdet\sonic\certs', N'server.pfx',
|
|
||||||
N'SonicContainer', N'DE-Test', N'sonic-container-a',
|
|
||||||
N'dekun-painwbdet', 443, N'esb-test.firma.local', 10),
|
|
||||||
(N'DE-Test Container B (XApi)', N'TEST', 1,
|
|
||||||
N'\\dekun-painwbdet\sonic\certs', N'server.pfx',
|
|
||||||
N'SonicContainerWithXapi', N'DE-Test', N'sonic-container-b',
|
|
||||||
N'dekun-painwbdet', 443, N'esb-test.firma.local', 20);
|
|
||||||
*/
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
@echo off
|
|
||||||
REM Prueft ob JMSConnectorAddress mit den Sonic-Client-JARs ladbar ist.
|
|
||||||
set LIB=C:\DEV\MQ10.0\lib
|
|
||||||
set JAVA=C:\Program Files (x86)\Java\jre1.8.0_501\bin\java.exe
|
|
||||||
|
|
||||||
if not exist "%JAVA%" (
|
|
||||||
echo Java nicht gefunden: %JAVA%
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
if not exist "%LIB%\mfcontext.jar" (
|
|
||||||
echo mfcontext.jar fehlt unter %LIB%
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
set CP=%LIB%\mgmt_client.jar;%LIB%\mgmt_config.jar;%LIB%\sonic_mgmt_client.jar;%LIB%\mfcontext.jar;%LIB%\sonic_Client.jar;%LIB%\sonic_Client_ext.jar
|
|
||||||
|
|
||||||
echo Java: %JAVA%
|
|
||||||
echo CP: %CP%
|
|
||||||
echo.
|
|
||||||
|
|
||||||
"%JAVA%" -cp "%CP%" -version
|
|
||||||
echo.
|
|
||||||
|
|
||||||
"%JAVA%" -cp "%CP%" com.sonicsw.mf.jmx.client.JMSConnectorAddress 2>&1
|
|
||||||
echo ExitCode=%ERRORLEVEL%
|
|
||||||
echo.
|
|
||||||
echo Erwartet: oft "main method" / NoSuchMethodError ODER Usage - Hauptsache KEIN ClassNotFoundException.
|
|
||||||
exit /b 0
|
|
||||||
@@ -9,7 +9,6 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -24,12 +23,6 @@
|
|||||||
<None Update="Data\targets.sample.json">
|
<None Update="Data\targets.sample.json">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
<None Update="Demo\**\*">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Sql\**\*">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Tools\**\*">
|
<None Update="Tools\**\*">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
{
|
{
|
||||||
"ConnectionString": "",
|
|
||||||
"UseOfflineSampleData": true,
|
"UseOfflineSampleData": true,
|
||||||
"SampleTargetsPath": "Data/targets.sample.json",
|
"SampleTargetsPath": "Data/targets.sample.json",
|
||||||
"LogDirectory": "Logs",
|
"LogDirectory": "Logs",
|
||||||
"TlsTimeoutSeconds": 8,
|
|
||||||
"TlsRetryCount": 2,
|
|
||||||
"SonicConnections": [
|
"SonicConnections": [
|
||||||
{
|
{
|
||||||
"Name": "DE-Test",
|
"Name": "DE-Test",
|
||||||
@@ -12,7 +9,6 @@
|
|||||||
"ConnectionUrl": "tcp://dekun-painwbdet:13070",
|
"ConnectionUrl": "tcp://dekun-painwbdet:13070",
|
||||||
"Username": "Administrator",
|
"Username": "Administrator",
|
||||||
"Password": "Administrator",
|
"Password": "Administrator",
|
||||||
"ManagementMode": "MfApi",
|
|
||||||
"SonicHome": "C:\\DEV\\MQ10.0",
|
"SonicHome": "C:\\DEV\\MQ10.0",
|
||||||
"JavaHome": "C:\\Program Files (x86)\\Java\\jre1.8.0_501",
|
"JavaHome": "C:\\Program Files (x86)\\Java\\jre1.8.0_501",
|
||||||
"JavaPath": "C:\\Program Files (x86)\\Java\\jre1.8.0_501\\bin\\java.exe",
|
"JavaPath": "C:\\Program Files (x86)\\Java\\jre1.8.0_501\\bin\\java.exe",
|
||||||
@@ -20,20 +16,8 @@
|
|||||||
"KnownContainers": [
|
"KnownContainers": [
|
||||||
"ct-ZADBService"
|
"ct-ZADBService"
|
||||||
],
|
],
|
||||||
"WinRmUsername": "",
|
|
||||||
"WinRmPassword": "",
|
|
||||||
"WinRmPort": 5985,
|
|
||||||
"TimeoutSeconds": 120,
|
"TimeoutSeconds": 120,
|
||||||
"PostRestartDelaySeconds": 20,
|
"PostRestartDelaySeconds": 20
|
||||||
"ManagementHttpPort": 8080,
|
|
||||||
"ApiBasePath": "/api/v1",
|
|
||||||
"ContainerListPath": "",
|
|
||||||
"ContainerRestartPath": "",
|
|
||||||
"ContainerStopPath": "",
|
|
||||||
"ContainerStartPath": "",
|
|
||||||
"WinRmRestartScript": "",
|
|
||||||
"WinRmContainerListScript": "",
|
|
||||||
"WinRmXapiImportScript": ""
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user