Fix app startup and enable Sonic ESB restart via remote CMD.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-24 08:17:35 +02:00
co-authored by Cursor
parent ed8e2822d1
commit c2268f1908
9 changed files with 446 additions and 207 deletions
@@ -9,19 +9,20 @@ namespace ZA.CoreService.ESBCertificateManager.Services;
/// <summary>
/// Verwaltet Sonic ESB Container über die Management Console.
///
/// Modus = WinRm (Standard, Sonic 10.x):
/// PowerShell Remoting (Invoke-Command) auf dem Sonic-Server.
/// Erfordert WinRM auf dem Zielrechner: Enable-PSRemoting -Force
/// Modus = WinRm (Standard):
/// PowerShell Remoting (Invoke-Command) → CMD stop/startcontainer auf dem Sonic-Server.
///
/// Modus = LocalCmd:
/// Dieselbe CMD/PowerShell-Logik lokal (Test direkt auf dem Sonic-PC).
///
/// Modus = HttpApi:
/// HTTP REST API mit automatischer Pfad-Erkennung.
/// Probiert: /mf/rest/v1, /api/v1, /sonic/management, /containers
/// </summary>
public sealed class SonicManagementClient : IDisposable
{
private readonly SonicConnection _connection;
private readonly HttpClient? _http;
private readonly WinRmExecutor? _winRm;
private readonly WinRmExecutor? _scriptRunner;
// Gecachter HTTP-Basis-Pfad (nur im HttpApi-Modus)
private string? _resolvedContainerBasePath;
@@ -34,13 +35,16 @@ public sealed class SonicManagementClient : IDisposable
"/containers"
];
private bool UsesScripts =>
_connection.ManagementMode is SonicManagementMode.WinRm or SonicManagementMode.LocalCmd;
public SonicManagementClient(SonicConnection connection)
{
_connection = connection;
if (connection.ManagementMode == SonicManagementMode.WinRm)
if (UsesScripts)
{
_winRm = new WinRmExecutor(connection);
_scriptRunner = new WinRmExecutor(connection);
}
else
{
@@ -77,10 +81,13 @@ public sealed class SonicManagementClient : IDisposable
public async Task<(bool Success, string? Error, string? ResolvedPath)> CheckConnectionAsync(
CancellationToken cancellationToken = default)
{
if (_connection.ManagementMode == SonicManagementMode.WinRm)
if (UsesScripts)
{
(bool ok, string? error) = await _winRm!.TestConnectionAsync(cancellationToken);
return (ok, error, ok ? $"WinRM auf {ExtractHost(_connection.ConnectionUrl)}:{_connection.WinRmPort}" : null);
(bool ok, string? error) = await _scriptRunner!.TestConnectionAsync(cancellationToken);
string modeLabel = _connection.ManagementMode == SonicManagementMode.LocalCmd
? $"LocalCmd (SonicHome={_connection.SonicHome})"
: $"WinRM auf {ExtractHost(_connection.ConnectionUrl)}:{_connection.WinRmPort}";
return (ok, error, ok ? modeLabel : null);
}
try
@@ -116,9 +123,9 @@ public sealed class SonicManagementClient : IDisposable
public async Task<(bool Success, IReadOnlyList<string> ContainerNames, string? Error)> GetContainersAsync(
CancellationToken cancellationToken = default)
{
if (_connection.ManagementMode == SonicManagementMode.WinRm)
if (UsesScripts)
{
return await GetContainersViaWinRmAsync(cancellationToken);
return await GetContainersViaScriptAsync(cancellationToken);
}
return await GetContainersViaHttpAsync(cancellationToken);
@@ -131,9 +138,9 @@ public sealed class SonicManagementClient : IDisposable
string containerName,
CancellationToken cancellationToken = default)
{
if (_connection.ManagementMode == SonicManagementMode.WinRm)
if (UsesScripts)
{
return await RestartViaWinRmAsync(containerName, cancellationToken);
return await RestartViaScriptAsync(containerName, cancellationToken);
}
return await RestartViaHttpAsync(containerName, cancellationToken);
@@ -147,83 +154,72 @@ public sealed class SonicManagementClient : IDisposable
string xapiSourcePath,
CancellationToken cancellationToken = default)
{
if (_connection.ManagementMode == SonicManagementMode.WinRm)
if (UsesScripts)
{
return await ImportXapiViaWinRmAsync(containerName, xapiSourcePath, cancellationToken);
return await ImportXapiViaScriptAsync(containerName, xapiSourcePath, cancellationToken);
}
return await ImportXapiViaHttpAsync(containerName, xapiSourcePath, cancellationToken);
}
// ---------------------------------------------------------------
// WinRM-Implementierungen
// Script-Implementierungen (WinRM / LocalCmd)
// ---------------------------------------------------------------
private async Task<(bool, IReadOnlyList<string>, string?)> GetContainersViaWinRmAsync(
private async Task<(bool, IReadOnlyList<string>, string?)> GetContainersViaScriptAsync(
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(_connection.WinRmContainerListScript))
{
return (false, [],
"WinRmContainerListScript ist nicht konfiguriert.\n" +
"Beispiel: \"Get-Service -DisplayName 'Sonic*' | Select-Object -ExpandProperty DisplayName\"");
}
string script = WinRmExecutor.ApplyScriptTemplate(
_connection.WinRmContainerListScript,
_connection.ResolveContainerListScript(),
containerName: string.Empty,
domainName: _connection.DomainName);
domainName: _connection.DomainName,
sonicHome: _connection.SonicHome);
(bool ok, string? output, string? error) =
await _winRm!.RunScriptAsync(script, cancellationToken);
await _scriptRunner!.RunScriptAsync(script, cancellationToken);
if (!ok)
{
return (false, [], $"WinRM Container-Liste fehlgeschlagen: {error}");
return (false, [], $"Container-Liste fehlgeschlagen ({_connection.ManagementMode}): {error}");
}
List<string> names = (output ?? string.Empty)
.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(l => l.Length > 0)
.Where(l => l.Length > 0 && !l.StartsWith("WARNING", StringComparison.OrdinalIgnoreCase))
.ToList();
return (true, names, null);
}
private async Task<(bool, string, string?)> RestartViaWinRmAsync(
private async Task<(bool, string, string?)> RestartViaScriptAsync(
string containerName, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(_connection.WinRmRestartScript))
{
return (false, "Neustart fehlgeschlagen",
"WinRmRestartScript ist nicht konfiguriert.\n" +
"Beispiel für Windows-Service: \"Restart-Service -Name 'CT-ZADBService' -Force\"\n" +
"Platzhalter {container} wird durch den Container-Namen ersetzt.");
}
string script = WinRmExecutor.ApplyScriptTemplate(
_connection.WinRmRestartScript,
_connection.ResolveRestartScript(),
containerName,
_connection.DomainName);
_connection.DomainName,
_connection.SonicHome);
(bool ok, string? output, string? error) =
await _winRm!.RunScriptAsync(script, cancellationToken);
await _scriptRunner!.RunScriptAsync(script, cancellationToken);
string mode = _connection.ManagementMode.ToString();
if (!ok)
{
return (false, "Neustart fehlgeschlagen (WinRM)",
$"Fehler: {error}\nAusgabe: {output}");
return (false, $"Neustart fehlgeschlagen ({mode})",
$"Fehler: {error}\nAusgabe: {output}\nSonicHome={_connection.SonicHome}");
}
await Task.Delay(
TimeSpan.FromSeconds(Math.Clamp(_connection.PostRestartDelaySeconds, 0, 120)),
cancellationToken);
return (true, "Container neugestartet (WinRM)",
return (true, $"ESB-Container neugestartet ({mode})",
$"Ausgabe: {output ?? "(keine)"}");
}
private async Task<(bool, string, string?)> ImportXapiViaWinRmAsync(
private async Task<(bool, string, string?)> ImportXapiViaScriptAsync(
string containerName, string xapiSourcePath, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(_connection.WinRmXapiImportScript))
@@ -236,21 +232,22 @@ public sealed class SonicManagementClient : IDisposable
_connection.WinRmXapiImportScript,
containerName,
_connection.DomainName,
_connection.SonicHome,
xapiSourcePath);
(bool importOk, string? importOut, string? importErr) =
await _winRm!.RunScriptAsync(script, cancellationToken);
await _scriptRunner!.RunScriptAsync(script, cancellationToken);
if (!importOk)
{
return (false, "XApi-Import fehlgeschlagen (WinRM)", importErr ?? importOut);
return (false, "XApi-Import fehlgeschlagen", importErr ?? importOut);
}
(bool restartOk, string restartStatus, string? restartDetail) =
await RestartViaWinRmAsync(containerName, cancellationToken);
await RestartViaScriptAsync(containerName, cancellationToken);
return restartOk
? (true, "XApi importiert + Container neugestartet (WinRM)",
? (true, "XApi importiert + Container neugestartet",
$"Import: {importOut} | Restart: {restartDetail}")
: (false, restartStatus, restartDetail);
}
@@ -473,8 +470,14 @@ public sealed class SonicManagementClient : IDisposable
? root.EnumerateArray()
: root.ValueKind == JsonValueKind.Object
? new[] { "containers", "data", "items", "result" }
.Where(root.TryGetProperty)
.SelectMany(k => { root.TryGetProperty(k, out JsonElement a); return a.EnumerateArray(); })
.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)