Fix app startup and enable Sonic ESB restart via remote CMD.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,7 +7,20 @@ public static class AppSettingsLoader
|
|||||||
{
|
{
|
||||||
public static AppSettings Load()
|
public static AppSettings Load()
|
||||||
{
|
{
|
||||||
string basePath = AppContext.BaseDirectory;
|
// Zuerst Output-Verzeichnis, dann aktuelles Arbeitsverzeichnis (z.B. VS Debug).
|
||||||
|
string basePath = Directory.Exists(AppContext.BaseDirectory)
|
||||||
|
? AppContext.BaseDirectory
|
||||||
|
: Directory.GetCurrentDirectory();
|
||||||
|
|
||||||
|
string settingsPath = Path.Combine(basePath, "appsettings.json");
|
||||||
|
if (!File.Exists(settingsPath))
|
||||||
|
{
|
||||||
|
string cwdPath = Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json");
|
||||||
|
if (File.Exists(cwdPath))
|
||||||
|
{
|
||||||
|
basePath = Directory.GetCurrentDirectory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
IConfigurationRoot configuration = new ConfigurationBuilder()
|
IConfigurationRoot configuration = new ConfigurationBuilder()
|
||||||
.SetBasePath(basePath)
|
.SetBasePath(basePath)
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"Id": 1,
|
"Id": 1,
|
||||||
"Name": "ESB Local A",
|
"Name": "DE-Test ESB Container",
|
||||||
"Environment": "DEV",
|
"Environment": "TEST",
|
||||||
"IsActive": true,
|
"IsActive": true,
|
||||||
"TargetDirectory": "DeploySandbox/target-a",
|
"TargetDirectory": "DeploySandbox/target-esb",
|
||||||
"CertificateFileName": "esb-cert.cer",
|
"CertificateFileName": "esb-cert.cer",
|
||||||
"ContainerName": "sonic-container-a",
|
"ContainerName": "ESB",
|
||||||
"RestartType": "None",
|
"RestartType": "SonicContainer",
|
||||||
"RestartCommand": "",
|
"RestartCommand": "",
|
||||||
"RestartArguments": "",
|
"RestartArguments": "",
|
||||||
"RestartTimeoutSeconds": 30,
|
"RestartTimeoutSeconds": 90,
|
||||||
"SonicConnectionName": "",
|
"SonicConnectionName": "DE-Test",
|
||||||
"XapiSourcePath": "",
|
"XapiSourcePath": "",
|
||||||
"TlsHost": "",
|
"TlsHost": "",
|
||||||
"TlsPort": null,
|
"TlsPort": null,
|
||||||
@@ -21,15 +21,15 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Id": 2,
|
"Id": 2,
|
||||||
"Name": "ESB Local B",
|
"Name": "Lokaler CMD-Test (Echo)",
|
||||||
"Environment": "DEV",
|
"Environment": "DEV",
|
||||||
"IsActive": true,
|
"IsActive": true,
|
||||||
"TargetDirectory": "DeploySandbox/target-b",
|
"TargetDirectory": "DeploySandbox/target-cmd",
|
||||||
"CertificateFileName": "esb-cert.cer",
|
"CertificateFileName": "esb-cert.cer",
|
||||||
"ContainerName": "sonic-container-b",
|
"ContainerName": "",
|
||||||
"RestartType": "Command",
|
"RestartType": "Command",
|
||||||
"RestartCommand": "cmd.exe",
|
"RestartCommand": "cmd.exe",
|
||||||
"RestartArguments": "/c echo Restart simulated for sonic-container-b",
|
"RestartArguments": "/c echo Neustart-Simulation OK",
|
||||||
"RestartTimeoutSeconds": 15,
|
"RestartTimeoutSeconds": 15,
|
||||||
"SonicConnectionName": "",
|
"SonicConnectionName": "",
|
||||||
"XapiSourcePath": "",
|
"XapiSourcePath": "",
|
||||||
@@ -38,45 +38,5 @@
|
|||||||
"TlsServerName": "",
|
"TlsServerName": "",
|
||||||
"ExpectedFingerprint": null,
|
"ExpectedFingerprint": null,
|
||||||
"SortOrder": 20
|
"SortOrder": 20
|
||||||
},
|
|
||||||
{
|
|
||||||
"Id": 3,
|
|
||||||
"Name": "DE-Test Container A (Sonic)",
|
|
||||||
"Environment": "TEST",
|
|
||||||
"IsActive": false,
|
|
||||||
"TargetDirectory": "DeploySandbox/target-c",
|
|
||||||
"CertificateFileName": "esb-cert.cer",
|
|
||||||
"ContainerName": "sonic-container-a",
|
|
||||||
"RestartType": "SonicContainer",
|
|
||||||
"RestartCommand": "",
|
|
||||||
"RestartArguments": "",
|
|
||||||
"RestartTimeoutSeconds": 60,
|
|
||||||
"SonicConnectionName": "DE-Test",
|
|
||||||
"XapiSourcePath": "",
|
|
||||||
"TlsHost": "dekun-painwbdet",
|
|
||||||
"TlsPort": 443,
|
|
||||||
"TlsServerName": "esb-test.firma.local",
|
|
||||||
"ExpectedFingerprint": null,
|
|
||||||
"SortOrder": 30
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Id": 4,
|
|
||||||
"Name": "DE-Test Container B (Sonic + XApi)",
|
|
||||||
"Environment": "TEST",
|
|
||||||
"IsActive": false,
|
|
||||||
"TargetDirectory": "DeploySandbox/target-d",
|
|
||||||
"CertificateFileName": "esb-cert.cer",
|
|
||||||
"ContainerName": "sonic-container-b",
|
|
||||||
"RestartType": "SonicContainerWithXapi",
|
|
||||||
"RestartCommand": "",
|
|
||||||
"RestartArguments": "",
|
|
||||||
"RestartTimeoutSeconds": 60,
|
|
||||||
"SonicConnectionName": "DE-Test",
|
|
||||||
"XapiSourcePath": "Assets/xapi-resources.xml",
|
|
||||||
"TlsHost": "dekun-painwbdet",
|
|
||||||
"TlsPort": 443,
|
|
||||||
"TlsServerName": "esb-test.firma.local",
|
|
||||||
"ExpectedFingerprint": null,
|
|
||||||
"SortOrder": 40
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ namespace ZA.CoreService.ESBCertificateManager
|
|||||||
private Button btnClose = null!;
|
private Button btnClose = null!;
|
||||||
private Button btnValidate = null!;
|
private Button btnValidate = null!;
|
||||||
private Button btnDeploy = null!;
|
private Button btnDeploy = null!;
|
||||||
|
private Button btnRestartOnly = null!;
|
||||||
private Button btnCancelRun = null!;
|
private Button btnCancelRun = null!;
|
||||||
|
|
||||||
private bool isMaximized = false;
|
private bool isMaximized = false;
|
||||||
@@ -1231,6 +1232,11 @@ namespace ZA.CoreService.ESBCertificateManager
|
|||||||
btnValidate.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
btnValidate.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
||||||
btnValidate.Click += async (_, _) => await RunValidationAsync();
|
btnValidate.Click += async (_, _) => await RunValidationAsync();
|
||||||
|
|
||||||
|
btnRestartOnly = CreateSecondaryButton("ESB neu starten");
|
||||||
|
btnRestartOnly.Size = new Size(150, 42);
|
||||||
|
btnRestartOnly.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
||||||
|
btnRestartOnly.Click += async (_, _) => await RunRestartOnlyAsync();
|
||||||
|
|
||||||
btnDeploy = CreatePrimaryButton("Deployment starten");
|
btnDeploy = CreatePrimaryButton("Deployment starten");
|
||||||
btnDeploy.Size = new Size(170, 42);
|
btnDeploy.Size = new Size(170, 42);
|
||||||
btnDeploy.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
btnDeploy.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
||||||
@@ -1238,12 +1244,14 @@ namespace ZA.CoreService.ESBCertificateManager
|
|||||||
|
|
||||||
panel.Controls.Add(btnCancelRun);
|
panel.Controls.Add(btnCancelRun);
|
||||||
panel.Controls.Add(btnValidate);
|
panel.Controls.Add(btnValidate);
|
||||||
|
panel.Controls.Add(btnRestartOnly);
|
||||||
panel.Controls.Add(btnDeploy);
|
panel.Controls.Add(btnDeploy);
|
||||||
|
|
||||||
panel.Resize += (_, _) =>
|
panel.Resize += (_, _) =>
|
||||||
{
|
{
|
||||||
btnDeploy.Left = panel.Width - btnDeploy.Width;
|
btnDeploy.Left = panel.Width - btnDeploy.Width;
|
||||||
btnValidate.Left = btnDeploy.Left - btnValidate.Width - 12;
|
btnRestartOnly.Left = btnDeploy.Left - btnRestartOnly.Width - 12;
|
||||||
|
btnValidate.Left = btnRestartOnly.Left - btnValidate.Width - 12;
|
||||||
btnCancelRun.Left = btnValidate.Left - btnCancelRun.Width - 12;
|
btnCancelRun.Left = btnValidate.Left - btnCancelRun.Width - 12;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1377,11 +1385,20 @@ namespace ZA.CoreService.ESBCertificateManager
|
|||||||
btnCancelRun.Enabled = _isOperationRunning;
|
btnCancelRun.Enabled = _isOperationRunning;
|
||||||
btnValidate.Enabled = idle && hasCertificate && hasTargets;
|
btnValidate.Enabled = idle && hasCertificate && hasTargets;
|
||||||
btnDeploy.Enabled = idle && hasCertificate && hasSelection;
|
btnDeploy.Enabled = idle && hasCertificate && hasSelection;
|
||||||
|
if (btnRestartOnly is not null)
|
||||||
|
{
|
||||||
|
btnRestartOnly.Enabled = idle && hasSelection;
|
||||||
|
}
|
||||||
|
|
||||||
if (dgvTargets is not null)
|
if (dgvTargets is not null)
|
||||||
{
|
{
|
||||||
dgvTargets.Enabled = idle;
|
dgvTargets.Enabled = idle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (btnLoadFromSonic is not null)
|
||||||
|
{
|
||||||
|
btnLoadFromSonic.Enabled = idle && _sonicDiscovery.HasConnections;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetStatus(string message, bool isError)
|
private void SetStatus(string message, bool isError)
|
||||||
@@ -1464,6 +1481,86 @@ namespace ZA.CoreService.ESBCertificateManager
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task RunRestartOnlyAsync()
|
||||||
|
{
|
||||||
|
if (_isOperationRunning)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DeploymentTarget> selected = GetSelectedTargets();
|
||||||
|
PreflightValidationResult preflight = _orchestrator.ValidateRestartOnly(selected);
|
||||||
|
|
||||||
|
if (!preflight.IsValid)
|
||||||
|
{
|
||||||
|
ShowPreflightIssues(
|
||||||
|
preflight,
|
||||||
|
"Neustart blockiert – Vorabprüfung fehlgeschlagen.",
|
||||||
|
"ESB neu starten");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogResult confirm = MessageBox.Show(
|
||||||
|
this,
|
||||||
|
$"ESB/Container für {selected.Count} Ziel(e) wirklich neu starten?\n\n" +
|
||||||
|
"Es wird stopcontainer/startcontainer über die Sonic-Verbindung ausgeführt\n" +
|
||||||
|
"(WinRM Remote-CMD oder LocalCmd laut appsettings).",
|
||||||
|
"ESB neu starten",
|
||||||
|
MessageBoxButtons.YesNo,
|
||||||
|
MessageBoxIcon.Warning);
|
||||||
|
|
||||||
|
if (confirm != DialogResult.Yes)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_runCts?.Dispose();
|
||||||
|
_runCts = new CancellationTokenSource();
|
||||||
|
SetOperationRunning(true);
|
||||||
|
UpdateToNextStep(3);
|
||||||
|
SetStatus($"ESB-Neustart läuft für {selected.Count} Ziel(e)…", isError: false);
|
||||||
|
|
||||||
|
Progress<TargetProgressUpdate> progress = new(update =>
|
||||||
|
{
|
||||||
|
SetTargetRowStatus(update.TargetId, update.StatusText);
|
||||||
|
SetStatus(update.StatusText, isError: update.SuccessHint == false);
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DeploymentRunResult runResult = await _orchestrator.RestartOnlyAsync(
|
||||||
|
selected,
|
||||||
|
progress,
|
||||||
|
_runCts.Token);
|
||||||
|
|
||||||
|
foreach (TargetStepResult targetResult in runResult.TargetResults)
|
||||||
|
{
|
||||||
|
SetTargetRowStatus(targetResult.TargetId, targetResult.StatusText);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetStatus(
|
||||||
|
runResult.OverallSuccess
|
||||||
|
? $"Neustart erfolgreich ({runResult.TargetResults.Count} Ziel(e))."
|
||||||
|
: "Neustart mit Fehlern beendet. Details in Status-Spalte / Log.",
|
||||||
|
isError: !runResult.OverallSuccess);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
SetStatus("Neustart abgebrochen.", isError: true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
SetStatus($"Neustart fehlgeschlagen: {ex.Message}", isError: true);
|
||||||
|
MessageBox.Show(this, ex.Message, "ESB neu starten", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
SetOperationRunning(false);
|
||||||
|
_runCts?.Dispose();
|
||||||
|
_runCts = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task RunDeploymentAsync()
|
private async Task RunDeploymentAsync()
|
||||||
{
|
{
|
||||||
if (_isOperationRunning || _loadedCertificateInfo is null)
|
if (_isOperationRunning || _loadedCertificateInfo is null)
|
||||||
@@ -1680,9 +1777,6 @@ namespace ZA.CoreService.ESBCertificateManager
|
|||||||
|
|
||||||
lblSonicStatus = new Label
|
lblSonicStatus = new Label
|
||||||
{
|
{
|
||||||
Text = _sonicDiscovery.HasConnections
|
|
||||||
? "Keine Verbindung konfiguriert"
|
|
||||||
: "Keine Sonic-Verbindung konfiguriert",
|
|
||||||
ForeColor = MutedTextColor,
|
ForeColor = MutedTextColor,
|
||||||
Font = new Font("Segoe UI", 8.5f),
|
Font = new Font("Segoe UI", 8.5f),
|
||||||
AutoSize = true,
|
AutoSize = true,
|
||||||
@@ -1696,7 +1790,10 @@ namespace ZA.CoreService.ESBCertificateManager
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
lblSonicStatus.Text = $"{_sonicDiscovery.Connections.Count} Verbindung(en) konfiguriert – noch nicht geladen";
|
SonicConnection first = _sonicDiscovery.Connections[0];
|
||||||
|
lblSonicStatus.Text =
|
||||||
|
$"{_sonicDiscovery.Connections.Count} Verb. | {first.ManagementMode} | {first.DomainName} | SonicHome={first.SonicHome}";
|
||||||
|
lblSonicStatus.ForeColor = GreenColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
card.Controls.Add(sonicLabel);
|
card.Controls.Add(sonicLabel);
|
||||||
|
|||||||
@@ -15,93 +15,148 @@ public sealed class SonicConnection
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sonic-Broker-/Management-URL im Sonic-Format, z.B. "tcp://dekun-painwbdet:13070".
|
/// Sonic-Broker-/Management-URL im Sonic-Format, z.B. "tcp://dekun-painwbdet:13070".
|
||||||
/// Der Hostname wird daraus extrahiert (für HTTP-Modus: Basis-URL, für WinRM-Modus: Zielrechner).
|
/// Der Hostname wird daraus extrahiert (WinRM-Ziel / HTTP-Basis).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string ConnectionUrl { get; init; }
|
public required string ConnectionUrl { get; init; }
|
||||||
|
|
||||||
/// <summary>Benutzername für die Management-Konsole.</summary>
|
/// <summary>Benutzername für WinRM / Management-Konsole.</summary>
|
||||||
public required string Username { get; init; }
|
public required string Username { get; init; }
|
||||||
|
|
||||||
/// <summary>Passwort für die Management-Konsole.</summary>
|
/// <summary>Passwort für WinRM / Management-Konsole.</summary>
|
||||||
public required string Password { get; init; }
|
public required string Password { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Management-Modus: HttpApi (REST) oder WinRm (PowerShell Remoting).
|
/// Management-Modus:
|
||||||
/// Standard: WinRm – da Sonic 10.x kein HTTP REST API bereitstellt.
|
/// - WinRm: PowerShell Remoting auf dem Sonic-Server (Standard)
|
||||||
|
/// - LocalCmd: CMD/PowerShell lokal (zum Testen direkt auf dem Sonic-PC)
|
||||||
|
/// - HttpApi: REST, falls vorhanden
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SonicManagementMode ManagementMode { get; init; } = SonicManagementMode.WinRm;
|
public SonicManagementMode ManagementMode { get; init; } = SonicManagementMode.WinRm;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Installationsroot von Sonic MQ / Management Console, z.B. "C:\Sonic\MQ10.0".
|
||||||
|
/// Wird für Default-Scripts (stopcontainer/startcontainer) benötigt.
|
||||||
|
/// </summary>
|
||||||
|
public string SonicHome { get; init; } = @"C:\Sonic\MQ10.0";
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
// HTTP REST API (ManagementMode = HttpApi)
|
// HTTP REST API (ManagementMode = HttpApi)
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
/// <summary>HTTP-Port der Sonic Management Console REST-API (Standard: 8080).</summary>
|
|
||||||
public int ManagementHttpPort { get; init; } = 8080;
|
public int ManagementHttpPort { get; init; } = 8080;
|
||||||
|
|
||||||
/// <summary>Präfix für alle REST-API-Pfade (Standard: "/api/v1").</summary>
|
|
||||||
public string ApiBasePath { get; init; } = "/api/v1";
|
public string ApiBasePath { get; init; } = "/api/v1";
|
||||||
|
|
||||||
/// <summary>Konfigurierter Pfad zum Auflisten aller Container. Leer = automatische Erkennung.</summary>
|
|
||||||
public string ContainerListPath { get; init; } = string.Empty;
|
public string ContainerListPath { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>Konfigurierter Pfad zum Neustarten. Platzhalter: {domain}, {container}.</summary>
|
|
||||||
public string ContainerRestartPath { get; init; } = string.Empty;
|
public string ContainerRestartPath { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>Konfigurierter Pfad zum Stoppen. Platzhalter: {domain}, {container}.</summary>
|
|
||||||
public string ContainerStopPath { get; init; } = string.Empty;
|
public string ContainerStopPath { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>Konfigurierter Pfad zum Starten. Platzhalter: {domain}, {container}.</summary>
|
|
||||||
public string ContainerStartPath { get; init; } = string.Empty;
|
public string ContainerStartPath { get; init; } = string.Empty;
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
// WinRM / PowerShell Remoting (ManagementMode = WinRm)
|
// WinRM / LocalCmd
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// WinRM-Port auf dem Zielrechner (Standard: 5985 = HTTP, 5986 = HTTPS).
|
|
||||||
/// </summary>
|
|
||||||
public int WinRmPort { get; init; } = 5985;
|
public int WinRmPort { get; init; } = 5985;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// PowerShell-Scriptblock zum Neustarten eines Containers.
|
/// PowerShell-Script zum Neustarten. Leer = Default über Sonic bin\stop/startcontainer.bat.
|
||||||
/// Platzhalter: {container} = Container-Name (nicht enkodiert), {domain} = Domain-Name.
|
/// Platzhalter: {container}, {domain}, {sonicHome}
|
||||||
/// Beispiel für Windows-Service: "Restart-Service -Name 'CT-ZADBService' -Force"
|
|
||||||
/// Beispiel für Sonic-Skript: "& 'C:\\Sonic\\bin\\stopContainer.bat' '{container}'; Start-Sleep 5; & 'C:\\Sonic\\bin\\startContainer.bat' '{container}'"
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string WinRmRestartScript { get; init; } = string.Empty;
|
public string WinRmRestartScript { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// PowerShell-Scriptblock zum Auflisten aller Container der Domain.
|
/// PowerShell-Script zum Auflisten der Container. Leer = Default (Services + SonicHome).
|
||||||
/// Ausgabe: eine Zeile pro Container-Name.
|
|
||||||
/// Beispiel: "Get-Service -DisplayName 'Sonic*' | Select-Object -ExpandProperty Name"
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string WinRmContainerListScript { get; init; } = string.Empty;
|
public string WinRmContainerListScript { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// PowerShell-Scriptblock für XApi-Import.
|
|
||||||
/// Platzhalter: {container}, {xapiPath}.
|
|
||||||
/// </summary>
|
|
||||||
public string WinRmXapiImportScript { get; init; } = string.Empty;
|
public string WinRmXapiImportScript { get; init; } = string.Empty;
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
// Gemeinsame Einstellungen
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
|
|
||||||
/// <summary>Timeout in Sekunden für einzelne API-/Script-Aufrufe (Standard: 60).</summary>
|
|
||||||
public int TimeoutSeconds { get; init; } = 60;
|
public int TimeoutSeconds { get; init; } = 60;
|
||||||
|
|
||||||
/// <summary>Wartezeit in Sekunden nach einem Container-Neustart (Standard: 15).</summary>
|
|
||||||
public int PostRestartDelaySeconds { get; init; } = 15;
|
public int PostRestartDelaySeconds { get; init; } = 15;
|
||||||
|
|
||||||
|
/// <summary>Effektives Restart-Script inkl. Default, wenn leer.</summary>
|
||||||
|
public string ResolveRestartScript()
|
||||||
|
=> string.IsNullOrWhiteSpace(WinRmRestartScript)
|
||||||
|
? DefaultRestartScript
|
||||||
|
: WinRmRestartScript;
|
||||||
|
|
||||||
|
/// <summary>Effektives List-Script inkl. Default, wenn leer.</summary>
|
||||||
|
public string ResolveContainerListScript()
|
||||||
|
=> string.IsNullOrWhiteSpace(WinRmContainerListScript)
|
||||||
|
? DefaultContainerListScript
|
||||||
|
: WinRmContainerListScript;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default: Sonic Management Console Tools per CMD auf dem Ziel-PC.
|
||||||
|
/// stopcontainer.bat / startcontainer.bat unter SonicHome\bin.
|
||||||
|
/// </summary>
|
||||||
|
public const string DefaultRestartScript =
|
||||||
|
"""
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$sonicHome = '{sonicHome}'
|
||||||
|
$domain = '{domain}'
|
||||||
|
$container = '{container}'
|
||||||
|
$bin = Join-Path $sonicHome 'bin'
|
||||||
|
$stop = Join-Path $bin 'stopcontainer.bat'
|
||||||
|
$start = Join-Path $bin 'startcontainer.bat'
|
||||||
|
$fullName = if ($container -like '*.*') { $container } else { "$domain.$container" }
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $stop)) {
|
||||||
|
throw "Sonic stopcontainer.bat nicht gefunden: $stop (SonicHome prüfen)"
|
||||||
|
}
|
||||||
|
if (-not (Test-Path -LiteralPath $start)) {
|
||||||
|
throw "Sonic startcontainer.bat nicht gefunden: $start (SonicHome prüfen)"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output "STOP $fullName via $stop"
|
||||||
|
$stopProc = Start-Process -FilePath 'cmd.exe' -ArgumentList @('/c', "`"$stop`" `"$fullName`"") -Wait -PassThru -NoNewWindow
|
||||||
|
if ($stopProc.ExitCode -ne 0) {
|
||||||
|
Write-Warning "stopcontainer ExitCode=$($stopProc.ExitCode) – starte trotzdem neu"
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Sleep -Seconds 5
|
||||||
|
|
||||||
|
Write-Output "START $fullName via $start"
|
||||||
|
$startProc = Start-Process -FilePath 'cmd.exe' -ArgumentList @('/c', "`"$start`" `"$fullName`"") -Wait -PassThru -NoNewWindow
|
||||||
|
if ($startProc.ExitCode -ne 0) {
|
||||||
|
throw "startcontainer fehlgeschlagen, ExitCode=$($startProc.ExitCode)"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output "Neustart ok: $fullName"
|
||||||
|
""";
|
||||||
|
|
||||||
|
public const string DefaultContainerListScript =
|
||||||
|
"""
|
||||||
|
$ErrorActionPreference = 'Continue'
|
||||||
|
$sonicHome = '{sonicHome}'
|
||||||
|
$names = @()
|
||||||
|
|
||||||
|
# Windows-Dienste mit Sonic/ESB im Namen
|
||||||
|
$names += @(Get-Service -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.DisplayName -match 'Sonic|ESB|MQ' -or $_.Name -match 'Sonic|ESB|MQ' } |
|
||||||
|
Select-Object -ExpandProperty Name)
|
||||||
|
|
||||||
|
# Container-Cache-Ordner unter SonicHome (Domain.Container.cache)
|
||||||
|
if (Test-Path -LiteralPath $sonicHome) {
|
||||||
|
$names += @(Get-ChildItem -LiteralPath $sonicHome -Directory -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.Name -like '*.cache' } |
|
||||||
|
ForEach-Object { $_.Name -replace '\.cache$', '' })
|
||||||
|
}
|
||||||
|
|
||||||
|
$names | Where-Object { $_ } | Sort-Object -Unique
|
||||||
|
""";
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SonicManagementMode
|
public enum SonicManagementMode
|
||||||
{
|
{
|
||||||
/// <summary>HTTP REST API (wenn vom Sonic-Server bereitgestellt).</summary>
|
/// <summary>HTTP REST API (wenn vom Sonic-Server bereitgestellt).</summary>
|
||||||
HttpApi,
|
HttpApi = 0,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// PowerShell Remoting (WinRM) – Standard für Sonic 10.x auf Windows.
|
/// PowerShell Remoting (WinRM) – führt Scripts auf dem Sonic-Server aus.
|
||||||
/// Führt konfigurierte Scriptblöcke via Invoke-Command auf dem Sonic-Server aus.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
WinRm
|
WinRm = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CMD/PowerShell lokal auf diesem PC – zum Testen direkt auf dem Sonic-Rechner.
|
||||||
|
/// </summary>
|
||||||
|
LocalCmd = 2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,89 @@ public sealed class DeploymentOrchestrator
|
|||||||
IReadOnlyList<DeploymentTarget> selectedTargets)
|
IReadOnlyList<DeploymentTarget> selectedTargets)
|
||||||
=> _preflightValidator.Validate(certificatePath, certificateInfo, selectedTargets);
|
=> _preflightValidator.Validate(certificatePath, certificateInfo, selectedTargets);
|
||||||
|
|
||||||
|
public PreflightValidationResult ValidateRestartOnly(IReadOnlyList<DeploymentTarget> selectedTargets)
|
||||||
|
=> _preflightValidator.ValidateRestartOnly(selectedTargets);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Nur ESB-/Container-Neustart – ohne Zertifikatskopieren und ohne TLS-Probe.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<DeploymentRunResult> RestartOnlyAsync(
|
||||||
|
IReadOnlyList<DeploymentTarget> selectedTargets,
|
||||||
|
IProgress<TargetProgressUpdate>? progress,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
DateTimeOffset startedAt = DateTimeOffset.Now;
|
||||||
|
List<TargetStepResult> results = [];
|
||||||
|
|
||||||
|
using RunLogger logger = new(_settings.LogDirectory);
|
||||||
|
logger.Write($"Neustart-only gestartet für {selectedTargets.Count} Ziel(e).");
|
||||||
|
|
||||||
|
foreach (DeploymentTarget target in selectedTargets)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
results.Add(await RestartTargetOnlyAsync(target, logger, progress, cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTimeOffset finishedAt = DateTimeOffset.Now;
|
||||||
|
DeploymentRunResult runResult = new()
|
||||||
|
{
|
||||||
|
TargetResults = results,
|
||||||
|
StartedAt = startedAt,
|
||||||
|
FinishedAt = finishedAt,
|
||||||
|
CertificateFilePath = string.Empty,
|
||||||
|
CertificateFingerprint = string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.Write(
|
||||||
|
$"Neustart-only beendet. Erfolg={runResult.OverallSuccess}; Dauer={(finishedAt - startedAt).TotalSeconds:F1}s; Log={logger.LogFilePath}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _sqlRunLogger.PersistRunResultAsync(runResult, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Write($"SQL-Protokollierung fehlgeschlagen (nicht kritisch): {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return runResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<TargetStepResult> RestartTargetOnlyAsync(
|
||||||
|
DeploymentTarget target,
|
||||||
|
RunLogger logger,
|
||||||
|
IProgress<TargetProgressUpdate>? progress,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
List<string> steps = [];
|
||||||
|
DateTimeOffset targetStart = DateTimeOffset.Now;
|
||||||
|
progress?.Report(new TargetProgressUpdate(target.Id, "Neustart…", false));
|
||||||
|
|
||||||
|
(bool restartOk, string restartStatus, string? restartDetail) =
|
||||||
|
await _restartExecutor.ExecuteAsync(target, cancellationToken);
|
||||||
|
|
||||||
|
RecordStep(steps, logger, target.Name, "Restart", restartStatus, restartDetail);
|
||||||
|
|
||||||
|
TargetStepResult targetResult = new()
|
||||||
|
{
|
||||||
|
TargetId = target.Id,
|
||||||
|
TargetName = target.Name,
|
||||||
|
Success = restartOk,
|
||||||
|
StatusText = restartStatus,
|
||||||
|
Detail = restartDetail,
|
||||||
|
Steps = steps,
|
||||||
|
StartedAt = targetStart,
|
||||||
|
FinishedAt = DateTimeOffset.Now,
|
||||||
|
CopySucceeded = true,
|
||||||
|
RestartSucceeded = restartOk,
|
||||||
|
TlsSucceeded = true,
|
||||||
|
ObservedFingerprint = null
|
||||||
|
};
|
||||||
|
|
||||||
|
progress?.Report(new TargetProgressUpdate(target.Id, restartStatus, restartOk));
|
||||||
|
return targetResult;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<DeploymentRunResult> RunAsync(
|
public async Task<DeploymentRunResult> RunAsync(
|
||||||
string certificatePath,
|
string certificatePath,
|
||||||
CertificateInfo certificateInfo,
|
CertificateInfo certificateInfo,
|
||||||
|
|||||||
@@ -32,13 +32,45 @@ public sealed class PreflightValidator
|
|||||||
|
|
||||||
foreach (DeploymentTarget target in selectedTargets)
|
foreach (DeploymentTarget target in selectedTargets)
|
||||||
{
|
{
|
||||||
ValidateTarget(result, target);
|
ValidateTarget(result, target, requireDeployPaths: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ValidateTarget(PreflightValidationResult result, DeploymentTarget target)
|
/// <summary>
|
||||||
|
/// Vorabprüfung nur für ESB-Neustart (ohne Zertifikat / Kopierpfade).
|
||||||
|
/// </summary>
|
||||||
|
public PreflightValidationResult ValidateRestartOnly(IReadOnlyList<DeploymentTarget> selectedTargets)
|
||||||
|
{
|
||||||
|
PreflightValidationResult result = new();
|
||||||
|
|
||||||
|
if (selectedTargets.Count == 0)
|
||||||
|
{
|
||||||
|
AddIssue(result, "Bitte mindestens ein Ziel anhaken.");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (DeploymentTarget target in selectedTargets)
|
||||||
|
{
|
||||||
|
if (target.RestartType is RestartType.None)
|
||||||
|
{
|
||||||
|
AddIssue(result, $"Ziel '{target.Name}': RestartType=None – kein Neustart konfiguriert.", target.Id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidateTarget(result, target, requireDeployPaths: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateTarget(
|
||||||
|
PreflightValidationResult result,
|
||||||
|
DeploymentTarget target,
|
||||||
|
bool requireDeployPaths)
|
||||||
|
{
|
||||||
|
if (requireDeployPaths)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(target.TargetDirectory))
|
if (string.IsNullOrWhiteSpace(target.TargetDirectory))
|
||||||
{
|
{
|
||||||
@@ -49,6 +81,7 @@ public sealed class PreflightValidator
|
|||||||
{
|
{
|
||||||
AddIssue(result, $"Ziel '{target.Name}': CertificateFileName fehlt.", target.Id);
|
AddIssue(result, $"Ziel '{target.Name}': CertificateFileName fehlt.", target.Id);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (target.RestartType == RestartType.Command
|
if (target.RestartType == RestartType.Command
|
||||||
&& string.IsNullOrWhiteSpace(target.RestartCommand))
|
&& string.IsNullOrWhiteSpace(target.RestartCommand))
|
||||||
@@ -75,7 +108,7 @@ public sealed class PreflightValidator
|
|||||||
AddIssue(result, $"Ziel '{target.Name}': XapiSourcePath fehlt bei RestartType=SonicContainerWithXapi.", target.Id);
|
AddIssue(result, $"Ziel '{target.Name}': XapiSourcePath fehlt bei RestartType=SonicContainerWithXapi.", target.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(target.TargetDirectory))
|
if (!requireDeployPaths || string.IsNullOrWhiteSpace(target.TargetDirectory))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,19 +9,20 @@ namespace ZA.CoreService.ESBCertificateManager.Services;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Verwaltet Sonic ESB Container über die Management Console.
|
/// Verwaltet Sonic ESB Container über die Management Console.
|
||||||
///
|
///
|
||||||
/// Modus = WinRm (Standard, Sonic 10.x):
|
/// Modus = WinRm (Standard):
|
||||||
/// PowerShell Remoting (Invoke-Command) auf dem Sonic-Server.
|
/// PowerShell Remoting (Invoke-Command) → CMD stop/startcontainer auf dem Sonic-Server.
|
||||||
/// Erfordert WinRM auf dem Zielrechner: Enable-PSRemoting -Force
|
///
|
||||||
|
/// Modus = LocalCmd:
|
||||||
|
/// Dieselbe CMD/PowerShell-Logik lokal (Test direkt auf dem Sonic-PC).
|
||||||
///
|
///
|
||||||
/// Modus = HttpApi:
|
/// Modus = HttpApi:
|
||||||
/// HTTP REST API mit automatischer Pfad-Erkennung.
|
/// HTTP REST API mit automatischer Pfad-Erkennung.
|
||||||
/// Probiert: /mf/rest/v1, /api/v1, /sonic/management, /containers
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class SonicManagementClient : IDisposable
|
public sealed class SonicManagementClient : IDisposable
|
||||||
{
|
{
|
||||||
private readonly SonicConnection _connection;
|
private readonly SonicConnection _connection;
|
||||||
private readonly HttpClient? _http;
|
private readonly HttpClient? _http;
|
||||||
private readonly WinRmExecutor? _winRm;
|
private readonly WinRmExecutor? _scriptRunner;
|
||||||
|
|
||||||
// Gecachter HTTP-Basis-Pfad (nur im HttpApi-Modus)
|
// Gecachter HTTP-Basis-Pfad (nur im HttpApi-Modus)
|
||||||
private string? _resolvedContainerBasePath;
|
private string? _resolvedContainerBasePath;
|
||||||
@@ -34,13 +35,16 @@ public sealed class SonicManagementClient : IDisposable
|
|||||||
"/containers"
|
"/containers"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private bool UsesScripts =>
|
||||||
|
_connection.ManagementMode is SonicManagementMode.WinRm or SonicManagementMode.LocalCmd;
|
||||||
|
|
||||||
public SonicManagementClient(SonicConnection connection)
|
public SonicManagementClient(SonicConnection connection)
|
||||||
{
|
{
|
||||||
_connection = connection;
|
_connection = connection;
|
||||||
|
|
||||||
if (connection.ManagementMode == SonicManagementMode.WinRm)
|
if (UsesScripts)
|
||||||
{
|
{
|
||||||
_winRm = new WinRmExecutor(connection);
|
_scriptRunner = new WinRmExecutor(connection);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -77,10 +81,13 @@ public sealed class SonicManagementClient : IDisposable
|
|||||||
public async Task<(bool Success, string? Error, string? ResolvedPath)> CheckConnectionAsync(
|
public async Task<(bool Success, string? Error, string? ResolvedPath)> CheckConnectionAsync(
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (_connection.ManagementMode == SonicManagementMode.WinRm)
|
if (UsesScripts)
|
||||||
{
|
{
|
||||||
(bool ok, string? error) = await _winRm!.TestConnectionAsync(cancellationToken);
|
(bool ok, string? error) = await _scriptRunner!.TestConnectionAsync(cancellationToken);
|
||||||
return (ok, error, ok ? $"WinRM auf {ExtractHost(_connection.ConnectionUrl)}:{_connection.WinRmPort}" : null);
|
string modeLabel = _connection.ManagementMode == SonicManagementMode.LocalCmd
|
||||||
|
? $"LocalCmd (SonicHome={_connection.SonicHome})"
|
||||||
|
: $"WinRM auf {ExtractHost(_connection.ConnectionUrl)}:{_connection.WinRmPort}";
|
||||||
|
return (ok, error, ok ? modeLabel : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -116,9 +123,9 @@ public sealed class SonicManagementClient : IDisposable
|
|||||||
public async Task<(bool Success, IReadOnlyList<string> ContainerNames, string? Error)> GetContainersAsync(
|
public async Task<(bool Success, IReadOnlyList<string> ContainerNames, string? Error)> GetContainersAsync(
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (_connection.ManagementMode == SonicManagementMode.WinRm)
|
if (UsesScripts)
|
||||||
{
|
{
|
||||||
return await GetContainersViaWinRmAsync(cancellationToken);
|
return await GetContainersViaScriptAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await GetContainersViaHttpAsync(cancellationToken);
|
return await GetContainersViaHttpAsync(cancellationToken);
|
||||||
@@ -131,9 +138,9 @@ public sealed class SonicManagementClient : IDisposable
|
|||||||
string containerName,
|
string containerName,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (_connection.ManagementMode == SonicManagementMode.WinRm)
|
if (UsesScripts)
|
||||||
{
|
{
|
||||||
return await RestartViaWinRmAsync(containerName, cancellationToken);
|
return await RestartViaScriptAsync(containerName, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await RestartViaHttpAsync(containerName, cancellationToken);
|
return await RestartViaHttpAsync(containerName, cancellationToken);
|
||||||
@@ -147,83 +154,72 @@ public sealed class SonicManagementClient : IDisposable
|
|||||||
string xapiSourcePath,
|
string xapiSourcePath,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (_connection.ManagementMode == SonicManagementMode.WinRm)
|
if (UsesScripts)
|
||||||
{
|
{
|
||||||
return await ImportXapiViaWinRmAsync(containerName, xapiSourcePath, cancellationToken);
|
return await ImportXapiViaScriptAsync(containerName, xapiSourcePath, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await ImportXapiViaHttpAsync(containerName, xapiSourcePath, cancellationToken);
|
return await ImportXapiViaHttpAsync(containerName, xapiSourcePath, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
// WinRM-Implementierungen
|
// Script-Implementierungen (WinRM / LocalCmd)
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
private async Task<(bool, IReadOnlyList<string>, string?)> GetContainersViaWinRmAsync(
|
private async Task<(bool, IReadOnlyList<string>, string?)> GetContainersViaScriptAsync(
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(_connection.WinRmContainerListScript))
|
|
||||||
{
|
|
||||||
return (false, [],
|
|
||||||
"WinRmContainerListScript ist nicht konfiguriert.\n" +
|
|
||||||
"Beispiel: \"Get-Service -DisplayName 'Sonic*' | Select-Object -ExpandProperty DisplayName\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
string script = WinRmExecutor.ApplyScriptTemplate(
|
string script = WinRmExecutor.ApplyScriptTemplate(
|
||||||
_connection.WinRmContainerListScript,
|
_connection.ResolveContainerListScript(),
|
||||||
containerName: string.Empty,
|
containerName: string.Empty,
|
||||||
domainName: _connection.DomainName);
|
domainName: _connection.DomainName,
|
||||||
|
sonicHome: _connection.SonicHome);
|
||||||
|
|
||||||
(bool ok, string? output, string? error) =
|
(bool ok, string? output, string? error) =
|
||||||
await _winRm!.RunScriptAsync(script, cancellationToken);
|
await _scriptRunner!.RunScriptAsync(script, cancellationToken);
|
||||||
|
|
||||||
if (!ok)
|
if (!ok)
|
||||||
{
|
{
|
||||||
return (false, [], $"WinRM Container-Liste fehlgeschlagen: {error}");
|
return (false, [], $"Container-Liste fehlgeschlagen ({_connection.ManagementMode}): {error}");
|
||||||
}
|
}
|
||||||
|
|
||||||
List<string> names = (output ?? string.Empty)
|
List<string> names = (output ?? string.Empty)
|
||||||
.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||||
.Where(l => l.Length > 0)
|
.Where(l => l.Length > 0 && !l.StartsWith("WARNING", StringComparison.OrdinalIgnoreCase))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return (true, names, null);
|
return (true, names, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(bool, string, string?)> RestartViaWinRmAsync(
|
private async Task<(bool, string, string?)> RestartViaScriptAsync(
|
||||||
string containerName, CancellationToken cancellationToken)
|
string containerName, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(_connection.WinRmRestartScript))
|
|
||||||
{
|
|
||||||
return (false, "Neustart fehlgeschlagen",
|
|
||||||
"WinRmRestartScript ist nicht konfiguriert.\n" +
|
|
||||||
"Beispiel für Windows-Service: \"Restart-Service -Name 'CT-ZADBService' -Force\"\n" +
|
|
||||||
"Platzhalter {container} wird durch den Container-Namen ersetzt.");
|
|
||||||
}
|
|
||||||
|
|
||||||
string script = WinRmExecutor.ApplyScriptTemplate(
|
string script = WinRmExecutor.ApplyScriptTemplate(
|
||||||
_connection.WinRmRestartScript,
|
_connection.ResolveRestartScript(),
|
||||||
containerName,
|
containerName,
|
||||||
_connection.DomainName);
|
_connection.DomainName,
|
||||||
|
_connection.SonicHome);
|
||||||
|
|
||||||
(bool ok, string? output, string? error) =
|
(bool ok, string? output, string? error) =
|
||||||
await _winRm!.RunScriptAsync(script, cancellationToken);
|
await _scriptRunner!.RunScriptAsync(script, cancellationToken);
|
||||||
|
|
||||||
|
string mode = _connection.ManagementMode.ToString();
|
||||||
|
|
||||||
if (!ok)
|
if (!ok)
|
||||||
{
|
{
|
||||||
return (false, "Neustart fehlgeschlagen (WinRM)",
|
return (false, $"Neustart fehlgeschlagen ({mode})",
|
||||||
$"Fehler: {error}\nAusgabe: {output}");
|
$"Fehler: {error}\nAusgabe: {output}\nSonicHome={_connection.SonicHome}");
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.Delay(
|
await Task.Delay(
|
||||||
TimeSpan.FromSeconds(Math.Clamp(_connection.PostRestartDelaySeconds, 0, 120)),
|
TimeSpan.FromSeconds(Math.Clamp(_connection.PostRestartDelaySeconds, 0, 120)),
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
return (true, "Container neugestartet (WinRM)",
|
return (true, $"ESB-Container neugestartet ({mode})",
|
||||||
$"Ausgabe: {output ?? "(keine)"}");
|
$"Ausgabe: {output ?? "(keine)"}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(bool, string, string?)> ImportXapiViaWinRmAsync(
|
private async Task<(bool, string, string?)> ImportXapiViaScriptAsync(
|
||||||
string containerName, string xapiSourcePath, CancellationToken cancellationToken)
|
string containerName, string xapiSourcePath, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(_connection.WinRmXapiImportScript))
|
if (string.IsNullOrWhiteSpace(_connection.WinRmXapiImportScript))
|
||||||
@@ -236,21 +232,22 @@ public sealed class SonicManagementClient : IDisposable
|
|||||||
_connection.WinRmXapiImportScript,
|
_connection.WinRmXapiImportScript,
|
||||||
containerName,
|
containerName,
|
||||||
_connection.DomainName,
|
_connection.DomainName,
|
||||||
|
_connection.SonicHome,
|
||||||
xapiSourcePath);
|
xapiSourcePath);
|
||||||
|
|
||||||
(bool importOk, string? importOut, string? importErr) =
|
(bool importOk, string? importOut, string? importErr) =
|
||||||
await _winRm!.RunScriptAsync(script, cancellationToken);
|
await _scriptRunner!.RunScriptAsync(script, cancellationToken);
|
||||||
|
|
||||||
if (!importOk)
|
if (!importOk)
|
||||||
{
|
{
|
||||||
return (false, "XApi-Import fehlgeschlagen (WinRM)", importErr ?? importOut);
|
return (false, "XApi-Import fehlgeschlagen", importErr ?? importOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
(bool restartOk, string restartStatus, string? restartDetail) =
|
(bool restartOk, string restartStatus, string? restartDetail) =
|
||||||
await RestartViaWinRmAsync(containerName, cancellationToken);
|
await RestartViaScriptAsync(containerName, cancellationToken);
|
||||||
|
|
||||||
return restartOk
|
return restartOk
|
||||||
? (true, "XApi importiert + Container neugestartet (WinRM)",
|
? (true, "XApi importiert + Container neugestartet",
|
||||||
$"Import: {importOut} | Restart: {restartDetail}")
|
$"Import: {importOut} | Restart: {restartDetail}")
|
||||||
: (false, restartStatus, restartDetail);
|
: (false, restartStatus, restartDetail);
|
||||||
}
|
}
|
||||||
@@ -473,8 +470,14 @@ public sealed class SonicManagementClient : IDisposable
|
|||||||
? root.EnumerateArray()
|
? root.EnumerateArray()
|
||||||
: root.ValueKind == JsonValueKind.Object
|
: root.ValueKind == JsonValueKind.Object
|
||||||
? new[] { "containers", "data", "items", "result" }
|
? new[] { "containers", "data", "items", "result" }
|
||||||
.Where(root.TryGetProperty)
|
.Where(k => root.TryGetProperty(k, out _))
|
||||||
.SelectMany(k => { root.TryGetProperty(k, out JsonElement a); return a.EnumerateArray(); })
|
.SelectMany(k =>
|
||||||
|
{
|
||||||
|
root.TryGetProperty(k, out JsonElement a);
|
||||||
|
return a.ValueKind == JsonValueKind.Array
|
||||||
|
? a.EnumerateArray()
|
||||||
|
: Enumerable.Empty<JsonElement>();
|
||||||
|
})
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
foreach (JsonElement el in elements)
|
foreach (JsonElement el in elements)
|
||||||
|
|||||||
@@ -5,14 +5,7 @@ using ZA.CoreService.ESBCertificateManager.Models;
|
|||||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Führt PowerShell-Befehle via WinRM (Invoke-Command) auf dem Sonic-Server aus.
|
/// Führt PowerShell-Befehle lokal oder via WinRM (Invoke-Command) auf dem Sonic-Server aus.
|
||||||
///
|
|
||||||
/// Voraussetzungen auf dem Zielrechner:
|
|
||||||
/// - WinRM muss aktiviert sein: Enable-PSRemoting -Force
|
|
||||||
/// - Ausführungsrichtlinie: Set-ExecutionPolicy RemoteSigned
|
|
||||||
///
|
|
||||||
/// Voraussetzungen auf dem App-Rechner (einmalig, als Admin):
|
|
||||||
/// - Set-Item WSMan:\localhost\Client\TrustedHosts -Value "dekun-painwbdet"
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class WinRmExecutor
|
public sealed class WinRmExecutor
|
||||||
{
|
{
|
||||||
@@ -23,15 +16,20 @@ public sealed class WinRmExecutor
|
|||||||
_connection = connection;
|
_connection = connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prüft die WinRM-Konnektivität und ob der Sonic-Server per TCP erreichbar ist.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<(bool Success, string? Error)> TestConnectionAsync(
|
public async Task<(bool Success, string? Error)> TestConnectionAsync(
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
|
if (_connection.ManagementMode == SonicManagementMode.LocalCmd)
|
||||||
|
{
|
||||||
|
(bool ok, string? output, string? error) = await RunScriptAsync(
|
||||||
|
"$env:COMPUTERNAME", cancellationToken);
|
||||||
|
return ok
|
||||||
|
? (true, null)
|
||||||
|
: (false, $"LocalCmd fehlgeschlagen: {error ?? output}");
|
||||||
|
}
|
||||||
|
|
||||||
string host = ExtractHost(_connection.ConnectionUrl);
|
string host = ExtractHost(_connection.ConnectionUrl);
|
||||||
|
|
||||||
// TCP-Ping auf WinRM-Port
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using System.Net.Sockets.TcpClient tcp = new();
|
using System.Net.Sockets.TcpClient tcp = new();
|
||||||
@@ -44,30 +42,25 @@ public sealed class WinRmExecutor
|
|||||||
{
|
{
|
||||||
return (false,
|
return (false,
|
||||||
$"WinRM-Port {_connection.WinRmPort} auf '{host}' nicht erreichbar: {ex.Message}\n" +
|
$"WinRM-Port {_connection.WinRmPort} auf '{host}' nicht erreichbar: {ex.Message}\n" +
|
||||||
$"Auf dem Zielrechner ausführen: Enable-PSRemoting -Force");
|
"Auf dem Zielrechner: Enable-PSRemoting -Force\n" +
|
||||||
|
"Oder ManagementMode=LocalCmd setzen und die App auf dem Sonic-PC starten.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kurztest: Hostname zurückgeben
|
(bool sessionOk, _, string? sessionError) = await RunScriptAsync(
|
||||||
(bool ok, string? output, string? error) = await RunScriptAsync(
|
|
||||||
"$env:COMPUTERNAME", cancellationToken);
|
"$env:COMPUTERNAME", cancellationToken);
|
||||||
|
|
||||||
return ok
|
return sessionOk
|
||||||
? (true, null)
|
? (true, null)
|
||||||
: (false, $"WinRM-Verbindung fehlgeschlagen: {error}");
|
: (false, $"WinRM-Verbindung fehlgeschlagen: {sessionError}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Führt einen Scriptblock auf dem Remote-Rechner aus und gibt Stdout zurück.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<(bool Success, string? Output, string? Error)> RunScriptAsync(
|
public async Task<(bool Success, string? Output, string? Error)> RunScriptAsync(
|
||||||
string scriptBlock,
|
string scriptBlock,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
string host = ExtractHost(_connection.ConnectionUrl);
|
string fullScript = _connection.ManagementMode == SonicManagementMode.LocalCmd
|
||||||
|
? BuildLocalScript(scriptBlock)
|
||||||
// Passwort als SecureString – bleibt im PowerShell-Prozess, wird nicht als Argument übergeben
|
: BuildRemoteScript(ExtractHost(_connection.ConnectionUrl), scriptBlock);
|
||||||
// Stattdessen: Scriptblock über stdin senden
|
|
||||||
string fullScript = BuildScript(host, scriptBlock);
|
|
||||||
|
|
||||||
ProcessStartInfo psi = new()
|
ProcessStartInfo psi = new()
|
||||||
{
|
{
|
||||||
@@ -88,8 +81,7 @@ public sealed class WinRmExecutor
|
|||||||
return (false, null, "PowerShell-Prozess konnte nicht gestartet werden.");
|
return (false, null, "PowerShell-Prozess konnte nicht gestartet werden.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skript über stdin – Credentials gehen NICHT als sichtbares Argument durch
|
await process.StandardInput.WriteAsync(fullScript.AsMemory(), cancellationToken);
|
||||||
await process.StandardInput.WriteAsync(fullScript);
|
|
||||||
process.StandardInput.Close();
|
process.StandardInput.Close();
|
||||||
|
|
||||||
using CancellationTokenSource timeoutCts =
|
using CancellationTokenSource timeoutCts =
|
||||||
@@ -106,7 +98,7 @@ public sealed class WinRmExecutor
|
|||||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
try { process.Kill(entireProcessTree: true); } catch { /* ignore */ }
|
try { process.Kill(entireProcessTree: true); } catch { /* ignore */ }
|
||||||
return (false, null, $"WinRM-Ausführung Timeout nach {_connection.TimeoutSeconds}s.");
|
return (false, null, $"Ausführung Timeout nach {_connection.TimeoutSeconds}s.");
|
||||||
}
|
}
|
||||||
|
|
||||||
string stdout = (await stdoutTask).Trim();
|
string stdout = (await stdoutTask).Trim();
|
||||||
@@ -121,9 +113,11 @@ public sealed class WinRmExecutor
|
|||||||
stderr.Length > 0 ? Truncate(stderr) : $"PowerShell ExitCode={process.ExitCode}");
|
stderr.Length > 0 ? Truncate(stderr) : $"PowerShell ExitCode={process.ExitCode}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private string BuildScript(string host, string scriptBlock)
|
private static string BuildLocalScript(string scriptBlock)
|
||||||
|
=> "$ErrorActionPreference = 'Stop'\n" + scriptBlock;
|
||||||
|
|
||||||
|
private string BuildRemoteScript(string host, string scriptBlock)
|
||||||
{
|
{
|
||||||
// Passwort über Variable, nicht als Argument – verhindert Sichtbarkeit in Prozessliste
|
|
||||||
string escapedPwd = _connection.Password.Replace("'", "''");
|
string escapedPwd = _connection.Password.Replace("'", "''");
|
||||||
string escapedUser = _connection.Username.Replace("'", "''");
|
string escapedUser = _connection.Username.Replace("'", "''");
|
||||||
string escapedHost = host.Replace("'", "''");
|
string escapedHost = host.Replace("'", "''");
|
||||||
@@ -137,21 +131,17 @@ public sealed class WinRmExecutor
|
|||||||
"} -ErrorAction Stop";
|
"} -ErrorAction Stop";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public static string ApplyScriptTemplate(
|
||||||
/// Ersetzt Platzhalter in einem konfigurierten WinRM-Script.
|
string template,
|
||||||
/// {container} → Container-Name (einfache Hochkommas werden verdoppelt)
|
string containerName,
|
||||||
/// {domain} → Domain-Name
|
string domainName = "",
|
||||||
/// {xapiPath} → Pfad zur XApi-Quelldatei
|
string sonicHome = "",
|
||||||
/// </summary>
|
string xapiPath = "")
|
||||||
public static string ApplyScriptTemplate(string template, string containerName,
|
|
||||||
string domainName = "", string xapiPath = "")
|
|
||||||
=> template
|
=> template
|
||||||
.Replace("{container}", containerName.Replace("'", "''"),
|
.Replace("{container}", containerName.Replace("'", "''"), StringComparison.OrdinalIgnoreCase)
|
||||||
StringComparison.OrdinalIgnoreCase)
|
.Replace("{domain}", domainName.Replace("'", "''"), StringComparison.OrdinalIgnoreCase)
|
||||||
.Replace("{domain}", domainName.Replace("'", "''"),
|
.Replace("{sonicHome}", sonicHome.Replace("'", "''"), StringComparison.OrdinalIgnoreCase)
|
||||||
StringComparison.OrdinalIgnoreCase)
|
.Replace("{xapiPath}", xapiPath.Replace("'", "''"), StringComparison.OrdinalIgnoreCase);
|
||||||
.Replace("{xapiPath}", xapiPath.Replace("'", "''"),
|
|
||||||
StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
private static string ExtractHost(string connectionUrl)
|
private static string ExtractHost(string connectionUrl)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,17 +10,22 @@
|
|||||||
"Name": "DE-Test",
|
"Name": "DE-Test",
|
||||||
"DomainName": "proalpha-test",
|
"DomainName": "proalpha-test",
|
||||||
"ConnectionUrl": "tcp://dekun-painwbdet:13070",
|
"ConnectionUrl": "tcp://dekun-painwbdet:13070",
|
||||||
"ManagementHttpPort": 8080,
|
|
||||||
"ApiBasePath": "/api/v1",
|
|
||||||
"Username": "Administrator",
|
"Username": "Administrator",
|
||||||
"Password": "Administrator",
|
"Password": "Administrator",
|
||||||
"TimeoutSeconds": 30,
|
"ManagementMode": "WinRm",
|
||||||
|
"SonicHome": "C:\\Sonic\\MQ10.0",
|
||||||
|
"WinRmPort": 5985,
|
||||||
|
"TimeoutSeconds": 90,
|
||||||
"PostRestartDelaySeconds": 15,
|
"PostRestartDelaySeconds": 15,
|
||||||
|
"ManagementHttpPort": 8080,
|
||||||
|
"ApiBasePath": "/api/v1",
|
||||||
"ContainerListPath": "",
|
"ContainerListPath": "",
|
||||||
"ContainerRestartPath": "",
|
"ContainerRestartPath": "",
|
||||||
"ContainerStopPath": "",
|
"ContainerStopPath": "",
|
||||||
"ContainerStartPath": ""
|
"ContainerStartPath": "",
|
||||||
|
"WinRmRestartScript": "",
|
||||||
|
"WinRmContainerListScript": "",
|
||||||
|
"WinRmXapiImportScript": ""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user