155 lines
6.0 KiB
C#
155 lines
6.0 KiB
C#
using System.Diagnostics;
|
|
using System.Text;
|
|
using ZA.CoreService.ESBCertificateManager.Models;
|
|
|
|
namespace ZA.CoreService.ESBCertificateManager.Services;
|
|
|
|
/// <summary>
|
|
/// Führt PowerShell-Befehle lokal oder via WinRM (Invoke-Command) auf dem Sonic-Server aus.
|
|
/// </summary>
|
|
public sealed class WinRmExecutor
|
|
{
|
|
private readonly SonicConnection _connection;
|
|
|
|
public WinRmExecutor(SonicConnection connection)
|
|
{
|
|
_connection = connection;
|
|
}
|
|
|
|
public async Task<(bool Success, string? Error)> TestConnectionAsync(
|
|
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);
|
|
|
|
try
|
|
{
|
|
using System.Net.Sockets.TcpClient tcp = new();
|
|
using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
cts.CancelAfter(TimeSpan.FromSeconds(Math.Clamp(_connection.TimeoutSeconds, 5, 30)));
|
|
|
|
await tcp.ConnectAsync(host, _connection.WinRmPort, cts.Token);
|
|
}
|
|
catch (Exception ex) when (ex is not OperationCanceledException)
|
|
{
|
|
return (false,
|
|
$"WinRM-Port {_connection.WinRmPort} auf '{host}' nicht erreichbar: {ex.Message}\n" +
|
|
"Auf dem Zielrechner: Enable-PSRemoting -Force\n" +
|
|
"Oder ManagementMode=LocalCmd setzen und die App auf dem Sonic-PC starten.");
|
|
}
|
|
|
|
(bool sessionOk, _, string? sessionError) = await RunScriptAsync(
|
|
"$env:COMPUTERNAME", cancellationToken);
|
|
|
|
return sessionOk
|
|
? (true, null)
|
|
: (false, $"WinRM-Verbindung fehlgeschlagen: {sessionError}");
|
|
}
|
|
|
|
public async Task<(bool Success, string? Output, string? Error)> RunScriptAsync(
|
|
string scriptBlock,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
string fullScript = _connection.ManagementMode == SonicManagementMode.LocalCmd
|
|
? BuildLocalScript(scriptBlock)
|
|
: BuildRemoteScript(ExtractHost(_connection.ConnectionUrl), scriptBlock);
|
|
|
|
ProcessStartInfo psi = new()
|
|
{
|
|
FileName = "powershell.exe",
|
|
Arguments = "-NonInteractive -NoProfile -ExecutionPolicy Bypass -Command -",
|
|
UseShellExecute = false,
|
|
RedirectStandardInput = true,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
CreateNoWindow = true,
|
|
StandardInputEncoding = Encoding.UTF8
|
|
};
|
|
|
|
using Process process = new() { StartInfo = psi };
|
|
|
|
if (!process.Start())
|
|
{
|
|
return (false, null, "PowerShell-Prozess konnte nicht gestartet werden.");
|
|
}
|
|
|
|
await process.StandardInput.WriteAsync(fullScript.AsMemory(), cancellationToken);
|
|
process.StandardInput.Close();
|
|
|
|
using CancellationTokenSource timeoutCts =
|
|
CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
timeoutCts.CancelAfter(TimeSpan.FromSeconds(Math.Clamp(_connection.TimeoutSeconds, 10, 600)));
|
|
|
|
Task<string> stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
|
|
Task<string> 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, $"Ausführung Timeout nach {_connection.TimeoutSeconds}s.");
|
|
}
|
|
|
|
string stdout = (await stdoutTask).Trim();
|
|
string stderr = (await stderrTask).Trim();
|
|
|
|
if (process.ExitCode == 0)
|
|
{
|
|
return (true, stdout, stderr.Length > 0 ? stderr : null);
|
|
}
|
|
|
|
return (false, stdout.Length > 0 ? stdout : null,
|
|
stderr.Length > 0 ? Truncate(stderr) : $"PowerShell ExitCode={process.ExitCode}");
|
|
}
|
|
|
|
private static string BuildLocalScript(string scriptBlock)
|
|
=> "$ErrorActionPreference = 'Stop'\n" + scriptBlock;
|
|
|
|
private string BuildRemoteScript(string host, string scriptBlock)
|
|
{
|
|
string escapedPwd = _connection.Password.Replace("'", "''");
|
|
string escapedUser = _connection.Username.Replace("'", "''");
|
|
string escapedHost = host.Replace("'", "''");
|
|
|
|
return
|
|
"$ErrorActionPreference = 'Stop'\n" +
|
|
$"$secPwd = ConvertTo-SecureString '{escapedPwd}' -AsPlainText -Force\n" +
|
|
$"$cred = New-Object System.Management.Automation.PSCredential('{escapedUser}', $secPwd)\n" +
|
|
$"Invoke-Command -ComputerName '{escapedHost}' -Port {_connection.WinRmPort} -Credential $cred -ScriptBlock {{\n" +
|
|
$" {scriptBlock}\n" +
|
|
"} -ErrorAction Stop";
|
|
}
|
|
|
|
public static string ApplyScriptTemplate(
|
|
string template,
|
|
string containerName,
|
|
string domainName = "",
|
|
string sonicHome = "",
|
|
string xapiPath = "")
|
|
=> template
|
|
.Replace("{container}", containerName.Replace("'", "''"), StringComparison.OrdinalIgnoreCase)
|
|
.Replace("{domain}", domainName.Replace("'", "''"), StringComparison.OrdinalIgnoreCase)
|
|
.Replace("{sonicHome}", sonicHome.Replace("'", "''"), StringComparison.OrdinalIgnoreCase)
|
|
.Replace("{xapiPath}", xapiPath.Replace("'", "''"), StringComparison.OrdinalIgnoreCase);
|
|
|
|
private static string ExtractHost(string connectionUrl)
|
|
{
|
|
try { return new Uri(connectionUrl).Host; }
|
|
catch { return connectionUrl; }
|
|
}
|
|
|
|
private static string Truncate(string s, int max = 600)
|
|
=> s.Length <= max ? s : s[..max] + "…";
|
|
}
|