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,