diff --git a/ZA.CoreService.ESBCertificateManager/Form1.cs b/ZA.CoreService.ESBCertificateManager/Form1.cs
index 51b7936..a2809db 100644
--- a/ZA.CoreService.ESBCertificateManager/Form1.cs
+++ b/ZA.CoreService.ESBCertificateManager/Form1.cs
@@ -1809,7 +1809,7 @@ namespace ZA.CoreService.ESBCertificateManager
{
SonicConnection first = _sonicDiscovery.Connections[0];
lblSonicStatus.Text =
- $"{_sonicDiscovery.Connections.Count} Verb. | {first.ManagementMode} | {first.DomainName} | SonicHome={first.SonicHome}";
+ $"{_sonicDiscovery.Connections.Count} Verb. | {first.ManagementModeDisplay} | {first.DomainName} | {first.ConnectionUrl}";
lblSonicStatus.ForeColor = GreenColor;
}
diff --git a/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs b/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs
index ff4a63b..4265d2d 100644
--- a/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs
+++ b/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs
@@ -38,15 +38,104 @@ public sealed class SonicConnection
public string WinRmPassword { get; init; } = string.Empty;
///
- /// WinRm = remote via PowerShell Remoting;
- /// LocalCmd = lokal auf dem Sonic-Server (kein WinRM, App muss dort laufen);
- /// HttpApi = REST.
+ /// MfApi = Sonic MF Management Runtime API über ConnectionUrl + SMC-Logins (Standard);
+ /// WinRm / LocalCmd = optionaler Fallback über stopcontainer/startcontainer;
+ /// HttpApi = REST (falls vorhanden).
///
- public SonicManagementMode ManagementMode { get; init; } = SonicManagementMode.WinRm;
+ public SonicManagementMode ManagementMode { get; init; } = SonicManagementMode.MfApi;
- /// Sonic-Installationsroot, z.B. "C:\Sonic\MQ10.0".
+ ///
+ /// Effektiver Modus: bei WinRm und lokalem ConnectionUrl-Host wird LocalCmd erzwungen.
+ /// MfApi bleibt unverändert (nutzt Domain-Manager-Verbindung).
+ ///
+ public SonicManagementMode EffectiveManagementMode
+ => ManagementMode == SonicManagementMode.WinRm && IsConnectionHostLocal(ConnectionUrl)
+ ? SonicManagementMode.LocalCmd
+ : ManagementMode;
+
+ /// True wenn WinRm konfiguriert war, aber wegen lokalem Host auf LocalCmd umgestellt wurde.
+ public bool IsLocalCmdAutoForced
+ => ManagementMode == SonicManagementMode.WinRm
+ && EffectiveManagementMode == SonicManagementMode.LocalCmd;
+
+ /// Anzeigetext für UI (inkl. Auto-Erkennung).
+ public string ManagementModeDisplay
+ => IsLocalCmdAutoForced
+ ? "LocalCmd (Host lokal erkannt)"
+ : EffectiveManagementMode switch
+ {
+ SonicManagementMode.MfApi => "MfApi (Sonic Domain Manager)",
+ _ => EffectiveManagementMode.ToString()
+ };
+
+ ///
+ /// True wenn der Host aus ConnectionUrl dieser Maschine entspricht
+ /// (localhost / 127.0.0.1 / ::1 / Computername).
+ ///
+ 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;
+ }
+
+ /// Sonic-Installationsroot, z.B. "C:\Sonic\MQ10.0" (enthält lib\*.jar für MfApi).
public string SonicHome { get; init; } = @"C:\Sonic\MQ10.0";
+ ///
+ /// Optionaler Pfad zu Sonic-Client-JARs für MfApi (mgmt_client.jar etc.).
+ /// Leer = SonicHome\lib.
+ ///
+ public string MfClientLibPath { get; init; } = string.Empty;
+
///
/// Container-Namen (kurz), z.B. ["DE-Test"]. Nicht die Domain.
///
@@ -281,5 +370,7 @@ public enum SonicManagementMode
{
HttpApi = 0,
WinRm = 1,
- LocalCmd = 2
+ LocalCmd = 2,
+ /// Sonic MF Management API über Domain-Manager (ConnectionUrl + SMC-Credentials).
+ MfApi = 3
}
diff --git a/ZA.CoreService.ESBCertificateManager/Services/SonicManagementClient.cs b/ZA.CoreService.ESBCertificateManager/Services/SonicManagementClient.cs
index eca3359..3e0e2ec 100644
--- a/ZA.CoreService.ESBCertificateManager/Services/SonicManagementClient.cs
+++ b/ZA.CoreService.ESBCertificateManager/Services/SonicManagementClient.cs
@@ -9,13 +9,12 @@ namespace ZA.CoreService.ESBCertificateManager.Services;
///
/// Verwaltet Sonic ESB Container über die Management Console.
///
-/// Modus = WinRm (Standard):
-/// PowerShell Remoting (Invoke-Command) → CMD stop/startcontainer auf dem Sonic-Server.
-/// WinRM nutzt WinRmUsername/WinRmPassword (Windows); leer = aktueller Benutzer.
-/// Username/Password bleiben Sonic-SMC-/Domain-Manager-Logins.
+/// Modus = MfApi (Standard):
+/// Offizielle Sonic MF Management Runtime API über ConnectionUrl + SMC-Logins
+/// (JMSConnectorClient → MFProxyFactory.createAgentProxy → IAgentProxy.restart).
///
-/// Modus = LocalCmd:
-/// Dieselbe CMD/PowerShell-Logik lokal (App muss auf dem Sonic-PC laufen; kein WinRM).
+/// Modus = WinRm / LocalCmd (optionaler Fallback):
+/// PowerShell/CMD stopcontainer/startcontainer (nicht Domain-Manager-nativ).
///
/// Modus = HttpApi:
/// HTTP REST API mit automatischer Pfad-Erkennung.
@@ -25,6 +24,7 @@ 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;
@@ -37,14 +37,22 @@ public sealed class SonicManagementClient : IDisposable
"/containers"
];
+ private SonicManagementMode Mode => _connection.EffectiveManagementMode;
+
+ private bool UsesMfApi => Mode == SonicManagementMode.MfApi;
+
private bool UsesScripts =>
- _connection.ManagementMode is SonicManagementMode.WinRm or SonicManagementMode.LocalCmd;
+ Mode is SonicManagementMode.WinRm or SonicManagementMode.LocalCmd;
public SonicManagementClient(SonicConnection connection)
{
_connection = connection;
- if (UsesScripts)
+ if (UsesMfApi)
+ {
+ _mfApi = new SonicMfApiExecutor(connection);
+ }
+ else if (UsesScripts)
{
_scriptRunner = new WinRmExecutor(connection);
}
@@ -77,16 +85,25 @@ public sealed class SonicManagementClient : IDisposable
///
/// Prüft die Verbindung zur Management Console.
- /// WinRm: TCP-Ping auf WinRM-Port + Test-PSSession.
- /// Http: Probe gegen bekannte API-Pfade.
+ /// MfApi: Domain-Manager über ConnectionUrl + SMC-Credentials.
+ /// WinRm/LocalCmd: Script-Laufzeit.
+ /// Http: Probe gegen bekannte API-Pfade.
///
public async Task<(bool Success, string? Error, string? ResolvedPath)> CheckConnectionAsync(
CancellationToken cancellationToken = default)
{
+ if (UsesMfApi)
+ {
+ (bool ok, string? error) = await _mfApi!.TestConnectionAsync(cancellationToken);
+ string modeLabel =
+ $"MfApi {_connection.ConnectionUrl} Domain={_connection.DomainName}";
+ return (ok, error, ok ? modeLabel : null);
+ }
+
if (UsesScripts)
{
(bool ok, string? error) = await _scriptRunner!.TestConnectionAsync(cancellationToken);
- string modeLabel = _connection.ManagementMode == SonicManagementMode.LocalCmd
+ string modeLabel = Mode == SonicManagementMode.LocalCmd
? $"LocalCmd (SonicHome={_connection.SonicHome})"
: $"WinRM auf {ExtractHost(_connection.ConnectionUrl)}:{_connection.WinRmPort}";
return (ok, error, ok ? modeLabel : null);
@@ -100,7 +117,7 @@ public sealed class SonicManagementClient : IDisposable
return (false,
$"Keine Sonic HTTP-API unter {_http!.BaseAddress} gefunden.\n" +
$"Geprüfte Pfade: {string.Join(", ", GetCandidatePaths())}\n" +
- $"Tipp: ManagementMode auf 'WinRm' setzen falls kein HTTP-API vorhanden.",
+ $"Tipp: ManagementMode auf 'MfApi' setzen (ConnectionUrl + SMC-Logins).",
null);
}
@@ -110,7 +127,7 @@ public sealed class SonicManagementClient : IDisposable
{
return (false,
$"HTTP-Timeout nach {_connection.TimeoutSeconds}s – Port {_connection.ManagementHttpPort} nicht erreichbar.\n" +
- $"Tipp: ManagementMode auf 'WinRm' setzen.",
+ $"Tipp: ManagementMode auf 'MfApi' setzen.",
null);
}
catch (Exception ex)
@@ -125,6 +142,11 @@ public sealed class SonicManagementClient : IDisposable
public async Task<(bool Success, IReadOnlyList ContainerNames, string? Error)> GetContainersAsync(
CancellationToken cancellationToken = default)
{
+ if (UsesMfApi)
+ {
+ return await GetContainersViaMfApiAsync(cancellationToken);
+ }
+
if (UsesScripts)
{
return await GetContainersViaScriptAsync(cancellationToken);
@@ -140,6 +162,11 @@ public sealed class SonicManagementClient : IDisposable
string containerName,
CancellationToken cancellationToken = default)
{
+ if (UsesMfApi)
+ {
+ return await RestartViaMfApiAsync(containerName, cancellationToken);
+ }
+
if (UsesScripts)
{
return await RestartViaScriptAsync(containerName, cancellationToken);
@@ -156,6 +183,41 @@ public sealed class SonicManagementClient : IDisposable
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);
@@ -164,6 +226,86 @@ public sealed class SonicManagementClient : IDisposable
return await ImportXapiViaHttpAsync(containerName, xapiSourcePath, cancellationToken);
}
+ // ---------------------------------------------------------------
+ // MfApi (Sonic Domain Manager / IAgentProxy.restart)
+ // ---------------------------------------------------------------
+
+ private async Task<(bool, IReadOnlyList, string?)> GetContainersViaMfApiAsync(
+ CancellationToken cancellationToken)
+ {
+ (bool ok, IReadOnlyList 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 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);
+
+ if (!ok)
+ {
+ return (false, "Neustart fehlgeschlagen (MfApi)",
+ $"IAgentProxy.restart für '{containerName}' (Domain '{_connection.DomainName}') " +
+ $"über {_connection.ConnectionUrl} fehlgeschlagen.\n" +
+ $"Fehler: {error}\nAusgabe: {output}\n" +
+ "Voraussetzungen: Domain Manager erreichbar, SMC-Logins korrekt, " +
+ "Java/JDK + Sonic-Client-JARs (SonicHome\\lib), Container online.");
+ }
+
+ await Task.Delay(
+ TimeSpan.FromSeconds(Math.Clamp(_connection.PostRestartDelaySeconds, 0, 120)),
+ cancellationToken);
+
+ return (true, $"Container '{containerName}' neugestartet (MfApi)",
+ $"Domain={_connection.DomainName}; ConnectionUrl={_connection.ConnectionUrl}\n{output}");
+ }
+
+ 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,
+ 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)
// ---------------------------------------------------------------
@@ -185,7 +327,7 @@ public sealed class SonicManagementClient : IDisposable
if (!ok)
{
- return (false, [], $"Container-Liste fehlgeschlagen ({_connection.ManagementMode}): {error}");
+ return (false, [], $"Container-Liste fehlgeschlagen ({Mode}): {error}");
}
List names = (output ?? string.Empty)
@@ -213,7 +355,7 @@ public sealed class SonicManagementClient : IDisposable
(bool ok, string? output, string? error) =
await _scriptRunner!.RunScriptAsync(script, cancellationToken);
- string mode = _connection.ManagementMode.ToString();
+ string mode = Mode.ToString();
bool verified = (output ?? string.Empty)
.Contains("OK:ContainerRestartVerified", StringComparison.OrdinalIgnoreCase);
diff --git a/ZA.CoreService.ESBCertificateManager/Services/SonicMfApiExecutor.cs b/ZA.CoreService.ESBCertificateManager/Services/SonicMfApiExecutor.cs
new file mode 100644
index 0000000..b674ca2
--- /dev/null
+++ b/ZA.CoreService.ESBCertificateManager/Services/SonicMfApiExecutor.cs
@@ -0,0 +1,392 @@
+using System.Diagnostics;
+using System.Text;
+using ZA.CoreService.ESBCertificateManager.Models;
+
+namespace ZA.CoreService.ESBCertificateManager.Services;
+
+///
+/// Führt Sonic-Container-Operationen über die offizielle MF Management Runtime API aus
+/// (JMSConnectorClient + MFProxyFactory + IAgentProxy.restart), per Java-Hilfsprogramm
+/// und Client-JARs unter SonicHome/lib bzw. MfClientLibPath.
+/// Nutzt ConnectionUrl + Username/Password aus appsettings (SMC / Domain Manager).
+///
+public sealed class SonicMfApiExecutor
+{
+ private readonly SonicConnection _connection;
+
+ public SonicMfApiExecutor(SonicConnection connection)
+ {
+ _connection = connection;
+ }
+
+ public Task<(bool Success, string? Error)> TestConnectionAsync(CancellationToken cancellationToken = default)
+ => RunAsync("ping", container: null, cancellationToken);
+
+ public async Task<(bool Success, IReadOnlyList Containers, string? Error)> ListContainersAsync(
+ CancellationToken cancellationToken = default)
+ {
+ (bool ok, string? output, string? error) = await RunRawAsync("list", null, cancellationToken);
+ if (!ok)
+ {
+ return (false, [], error ?? output);
+ }
+
+ List names = (output ?? string.Empty)
+ .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+ .Where(l => !l.StartsWith("OK:", StringComparison.OrdinalIgnoreCase))
+ .Where(l => !l.StartsWith("INFO:", StringComparison.OrdinalIgnoreCase))
+ .Where(l => !l.StartsWith("ERROR:", StringComparison.OrdinalIgnoreCase))
+ .Where(l => !l.StartsWith("WARN:", StringComparison.OrdinalIgnoreCase))
+ .Where(l => !l.StartsWith("Usage:", StringComparison.OrdinalIgnoreCase))
+ .ToList();
+
+ return (true, names, null);
+ }
+
+ public async Task<(bool Success, string? Output, string? Error)> RestartAsync(
+ string containerName,
+ CancellationToken cancellationToken = default)
+ {
+ (bool ok, string? output, string? error) = await RunRawAsync("restart", containerName, cancellationToken);
+ if (!ok)
+ {
+ return (false, output, error);
+ }
+
+ bool invoked = (output ?? string.Empty)
+ .Contains("OK:RestartInvoked", StringComparison.OrdinalIgnoreCase);
+ return invoked
+ ? (true, output, error)
+ : (false, output, error ?? "Kein OK:RestartInvoked von SonicMfContainerTool.");
+ }
+
+ private async Task<(bool Success, string? Error)> RunAsync(
+ string command,
+ string? container,
+ CancellationToken cancellationToken)
+ {
+ (bool ok, string? output, string? error) = await RunRawAsync(command, container, cancellationToken);
+ if (ok)
+ {
+ return (true, null);
+ }
+
+ return (false, error ?? output);
+ }
+
+ private async Task<(bool Success, string? Output, string? Error)> RunRawAsync(
+ string command,
+ string? container,
+ CancellationToken cancellationToken)
+ {
+ (bool prepared, string? javaExe, string? classDir, string? classpath, string? prepareError) =
+ await PrepareToolAsync(cancellationToken);
+ if (!prepared)
+ {
+ return (false, null, prepareError);
+ }
+
+ List args =
+ [
+ "-cp", Quote(classpath!),
+ "SonicMfContainerTool",
+ command,
+ "--domain", _connection.DomainName,
+ "--url", _connection.ConnectionUrl,
+ "--user", _connection.Username,
+ "--timeout", Math.Clamp(_connection.TimeoutSeconds, 5, 600).ToString()
+ ];
+
+ if (!string.IsNullOrWhiteSpace(container))
+ {
+ args.Add("--container");
+ args.Add(container);
+ }
+
+ ProcessStartInfo psi = new()
+ {
+ FileName = javaExe!,
+ Arguments = string.Join(" ", args.Select(EscapeArg)),
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true,
+ StandardOutputEncoding = Encoding.UTF8,
+ StandardErrorEncoding = Encoding.UTF8,
+ WorkingDirectory = classDir!
+ };
+
+ psi.Environment["ESB_SONIC_PASSWORD"] = _connection.Password ?? string.Empty;
+
+ using Process process = new() { StartInfo = psi };
+ if (!process.Start())
+ {
+ return (false, null, "Java-Prozess konnte nicht gestartet werden.");
+ }
+
+ using CancellationTokenSource timeoutCts =
+ CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ timeoutCts.CancelAfter(TimeSpan.FromSeconds(Math.Clamp(_connection.TimeoutSeconds, 10, 600)));
+
+ Task stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
+ Task 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, $"MfApi Timeout nach {_connection.TimeoutSeconds}s.");
+ }
+
+ string stdout = (await stdoutTask).Trim();
+ string stderr = (await stderrTask).Trim();
+ string combined = string.Join("\n", new[] { stdout, stderr }.Where(s => s.Length > 0));
+
+ if (process.ExitCode == 0
+ && !combined.Contains("ERROR:", StringComparison.OrdinalIgnoreCase))
+ {
+ return (true, combined, null);
+ }
+
+ string err = ExtractError(combined)
+ ?? $"SonicMfContainerTool ExitCode={process.ExitCode}";
+ return (false, combined.Length > 0 ? combined : null, err);
+ }
+
+ private async Task<(bool Ok, string? JavaExe, string? ClassDir, string? Classpath, string? Error)>
+ PrepareToolAsync(CancellationToken cancellationToken)
+ {
+ string? javaExe = ResolveJavaExecutable();
+ if (javaExe is null)
+ {
+ return (false, null, null, null,
+ "Java nicht gefunden (JAVA_HOME/bin/java.exe oder PATH). " +
+ "Für MfApi wird ein JRE/JDK benötigt.");
+ }
+
+ string? libDir = ResolveLibDirectory();
+ if (libDir is null)
+ {
+ return (false, null, null, null,
+ "Sonic-Client-JARs nicht gefunden. SonicHome\\lib oder MfClientLibPath setzen " +
+ $"(aktuell SonicHome='{_connection.SonicHome}', MfClientLibPath='{_connection.MfClientLibPath}'). " +
+ "Benötigt u.a. mgmt_client.jar / mfcontext.jar / sonic_Client.jar.");
+ }
+
+ string? sourcePath = ResolveToolSourcePath();
+ if (sourcePath is null)
+ {
+ return (false, null, null, null,
+ "Tools\\SonicMfContainerTool.java nicht gefunden (App-Ausgabeverzeichnis prüfen).");
+ }
+
+ string workDir = Path.Combine(Path.GetTempPath(), "esb-sonic-mf");
+ Directory.CreateDirectory(workDir);
+
+ string javaTarget = Path.Combine(workDir, "SonicMfContainerTool.java");
+ string classFile = Path.Combine(workDir, "SonicMfContainerTool.class");
+
+ File.Copy(sourcePath, javaTarget, overwrite: true);
+
+ string classpath = BuildClasspath(libDir, workDir);
+ bool needsCompile = !File.Exists(classFile)
+ || File.GetLastWriteTimeUtc(classFile) < File.GetLastWriteTimeUtc(sourcePath);
+
+ if (needsCompile)
+ {
+ string? javac = ResolveJavacExecutable(javaExe);
+ if (javac is null)
+ {
+ return (false, null, null, null,
+ "javac nicht gefunden. JDK installieren (nicht nur JRE), " +
+ "damit SonicMfContainerTool.java kompiliert werden kann.");
+ }
+
+ ProcessStartInfo compilePsi = new()
+ {
+ FileName = javac,
+ Arguments = $"-encoding UTF-8 -cp {Quote(BuildClasspath(libDir, null))} -d {Quote(workDir)} {Quote(javaTarget)}",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true,
+ StandardOutputEncoding = Encoding.UTF8,
+ StandardErrorEncoding = Encoding.UTF8
+ };
+
+ using Process compile = new() { StartInfo = compilePsi };
+ if (!compile.Start())
+ {
+ return (false, null, null, null, "javac konnte nicht gestartet werden.");
+ }
+
+ string cOut = await compile.StandardOutput.ReadToEndAsync(cancellationToken);
+ string cErr = await compile.StandardError.ReadToEndAsync(cancellationToken);
+ await compile.WaitForExitAsync(cancellationToken);
+
+ if (compile.ExitCode != 0 || !File.Exists(classFile))
+ {
+ return (false, null, null, null,
+ "Kompilieren von SonicMfContainerTool fehlgeschlagen.\n" +
+ Truncate((cErr + "\n" + cOut).Trim()) +
+ $"\nClasspath-Lib: {libDir}");
+ }
+ }
+
+ return (true, javaExe, workDir, classpath, null);
+ }
+
+ private string? ResolveLibDirectory()
+ {
+ List candidates = [];
+
+ if (!string.IsNullOrWhiteSpace(_connection.MfClientLibPath))
+ {
+ candidates.Add(_connection.MfClientLibPath);
+ }
+
+ if (!string.IsNullOrWhiteSpace(_connection.SonicHome))
+ {
+ candidates.Add(Path.Combine(_connection.SonicHome, "lib"));
+ candidates.Add(_connection.SonicHome);
+ }
+
+ foreach (string dir in candidates.Where(Directory.Exists))
+ {
+ if (Directory.EnumerateFiles(dir, "*.jar", SearchOption.TopDirectoryOnly).Any())
+ {
+ return dir;
+ }
+ }
+
+ return null;
+ }
+
+ private static string BuildClasspath(string libDir, string? extraDir)
+ {
+ // Java-Classpath-Wildcard für alle JARs im Lib-Ordner
+ string jars = Path.Combine(libDir, "*");
+ return string.IsNullOrWhiteSpace(extraDir)
+ ? jars
+ : jars + Path.PathSeparator + extraDir;
+ }
+
+ private static string? ResolveToolSourcePath()
+ {
+ string[] candidates =
+ [
+ Path.Combine(AppContext.BaseDirectory, "Tools", "SonicMfContainerTool.java"),
+ Path.Combine(Directory.GetCurrentDirectory(), "Tools", "SonicMfContainerTool.java"),
+ Path.Combine(AppContext.BaseDirectory, "SonicMfContainerTool.java")
+ ];
+
+ return candidates.FirstOrDefault(File.Exists);
+ }
+
+ private static string? ResolveJavaExecutable()
+ {
+ string? javaHome = Environment.GetEnvironmentVariable("JAVA_HOME");
+ if (!string.IsNullOrWhiteSpace(javaHome))
+ {
+ string candidate = Path.Combine(javaHome, "bin", "java.exe");
+ if (File.Exists(candidate))
+ {
+ return candidate;
+ }
+ }
+
+ return FindOnPath("java.exe") ?? FindOnPath("java");
+ }
+
+ private static string? ResolveJavacExecutable(string javaExe)
+ {
+ string? dir = Path.GetDirectoryName(javaExe);
+ if (!string.IsNullOrWhiteSpace(dir))
+ {
+ string sibling = Path.Combine(dir, OperatingSystem.IsWindows() ? "javac.exe" : "javac");
+ if (File.Exists(sibling))
+ {
+ return sibling;
+ }
+ }
+
+ string? javaHome = Environment.GetEnvironmentVariable("JAVA_HOME");
+ if (!string.IsNullOrWhiteSpace(javaHome))
+ {
+ string candidate = Path.Combine(javaHome, "bin",
+ OperatingSystem.IsWindows() ? "javac.exe" : "javac");
+ if (File.Exists(candidate))
+ {
+ return candidate;
+ }
+ }
+
+ return FindOnPath("javac.exe") ?? FindOnPath("javac");
+ }
+
+ private static string? FindOnPath(string fileName)
+ {
+ string? pathEnv = Environment.GetEnvironmentVariable("PATH");
+ if (string.IsNullOrWhiteSpace(pathEnv))
+ {
+ return null;
+ }
+
+ foreach (string dir in pathEnv.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
+ {
+ try
+ {
+ string candidate = Path.Combine(dir.Trim('"'), fileName);
+ if (File.Exists(candidate))
+ {
+ return candidate;
+ }
+ }
+ catch
+ {
+ // ignore bad PATH entries
+ }
+ }
+
+ return null;
+ }
+
+ private static string? ExtractError(string combined)
+ {
+ foreach (string line in combined.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries))
+ {
+ if (line.StartsWith("ERROR:", StringComparison.OrdinalIgnoreCase))
+ {
+ return line["ERROR:".Length..].Trim();
+ }
+ }
+
+ return string.IsNullOrWhiteSpace(combined) ? null : Truncate(combined);
+ }
+
+ private static string Quote(string value)
+ => "\"" + value.Replace("\"", "\\\"") + "\"";
+
+ private static string EscapeArg(string value)
+ {
+ if (value.Length == 0)
+ {
+ return "\"\"";
+ }
+
+ bool needsQuotes = value.Contains(' ') || value.Contains('\t') || value.Contains('"')
+ || value.Contains('*') || value.Contains(';');
+ if (!needsQuotes)
+ {
+ return value;
+ }
+
+ return "\"" + value.Replace("\"", "\\\"") + "\"";
+ }
+
+ private static string Truncate(string s, int max = 800)
+ => s.Length <= max ? s : s[..max] + "…";
+}
diff --git a/ZA.CoreService.ESBCertificateManager/Services/WinRmExecutor.cs b/ZA.CoreService.ESBCertificateManager/Services/WinRmExecutor.cs
index 858775c..bd9def5 100644
--- a/ZA.CoreService.ESBCertificateManager/Services/WinRmExecutor.cs
+++ b/ZA.CoreService.ESBCertificateManager/Services/WinRmExecutor.cs
@@ -27,7 +27,7 @@ public sealed class WinRmExecutor
public async Task<(bool Success, string? Error)> TestConnectionAsync(
CancellationToken cancellationToken = default)
{
- if (_connection.ManagementMode == SonicManagementMode.LocalCmd)
+ if (_connection.EffectiveManagementMode == SonicManagementMode.LocalCmd)
{
(bool ok, string? output, string? error) = await RunScriptAsync(
"$env:COMPUTERNAME", cancellationToken);
@@ -66,7 +66,7 @@ public sealed class WinRmExecutor
string scriptBlock,
CancellationToken cancellationToken = default)
{
- string fullScript = _connection.ManagementMode == SonicManagementMode.LocalCmd
+ string fullScript = _connection.EffectiveManagementMode == SonicManagementMode.LocalCmd
? BuildLocalScript(scriptBlock)
: BuildRemoteScript(ExtractHost(_connection.ConnectionUrl), scriptBlock);
@@ -126,7 +126,7 @@ public sealed class WinRmExecutor
? stderr
: $"PowerShell ExitCode={process.ExitCode}";
- string error = _connection.ManagementMode == SonicManagementMode.WinRm
+ string error = _connection.EffectiveManagementMode == SonicManagementMode.WinRm
? FormatWinRmFailure(rawError)
: Truncate(rawError);
diff --git a/ZA.CoreService.ESBCertificateManager/Tools/SonicMfContainerTool.java b/ZA.CoreService.ESBCertificateManager/Tools/SonicMfContainerTool.java
new file mode 100644
index 0000000..973304d
--- /dev/null
+++ b/ZA.CoreService.ESBCertificateManager/Tools/SonicMfContainerTool.java
@@ -0,0 +1,300 @@
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.Set;
+
+import javax.management.ObjectName;
+
+import com.sonicsw.mf.jmx.client.JMSConnectorAddress;
+import com.sonicsw.mf.jmx.client.JMSConnectorClient;
+import com.sonicsw.mf.mgmtapi.runtime.IAgentProxy;
+import com.sonicsw.mf.mgmtapi.runtime.MFProxyFactory;
+
+/**
+ * Sonic MF Management Runtime API helper (Aurea CX Messenger / Progress Sonic).
+ *
+ * Documented path (Management Application API):
+ * Hashtable env with ConnectionURLs / DefaultUser / DefaultPassword
+ * -> JMSConnectorAddress / JMSConnectorClient.connect
+ * -> MFProxyFactory.createAgentProxy(connector, ObjectName)
+ * -> IAgentProxy.restart() (same lifecycle action as SMC Restart)
+ *
+ * ObjectName pattern: {domain}.{container}:ID=AGENT
+ *
+ * Usage:
+ * java -cp "...libs...;." SonicMfContainerTool ping|list|restart
+ * --domain proalpha-test
+ * --url tcp://host:13070
+ * --user Administrator
+ * --password *** (or env ESB_SONIC_PASSWORD)
+ * [--container DE-Test] (required for restart)
+ * [--timeout 120]
+ */
+public final class SonicMfContainerTool
+{
+ public static void main(String[] args)
+ {
+ try
+ {
+ Args a = Args.parse(args);
+ if (a.command == null)
+ {
+ fail("Usage: SonicMfContainerTool ping|list|restart --domain D --url U --user U [--password P] [--container C] [--timeout SEC]");
+ return;
+ }
+
+ if (isBlank(a.domain) || isBlank(a.url) || isBlank(a.user))
+ {
+ fail("domain, url und user sind Pflicht.");
+ return;
+ }
+
+ if ("restart".equals(a.command) && isBlank(a.container))
+ {
+ fail("restart erfordert --container.");
+ return;
+ }
+
+ long timeoutMs = Math.max(5_000L, a.timeoutSec * 1000L);
+ JMSConnectorClient connector = connect(a.url, a.user, a.password, timeoutMs);
+ try
+ {
+ if ("ping".equals(a.command))
+ {
+ ping(connector, a.domain);
+ }
+ else if ("list".equals(a.command))
+ {
+ list(connector, a.domain);
+ }
+ else if ("restart".equals(a.command))
+ {
+ restart(connector, a.domain, a.container);
+ }
+ else
+ {
+ fail("Unbekannter Befehl: " + a.command);
+ }
+ }
+ finally
+ {
+ try { connector.disconnect(); } catch (Exception ignore) { /* ignore */ }
+ }
+ }
+ catch (Throwable t)
+ {
+ fail(t.getClass().getSimpleName() + ": " + t.getMessage());
+ }
+ }
+
+ private static JMSConnectorClient connect(String url, String user, String password, long timeoutMs)
+ throws Exception
+ {
+ Hashtable env = new Hashtable();
+ env.put("ConnectionURLs", url);
+ env.put("DefaultUser", user);
+ env.put("DefaultPassword", password == null ? "" : password);
+
+ JMSConnectorAddress address = new JMSConnectorAddress(env);
+ JMSConnectorClient connector = new JMSConnectorClient();
+ connector.connect(address, timeoutMs);
+ System.out.println("INFO:Connected url=" + url + " user=" + user);
+ return connector;
+ }
+
+ private static void ping(JMSConnectorClient connector, String domain) throws Exception
+ {
+ Set names = connector.queryNames(new ObjectName("*:ID=AGENT"), null);
+ int matched = 0;
+ if (names != null)
+ {
+ for (Iterator it = names.iterator(); it.hasNext(); )
+ {
+ ObjectName on = (ObjectName) it.next();
+ if (belongsToDomain(on, domain))
+ {
+ matched++;
+ }
+ }
+ }
+ System.out.println("OK:MfApiPing domain=" + domain + " agents=" + matched);
+ }
+
+ private static void list(JMSConnectorClient connector, String domain) throws Exception
+ {
+ Set names = connector.queryNames(new ObjectName("*:ID=AGENT"), null);
+ int count = 0;
+ if (names != null)
+ {
+ for (Iterator it = names.iterator(); it.hasNext(); )
+ {
+ ObjectName on = (ObjectName) it.next();
+ String container = extractContainer(on, domain);
+ if (container != null)
+ {
+ System.out.println(container);
+ count++;
+ }
+ }
+ }
+ if (count == 0)
+ {
+ System.out.println("WARN:KeineAgentContainer Domain=" + domain);
+ }
+ System.out.println("OK:List count=" + count);
+ }
+
+ private static void restart(JMSConnectorClient connector, String domain, String container) throws Exception
+ {
+ String shortName = shortContainer(container);
+ ObjectName on = resolveAgentObjectName(connector, domain, shortName);
+ System.out.println("INFO:ObjectName=" + on);
+
+ IAgentProxy agent = MFProxyFactory.createAgentProxy(connector, on);
+ String stateBefore = safeState(agent);
+ System.out.println("INFO:StateBefore=" + stateBefore);
+
+ agent.restart();
+ System.out.println("OK:RestartInvoked container=" + shortName + " domain=" + domain
+ + " stateBefore=" + stateBefore);
+ }
+
+ private static ObjectName resolveAgentObjectName(JMSConnectorClient connector, String domain, String container)
+ throws Exception
+ {
+ ObjectName preferred = new ObjectName(domain + "." + container + ":ID=AGENT");
+ Set exact = connector.queryNames(preferred, null);
+ if (exact != null && !exact.isEmpty())
+ {
+ return (ObjectName) exact.iterator().next();
+ }
+
+ Set names = connector.queryNames(new ObjectName("*:ID=AGENT"), null);
+ if (names != null)
+ {
+ for (Iterator it = names.iterator(); it.hasNext(); )
+ {
+ ObjectName on = (ObjectName) it.next();
+ String c = extractContainer(on, domain);
+ if (c != null && c.equalsIgnoreCase(container))
+ {
+ return on;
+ }
+ }
+ }
+
+ // Fallback: documented canonical name even if not yet in query result
+ return preferred;
+ }
+
+ private static boolean belongsToDomain(ObjectName on, String domain)
+ {
+ return extractContainer(on, domain) != null;
+ }
+
+ private static String extractContainer(ObjectName on, String domain)
+ {
+ if (on == null || isBlank(domain))
+ {
+ return null;
+ }
+ String jmxDomain = on.getDomain();
+ if (jmxDomain == null)
+ {
+ return null;
+ }
+ String prefix = domain + ".";
+ if (jmxDomain.regionMatches(true, 0, prefix, 0, prefix.length()))
+ {
+ return jmxDomain.substring(prefix.length());
+ }
+ return null;
+ }
+
+ private static String shortContainer(String container)
+ {
+ if (container == null)
+ {
+ return "";
+ }
+ int dot = container.indexOf('.');
+ if (dot >= 0 && dot < container.length() - 1)
+ {
+ return container.substring(dot + 1);
+ }
+ return container;
+ }
+
+ private static String safeState(IAgentProxy agent)
+ {
+ try
+ {
+ String s = agent.getStateString();
+ return s == null ? "?" : s;
+ }
+ catch (Throwable t)
+ {
+ return "?";
+ }
+ }
+
+ private static void fail(String message)
+ {
+ System.err.println("ERROR:" + message);
+ System.exit(2);
+ }
+
+ private static boolean isBlank(String s)
+ {
+ return s == null || s.trim().length() == 0;
+ }
+
+ private static final class Args
+ {
+ String command;
+ String domain;
+ String url;
+ String user;
+ String password;
+ String container;
+ int timeoutSec = 120;
+
+ static Args parse(String[] args)
+ {
+ Args a = new Args();
+ if (args == null || args.length == 0)
+ {
+ return a;
+ }
+
+ a.command = args[0].toLowerCase();
+ for (int i = 1; i < args.length; i++)
+ {
+ String key = args[i];
+ if (!key.startsWith("--") || i + 1 >= args.length)
+ {
+ continue;
+ }
+ String val = args[++i];
+ if ("--domain".equals(key)) a.domain = val;
+ else if ("--url".equals(key)) a.url = val;
+ else if ("--user".equals(key)) a.user = val;
+ else if ("--password".equals(key)) a.password = val;
+ else if ("--container".equals(key)) a.container = val;
+ else if ("--timeout".equals(key))
+ {
+ try { a.timeoutSec = Integer.parseInt(val); } catch (Exception ignore) { /* keep default */ }
+ }
+ }
+
+ if (isBlank(a.password))
+ {
+ String env = System.getenv("ESB_SONIC_PASSWORD");
+ if (!isBlank(env))
+ {
+ a.password = env;
+ }
+ }
+ return a;
+ }
+ }
+}
diff --git a/ZA.CoreService.ESBCertificateManager/ZA.CoreService.ESBCertificateManager.csproj b/ZA.CoreService.ESBCertificateManager/ZA.CoreService.ESBCertificateManager.csproj
index b9d9b85..cb95dc6 100644
--- a/ZA.CoreService.ESBCertificateManager/ZA.CoreService.ESBCertificateManager.csproj
+++ b/ZA.CoreService.ESBCertificateManager/ZA.CoreService.ESBCertificateManager.csproj
@@ -30,6 +30,9 @@
PreserveNewest
+
+ PreserveNewest
+
diff --git a/ZA.CoreService.ESBCertificateManager/appsettings.json b/ZA.CoreService.ESBCertificateManager/appsettings.json
index 528b2cb..9628465 100644
--- a/ZA.CoreService.ESBCertificateManager/appsettings.json
+++ b/ZA.CoreService.ESBCertificateManager/appsettings.json
@@ -10,18 +10,21 @@
"Name": "DE-Test",
"DomainName": "proalpha-test",
"ConnectionUrl": "tcp://dekun-painwbdet:13070",
- // Sonic SMC / Domain Manager (NICHT für WinRM):
+ // Sonic SMC / Domain Manager (für MfApi-Neustart):
"Username": "Administrator",
"Password": "Administrator",
- // Windows für WinRM: leer = aktueller Benutzer (z.B. gisler). Nie Sonic-SMC-Logins hier eintragen.
- // Lokal auf dem Sonic-Server testen: ManagementMode auf "LocalCmd" setzen (kein WinRM nötig).
- "WinRmUsername": "",
- "WinRmPassword": "",
- "ManagementMode": "WinRm",
+ // Standard: Sonic MF Management API (IAgentProxy.restart) – kein WinRM.
+ // Benötigt Java/JDK + Client-JARs unter SonicHome\\lib (oder MfClientLibPath).
+ // Optional-Fallback: "WinRm" / "LocalCmd" (stopcontainer/startcontainer).
+ "ManagementMode": "MfApi",
"SonicHome": "C:\\Sonic\\MQ10.0",
+ "MfClientLibPath": "",
"KnownContainers": [
"DE-Test"
],
+ // Nur für ManagementMode=WinRm relevant; leer lassen wenn nicht genutzt:
+ "WinRmUsername": "",
+ "WinRmPassword": "",
"WinRmPort": 5985,
"TimeoutSeconds": 120,
"PostRestartDelaySeconds": 20,