Keep original UI; drop unused deploy/XApi/WinRM/TLS/SQL paths.
Form1 design unchanged; only MfApi restart and certificate recognition remain active. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,339 +1,66 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using ZA.CoreService.ESBCertificateManager.Models;
|
||||
|
||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Verwaltet Sonic ESB Container über die Management Console.
|
||||
///
|
||||
/// 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.
|
||||
/// Sonic MfApi (wie SMC-Neustart) – ohne WinRM/HTTP/XApi.
|
||||
/// </summary>
|
||||
public sealed class SonicManagementClient : IDisposable
|
||||
{
|
||||
private readonly SonicConnection _connection;
|
||||
private readonly HttpClient? _http;
|
||||
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;
|
||||
private readonly SonicMfApiExecutor _mfApi;
|
||||
|
||||
public SonicManagementClient(SonicConnection connection)
|
||||
{
|
||||
_connection = connection;
|
||||
|
||||
if (UsesMfApi)
|
||||
{
|
||||
_mfApi = new SonicMfApiExecutor(connection);
|
||||
}
|
||||
else if (UsesScripts)
|
||||
{
|
||||
_scriptRunner = new WinRmExecutor(connection);
|
||||
}
|
||||
else
|
||||
{
|
||||
HttpClientHandler handler = new()
|
||||
{
|
||||
ServerCertificateCustomValidationCallback =
|
||||
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
|
||||
};
|
||||
|
||||
_http = new HttpClient(handler)
|
||||
{
|
||||
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"));
|
||||
}
|
||||
_mfApi = new SonicMfApiExecutor(connection);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Ö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)
|
||||
{
|
||||
if (UsesMfApi)
|
||||
(bool ok, string? error) = await _mfApi.TestConnectionAsync(cancellationToken);
|
||||
if (ok)
|
||||
{
|
||||
(bool ok, string? error) = await _mfApi!.TestConnectionAsync(cancellationToken);
|
||||
if (ok)
|
||||
{
|
||||
return (true, null,
|
||||
$"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}");
|
||||
}
|
||||
|
||||
if (_connection.KnownContainers.Count > 0
|
||||
|| await IsTcpReachableAsync(_connection.ConnectionUrl, cancellationToken))
|
||||
{
|
||||
return (true, null,
|
||||
$"MfApi ohne Java – KnownContainers/TCP; Hinweis: {Truncate(error ?? string.Empty, 180)}");
|
||||
}
|
||||
}
|
||||
|
||||
return (false, error, null);
|
||||
return (true, null, $"MfApi {_connection.ConnectionUrl} Domain={_connection.DomainName}");
|
||||
}
|
||||
|
||||
if (UsesScripts)
|
||||
if (_connection.KnownContainers.Count > 0
|
||||
&& await IsTcpReachableAsync(_connection.ConnectionUrl, cancellationToken))
|
||||
{
|
||||
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);
|
||||
return (true, null,
|
||||
$"MfApi-Ping fehlgeschlagen, KnownContainers/TCP ok. Hinweis: {Truncate(error)}");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
return (false, error ?? "MfApi-Verbindung fehlgeschlagen", null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Listet alle Container der Domain auf.
|
||||
/// </summary>
|
||||
public async Task<(bool Success, IReadOnlyList<string> ContainerNames, string? Error)> GetContainersAsync(
|
||||
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(
|
||||
string containerName,
|
||||
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) =
|
||||
await _mfApi!.RestartAsync(containerName, cancellationToken);
|
||||
await _mfApi.RestartAsync(containerName, cancellationToken);
|
||||
|
||||
if (ok)
|
||||
{
|
||||
@@ -342,26 +69,12 @@ public sealed class SonicManagementClient : IDisposable
|
||||
cancellationToken);
|
||||
|
||||
return (true, $"Container '{containerName}' neugestartet (MfApi)",
|
||||
$"Domain={_connection.DomainName}; ConnectionUrl={_connection.ConnectionUrl}\n{output}");
|
||||
$"Domain={_connection.DomainName}; URL={_connection.ConnectionUrl}\n{output}");
|
||||
}
|
||||
|
||||
string libHint = string.IsNullOrWhiteSpace(_connection.MfClientLibPath)
|
||||
? _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" +
|
||||
$"Fehler: {error}\nAusgabe: {output}\n\n" +
|
||||
"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).");
|
||||
$"Fehler: {error}\n\n{output}");
|
||||
}
|
||||
|
||||
private static async Task<bool> IsTcpReachableAsync(string connectionUrl, CancellationToken cancellationToken)
|
||||
@@ -369,12 +82,10 @@ public sealed class SonicManagementClient : IDisposable
|
||||
try
|
||||
{
|
||||
Uri uri = new(connectionUrl);
|
||||
string host = uri.Host;
|
||||
int port = uri.Port > 0 ? uri.Port : 2506;
|
||||
using System.Net.Sockets.TcpClient tcp = new();
|
||||
using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
await tcp.ConnectAsync(host, port, cts.Token);
|
||||
await tcp.ConnectAsync(uri.Host, uri.Port > 0 ? uri.Port : 2506, cts.Token);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
@@ -383,390 +94,11 @@ public sealed class SonicManagementClient : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private static SonicConnection CloneAsLocalCmd(SonicConnection source)
|
||||
=> new()
|
||||
{
|
||||
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] + "…";
|
||||
private static string Truncate(string? s, int max = 180)
|
||||
=> string.IsNullOrWhiteSpace(s) ? string.Empty
|
||||
: s.Length <= max ? s : s[..max] + "…";
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_http?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user