From d1b95bcb9d69eff34d526c9a76824a3c21632d8f Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 24 Jul 2026 11:09:46 +0200 Subject: [PATCH] Keep original UI; drop unused deploy/XApi/WinRM/TLS/SQL paths. Form1 design unchanged; only MfApi restart and certificate recognition remain active. Co-authored-by: Cursor --- .../Data/SqlRunLogger.cs | 167 ---- .../Data/SqlTargetRepository.cs | 111 --- .../Data/TargetRepositoryFactory.cs | 32 +- .../Data/targets.sample.json | 24 +- ZA.CoreService.ESBCertificateManager/Form1.cs | 99 +-- .../Models/SonicConnection.cs | 12 +- .../Services/CertificateDeployer.cs | 98 --- .../Services/DeploymentOrchestrator.cs | 184 +---- .../Services/PreflightValidator.cs | 123 +-- .../Services/RestartExecutor.cs | 121 +-- .../Services/SonicBinRestartExecutor.cs | 245 ------ .../Services/SonicManagementClient.cs | 722 +----------------- .../Services/TlsCertificateProbe.cs | 135 ---- .../Services/WinRmExecutor.cs | 268 ------- .../Sql/001_CreateSchema.sql | 157 ---- .../Sql/002_DeploymentRunModel.sql | 193 ----- ...A.CoreService.ESBCertificateManager.csproj | 7 - 17 files changed, 70 insertions(+), 2628 deletions(-) delete mode 100644 ZA.CoreService.ESBCertificateManager/Data/SqlRunLogger.cs delete mode 100644 ZA.CoreService.ESBCertificateManager/Data/SqlTargetRepository.cs delete mode 100644 ZA.CoreService.ESBCertificateManager/Services/CertificateDeployer.cs delete mode 100644 ZA.CoreService.ESBCertificateManager/Services/SonicBinRestartExecutor.cs delete mode 100644 ZA.CoreService.ESBCertificateManager/Services/TlsCertificateProbe.cs delete mode 100644 ZA.CoreService.ESBCertificateManager/Services/WinRmExecutor.cs delete mode 100644 ZA.CoreService.ESBCertificateManager/Sql/001_CreateSchema.sql delete mode 100644 ZA.CoreService.ESBCertificateManager/Sql/002_DeploymentRunModel.sql diff --git a/ZA.CoreService.ESBCertificateManager/Data/SqlRunLogger.cs b/ZA.CoreService.ESBCertificateManager/Data/SqlRunLogger.cs deleted file mode 100644 index f82d6d3..0000000 --- a/ZA.CoreService.ESBCertificateManager/Data/SqlRunLogger.cs +++ /dev/null @@ -1,167 +0,0 @@ -using Microsoft.Data.SqlClient; -using ZA.CoreService.ESBCertificateManager.Models; - -namespace ZA.CoreService.ESBCertificateManager.Data; - -/// -/// Schreibt Deployment-Lauf-Ergebnisse in die SQL-Datenbank. -/// Tabellen: dbo.DeploymentRuns (GUID-PK) + dbo.DeploymentTargetResults. -/// Ist kein ConnectionString konfiguriert, werden alle Operationen still übersprungen. -/// -public sealed class SqlRunLogger -{ - private readonly string? _connectionString; - - public SqlRunLogger(string? connectionString) - { - _connectionString = string.IsNullOrWhiteSpace(connectionString) - ? null - : connectionString; - } - - public bool IsEnabled => _connectionString is not null; - - /// - /// Legt einen neuen Lauf-Datensatz an. - /// - public async Task BeginRunAsync( - Guid runId, - DateTimeOffset startedAt, - string certificateFilePath, - string certificateFingerprint, - CancellationToken cancellationToken = default) - { - if (_connectionString is null) return; - - const string sql = """ - INSERT INTO dbo.DeploymentRuns - (Id, StartedAtUtc, SourceFile, SourceFingerprint, StartedBy, MachineName, OverallStatus) - VALUES - (@Id, @StartedAtUtc, @SourceFile, @SourceFingerprint, @StartedBy, @MachineName, N'Running'); - """; - - await using SqlConnection conn = new(_connectionString); - await conn.OpenAsync(cancellationToken); - await using SqlCommand cmd = new(sql, conn); - - cmd.Parameters.AddWithValue("@Id", runId); - cmd.Parameters.AddWithValue("@StartedAtUtc", startedAt.UtcDateTime); - cmd.Parameters.AddWithValue("@SourceFile", Path.GetFileName(certificateFilePath)); - cmd.Parameters.AddWithValue("@SourceFingerprint", certificateFingerprint); - cmd.Parameters.AddWithValue("@StartedBy", Environment.UserName); - cmd.Parameters.AddWithValue("@MachineName", Environment.MachineName); - - await cmd.ExecuteNonQueryAsync(cancellationToken); - } - - /// - /// Aktualisiert den Lauf-Datensatz mit Endzeitpunkt und Gesamtstatus. - /// - public async Task CompleteRunAsync( - Guid runId, - DateTimeOffset finishedAt, - bool overallSuccess, - int successCount, - int totalCount, - CancellationToken cancellationToken = default) - { - if (_connectionString is null) return; - - string status = overallSuccess - ? "Success" - : (successCount > 0 ? "PartialFailure" : "Failure"); - - const string sql = """ - UPDATE dbo.DeploymentRuns - SET FinishedAtUtc = @FinishedAtUtc, - OverallStatus = @OverallStatus - WHERE Id = @Id; - """; - - await using SqlConnection conn = new(_connectionString); - await conn.OpenAsync(cancellationToken); - await using SqlCommand cmd = new(sql, conn); - - cmd.Parameters.AddWithValue("@Id", runId); - cmd.Parameters.AddWithValue("@FinishedAtUtc", finishedAt.UtcDateTime); - cmd.Parameters.AddWithValue("@OverallStatus", status); - - await cmd.ExecuteNonQueryAsync(cancellationToken); - } - - /// - /// Schreibt das Ergebnis eines einzelnen Deployment-Ziels. - /// - public async Task WriteTargetResultAsync( - Guid runId, - TargetStepResult step, - CancellationToken cancellationToken = default) - { - if (_connectionString is null) return; - - string status = step.Success ? "Success" : "Failure"; - - const string sql = """ - INSERT INTO dbo.DeploymentTargetResults - (DeploymentRunId, TargetId, TargetName, - StartedAtUtc, FinishedAtUtc, - CopySucceeded, RestartSucceeded, TlsSucceeded, - ObservedFingerprint, Status, ErrorMessage) - VALUES - (@RunId, @TargetId, @TargetName, - @StartedAtUtc, @FinishedAtUtc, - @CopySucceeded, @RestartSucceeded, @TlsSucceeded, - @ObservedFingerprint, @Status, @ErrorMessage); - """; - - await using SqlConnection conn = new(_connectionString); - await conn.OpenAsync(cancellationToken); - await using SqlCommand cmd = new(sql, conn); - - cmd.Parameters.AddWithValue("@RunId", runId); - cmd.Parameters.AddWithValue("@TargetId", step.TargetId); - cmd.Parameters.AddWithValue("@TargetName", step.TargetName); - cmd.Parameters.AddWithValue("@StartedAtUtc", step.StartedAt.UtcDateTime); - cmd.Parameters.AddWithValue("@FinishedAtUtc", step.FinishedAt.UtcDateTime); - cmd.Parameters.AddWithValue("@CopySucceeded", step.CopySucceeded); - cmd.Parameters.AddWithValue("@RestartSucceeded", step.RestartSucceeded); - cmd.Parameters.AddWithValue("@TlsSucceeded", step.TlsSucceeded); - cmd.Parameters.AddWithValue("@ObservedFingerprint", (object?)step.ObservedFingerprint ?? DBNull.Value); - cmd.Parameters.AddWithValue("@Status", status); - cmd.Parameters.AddWithValue("@ErrorMessage", step.Success ? DBNull.Value : (object?)(step.Detail ?? step.StatusText)); - - await cmd.ExecuteNonQueryAsync(cancellationToken); - } - - /// - /// Persistiert ein vollständiges in einem einzigen Aufruf. - /// - public async Task PersistRunResultAsync( - DeploymentRunResult runResult, - CancellationToken cancellationToken = default) - { - if (_connectionString is null) return; - - await BeginRunAsync( - runResult.RunId, - runResult.StartedAt, - runResult.CertificateFilePath, - runResult.CertificateFingerprint, - cancellationToken); - - foreach (TargetStepResult step in runResult.TargetResults) - { - await WriteTargetResultAsync(runResult.RunId, step, cancellationToken); - } - - int successCount = runResult.TargetResults.Count(r => r.Success); - - await CompleteRunAsync( - runResult.RunId, - runResult.FinishedAt, - runResult.OverallSuccess, - successCount, - runResult.TargetResults.Count, - cancellationToken); - } -} diff --git a/ZA.CoreService.ESBCertificateManager/Data/SqlTargetRepository.cs b/ZA.CoreService.ESBCertificateManager/Data/SqlTargetRepository.cs deleted file mode 100644 index ba9c1fa..0000000 --- a/ZA.CoreService.ESBCertificateManager/Data/SqlTargetRepository.cs +++ /dev/null @@ -1,111 +0,0 @@ -using Microsoft.Data.SqlClient; -using ZA.CoreService.ESBCertificateManager.Models; - -namespace ZA.CoreService.ESBCertificateManager.Data; - -public sealed class SqlTargetRepository : ITargetRepository -{ - private readonly string _connectionString; - - public SqlTargetRepository(string connectionString) - { - _connectionString = connectionString; - } - - public string SourceDescription => "SQL Server (dbo.DeploymentTargets)"; - - public async Task> GetActiveTargetsAsync( - CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(_connectionString)) - { - throw new InvalidOperationException("Es ist keine SQL-Verbindungszeichenfolge konfiguriert."); - } - - const string sql = """ - SELECT - Id, - Name, - Environment, - IsActive, - CertificateTargetPath, - CertificateFileName, - ContainerName, - RestartType, - RestartCommand, - RestartArguments, - RestartTimeoutSeconds, - SonicConnectionName, - XapiSourcePath, - TlsHost, - TlsPort, - TlsServerName, - ExpectedFingerprint, - SortOrder - FROM dbo.DeploymentTargets - WHERE IsActive = 1 - ORDER BY SortOrder, Name; - """; - - List targets = []; - - await using SqlConnection connection = new(_connectionString); - await connection.OpenAsync(cancellationToken); - - await using SqlCommand command = new(sql, connection); - await using SqlDataReader reader = await command.ExecuteReaderAsync(cancellationToken); - - int id = reader.GetOrdinal("Id"); - int name = reader.GetOrdinal("Name"); - int environment = reader.GetOrdinal("Environment"); - int isActive = reader.GetOrdinal("IsActive"); - int certificateTargetPath = reader.GetOrdinal("CertificateTargetPath"); - int certificateFileName = reader.GetOrdinal("CertificateFileName"); - int containerName = reader.GetOrdinal("ContainerName"); - int restartType = reader.GetOrdinal("RestartType"); - int restartCommand = reader.GetOrdinal("RestartCommand"); - int restartArguments = reader.GetOrdinal("RestartArguments"); - int restartTimeoutSeconds = reader.GetOrdinal("RestartTimeoutSeconds"); - int sonicConnectionName = reader.GetOrdinal("SonicConnectionName"); - int xapiSourcePath = reader.GetOrdinal("XapiSourcePath"); - int tlsHost = reader.GetOrdinal("TlsHost"); - int tlsPort = reader.GetOrdinal("TlsPort"); - int tlsServerName = reader.GetOrdinal("TlsServerName"); - int expectedFingerprint = reader.GetOrdinal("ExpectedFingerprint"); - int sortOrder = reader.GetOrdinal("SortOrder"); - - while (await reader.ReadAsync(cancellationToken)) - { - RestartType parsedRestartType = Enum.TryParse( - reader.GetString(restartType), - ignoreCase: true, - out RestartType parsed) - ? parsed - : RestartType.None; - - targets.Add(new DeploymentTarget - { - Id = reader.GetInt32(id), - Name = reader.GetString(name), - Environment = reader.GetString(environment), - IsActive = reader.GetBoolean(isActive), - TargetDirectory = reader.GetString(certificateTargetPath), - CertificateFileName = reader.GetString(certificateFileName), - ContainerName = reader.GetString(containerName), - RestartType = parsedRestartType, - RestartCommand = reader.IsDBNull(restartCommand) ? string.Empty : reader.GetString(restartCommand), - RestartArguments = reader.GetString(restartArguments), - RestartTimeoutSeconds = reader.GetInt32(restartTimeoutSeconds), - SonicConnectionName = reader.GetString(sonicConnectionName), - XapiSourcePath = reader.GetString(xapiSourcePath), - TlsHost = reader.GetString(tlsHost), - TlsPort = reader.IsDBNull(tlsPort) ? null : reader.GetInt32(tlsPort), - TlsServerName = reader.GetString(tlsServerName), - ExpectedFingerprint = reader.IsDBNull(expectedFingerprint) ? null : reader.GetString(expectedFingerprint), - SortOrder = reader.GetInt32(sortOrder) - }); - } - - return targets; - } -} diff --git a/ZA.CoreService.ESBCertificateManager/Data/TargetRepositoryFactory.cs b/ZA.CoreService.ESBCertificateManager/Data/TargetRepositoryFactory.cs index 4ca5f84..607c6f1 100644 --- a/ZA.CoreService.ESBCertificateManager/Data/TargetRepositoryFactory.cs +++ b/ZA.CoreService.ESBCertificateManager/Data/TargetRepositoryFactory.cs @@ -11,34 +11,8 @@ public static class TargetRepositoryFactory string samplePath = Path.GetFullPath( Path.Combine(AppContext.BaseDirectory, settings.SampleTargetsPath)); - if (settings.UseOfflineSampleData) - { - ITargetRepository jsonRepo = new JsonTargetRepository(samplePath); - _ = await jsonRepo.GetActiveTargetsAsync(cancellationToken); - return (jsonRepo, $"Ziele geladen aus {jsonRepo.SourceDescription}."); - } - - if (string.IsNullOrWhiteSpace(settings.ConnectionString)) - { - return OfflineSample(samplePath, "Keine SQL-Verbindung konfiguriert – Offline-Sample wird verwendet."); - } - - try - { - SqlTargetRepository sqlRepo = new(settings.ConnectionString); - _ = await sqlRepo.GetActiveTargetsAsync(cancellationToken); - return (sqlRepo, $"Ziele geladen aus {sqlRepo.SourceDescription}."); - } - catch (Exception ex) - { - return OfflineSample( - samplePath, - $"SQL nicht erreichbar ({ex.Message}). Offline-Sample wird verwendet."); - } + ITargetRepository jsonRepo = new JsonTargetRepository(samplePath); + _ = await jsonRepo.GetActiveTargetsAsync(cancellationToken); + return (jsonRepo, $"Ziele geladen aus {jsonRepo.SourceDescription}."); } - - private static (ITargetRepository Repository, string LoadMessage) OfflineSample( - string samplePath, - string message) - => (new JsonTargetRepository(samplePath), message); } diff --git a/ZA.CoreService.ESBCertificateManager/Data/targets.sample.json b/ZA.CoreService.ESBCertificateManager/Data/targets.sample.json index 1a07203..14f657e 100644 --- a/ZA.CoreService.ESBCertificateManager/Data/targets.sample.json +++ b/ZA.CoreService.ESBCertificateManager/Data/targets.sample.json @@ -4,8 +4,8 @@ "Name": "DE-Test / ct-ZADBService", "Environment": "TEST", "IsActive": true, - "TargetDirectory": "DeploySandbox/target-esb", - "CertificateFileName": "esb-cert.cer", + "TargetDirectory": "", + "CertificateFileName": "", "ContainerName": "ct-ZADBService", "RestartType": "SonicContainer", "RestartCommand": "", @@ -18,25 +18,5 @@ "TlsServerName": "", "ExpectedFingerprint": null, "SortOrder": 10 - }, - { - "Id": 2, - "Name": "Lokaler CMD-Test (Echo)", - "Environment": "DEV", - "IsActive": false, - "TargetDirectory": "DeploySandbox/target-cmd", - "CertificateFileName": "esb-cert.cer", - "ContainerName": "", - "RestartType": "Command", - "RestartCommand": "cmd.exe", - "RestartArguments": "/c echo Neustart-Simulation OK", - "RestartTimeoutSeconds": 15, - "SonicConnectionName": "", - "XapiSourcePath": "", - "TlsHost": "", - "TlsPort": null, - "TlsServerName": "", - "ExpectedFingerprint": null, - "SortOrder": 20 } ] diff --git a/ZA.CoreService.ESBCertificateManager/Form1.cs b/ZA.CoreService.ESBCertificateManager/Form1.cs index 102d70c..1de8c15 100644 --- a/ZA.CoreService.ESBCertificateManager/Form1.cs +++ b/ZA.CoreService.ESBCertificateManager/Form1.cs @@ -1232,15 +1232,17 @@ namespace ZA.CoreService.ESBCertificateManager btnValidate.Anchor = AnchorStyles.Top | AnchorStyles.Right; btnValidate.Click += async (_, _) => await RunValidationAsync(); - btnRestartOnly = CreateSecondaryButton("ESB neu starten"); - btnRestartOnly.Size = new Size(150, 42); + // Primäraktion: Neustart (Deploy vorerst deaktiviert – Design/Layout bleibt) + btnRestartOnly = CreatePrimaryButton("ESB neu starten"); + btnRestartOnly.Size = new Size(170, 42); btnRestartOnly.Anchor = AnchorStyles.Top | AnchorStyles.Right; btnRestartOnly.Click += async (_, _) => await RunRestartOnlyAsync(); btnDeploy = CreatePrimaryButton("Deployment starten"); btnDeploy.Size = new Size(170, 42); btnDeploy.Anchor = AnchorStyles.Top | AnchorStyles.Right; - btnDeploy.Click += async (_, _) => await RunDeploymentAsync(); + btnDeploy.Visible = false; + btnDeploy.Enabled = false; panel.Controls.Add(btnCancelRun); panel.Controls.Add(btnValidate); @@ -1249,8 +1251,7 @@ namespace ZA.CoreService.ESBCertificateManager panel.Resize += (_, _) => { - btnDeploy.Left = panel.Width - btnDeploy.Width; - btnRestartOnly.Left = btnDeploy.Left - btnRestartOnly.Width - 12; + btnRestartOnly.Left = panel.Width - btnRestartOnly.Width; btnValidate.Left = btnRestartOnly.Left - btnValidate.Width - 12; btnCancelRun.Left = btnValidate.Left - btnCancelRun.Width - 12; }; @@ -1383,8 +1384,10 @@ namespace ZA.CoreService.ESBCertificateManager } btnCancelRun.Enabled = _isOperationRunning; - btnValidate.Enabled = idle && hasCertificate && hasTargets; - btnDeploy.Enabled = idle && hasCertificate && hasSelection; + // Prüfung nur für Neustart-Voraussetzungen (Zertifikat optional) + btnValidate.Enabled = idle && hasTargets; + btnDeploy.Enabled = false; + btnDeploy.Visible = false; if (btnRestartOnly is not null) { btnRestartOnly.Enabled = idle && hasSelection; @@ -1437,10 +1440,7 @@ namespace ZA.CoreService.ESBCertificateManager } List selected = GetSelectedTargets(); - PreflightValidationResult result = _orchestrator.ValidatePreflight( - txtCertificatePath.Text, - _loadedCertificateInfo, - selected); + PreflightValidationResult result = _orchestrator.ValidateRestartOnly(selected); HashSet failedTargetIds = result.Issues .Where(i => i.TargetId is not null) @@ -1581,81 +1581,10 @@ namespace ZA.CoreService.ESBCertificateManager } } - private async Task RunDeploymentAsync() + private Task RunDeploymentAsync() { - if (_isOperationRunning || _loadedCertificateInfo is null) - { - return; - } - - List selected = GetSelectedTargets(); - PreflightValidationResult preflight = _orchestrator.ValidatePreflight( - txtCertificatePath.Text, - _loadedCertificateInfo, - selected); - - if (!preflight.IsValid) - { - ShowPreflightIssues( - preflight, - "Deployment blockiert – Vorabprüfung fehlgeschlagen.", - "Deployment"); - return; - } - - _runCts?.Dispose(); - _runCts = new CancellationTokenSource(); - SetOperationRunning(true); - UpdateToNextStep(3); - SetStatus($"Deployment läuft für {selected.Count} Ziel(e)…", isError: false); - - Progress progress = new(update => - { - SetTargetRowStatus(update.TargetId, update.StatusText); - SetStatus(update.StatusText, isError: update.SuccessHint == false); - }); - - try - { - DeploymentRunResult runResult = await _orchestrator.RunAsync( - txtCertificatePath.Text, - _loadedCertificateInfo, - selected, - progress, - _runCts.Token); - - foreach (TargetStepResult targetResult in runResult.TargetResults) - { - SetTargetRowStatus(targetResult.TargetId, targetResult.StatusText); - } - - UpdateToNextStep(4); - SetStatus( - runResult.OverallSuccess - ? $"Deployment erfolgreich ({runResult.TargetResults.Count} Ziel(e))." - : "Deployment mit Fehlern beendet. Details in der Status-Spalte / Log.", - isError: !runResult.OverallSuccess); - } - catch (OperationCanceledException) - { - SetStatus("Deployment abgebrochen.", isError: true); - } - catch (Exception ex) - { - SetStatus($"Deployment fehlgeschlagen: {ex.Message}", isError: true); - MessageBox.Show( - this, - ex.Message, - "Deployment", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - } - finally - { - SetOperationRunning(false); - _runCts?.Dispose(); - _runCts = null; - } + // Deploy/Kopieren absichtlich deaktiviert – UI-Design bleibt. + return Task.CompletedTask; } private Panel CreateCard() diff --git a/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs b/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs index 8f2d30f..97622fe 100644 --- a/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs +++ b/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs @@ -65,16 +65,8 @@ public sealed class SonicConnection => 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 (SMC Management Application API)", - SonicManagementMode.LocalCmd => "LocalCmd (nur wenn stopcontainer.bat existiert)", - _ => EffectiveManagementMode.ToString() - }; + /// Anzeigetext für UI. + public string ManagementModeDisplay => "MfApi (SMC Management Application API)"; /// /// True wenn der Host aus ConnectionUrl dieser Maschine entspricht diff --git a/ZA.CoreService.ESBCertificateManager/Services/CertificateDeployer.cs b/ZA.CoreService.ESBCertificateManager/Services/CertificateDeployer.cs deleted file mode 100644 index ce7d4f2..0000000 --- a/ZA.CoreService.ESBCertificateManager/Services/CertificateDeployer.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System.Security.Cryptography; -using ZA.CoreService.ESBCertificateManager.Models; - -namespace ZA.CoreService.ESBCertificateManager.Services; - -public sealed class CertificateDeployer -{ - public Task<(bool Success, string Status, string? Detail, string? BackupPath)> DeployAsync( - string sourceCertificatePath, - DeploymentTarget target, - CancellationToken cancellationToken = default) - { - return Task.Run(() => Deploy(sourceCertificatePath, target, cancellationToken), cancellationToken); - } - - private static (bool Success, string Status, string? Detail, string? BackupPath) Deploy( - string sourceCertificatePath, - DeploymentTarget target, - CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (!File.Exists(sourceCertificatePath)) - { - return (false, "Quelle fehlt", $"Quelldatei nicht gefunden: {sourceCertificatePath}", null); - } - - string targetDirectory = PathResolver.ResolvePath(target.TargetDirectory); - Directory.CreateDirectory(targetDirectory); - - string destinationPath = Path.Combine(targetDirectory, target.CertificateFileName); - string? backupPath = null; - - try - { - if (File.Exists(destinationPath)) - { - backupPath = $"{destinationPath}.bak-{DateTime.Now:yyyyMMddHHmmss}"; - File.Copy(destinationPath, backupPath, overwrite: false); - } - - cancellationToken.ThrowIfCancellationRequested(); - File.Copy(sourceCertificatePath, destinationPath, overwrite: true); - - string sourceHash = ComputeSha256(sourceCertificatePath); - string destHash = ComputeSha256(destinationPath); - - if (!string.Equals(sourceHash, destHash, StringComparison.OrdinalIgnoreCase)) - { - RestoreFromBackupOrDelete(destinationPath, backupPath); - return (false, "Hash-Fehler", "SHA-256 von Quelle und Ziel stimmen nicht überein. Rollback ausgeführt.", backupPath); - } - - return (true, "Kopiert", $"Ziel: {destinationPath}", backupPath); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - try - { - if (backupPath is not null && File.Exists(backupPath) && File.Exists(destinationPath)) - { - File.Copy(backupPath, destinationPath, overwrite: true); - } - } - catch - { - // Rollback best-effort - } - - return (false, "Kopierfehler", ex.Message, backupPath); - } - } - - private static void RestoreFromBackupOrDelete(string destinationPath, string? backupPath) - { - if (backupPath is not null && File.Exists(backupPath)) - { - File.Copy(backupPath, destinationPath, overwrite: true); - return; - } - - if (File.Exists(destinationPath)) - { - File.Delete(destinationPath); - } - } - - public static string ComputeSha256(string filePath) - { - using FileStream stream = File.OpenRead(filePath); - byte[] hash = SHA256.HashData(stream); - return Convert.ToHexString(hash); - } -} diff --git a/ZA.CoreService.ESBCertificateManager/Services/DeploymentOrchestrator.cs b/ZA.CoreService.ESBCertificateManager/Services/DeploymentOrchestrator.cs index fb20aeb..99bdce5 100644 --- a/ZA.CoreService.ESBCertificateManager/Services/DeploymentOrchestrator.cs +++ b/ZA.CoreService.ESBCertificateManager/Services/DeploymentOrchestrator.cs @@ -1,37 +1,25 @@ -using ZA.CoreService.ESBCertificateManager.Data; using ZA.CoreService.ESBCertificateManager.Models; namespace ZA.CoreService.ESBCertificateManager.Services; +/// +/// Orchestriert nur noch Container-Neustart (kein Deploy/TLS/SQL). +/// public sealed class DeploymentOrchestrator { - private readonly CertificateDeployer _deployer = new(); private readonly RestartExecutor _restartExecutor; - private readonly TlsCertificateProbe _tlsProbe; private readonly PreflightValidator _preflightValidator = new(); - private readonly SqlRunLogger _sqlRunLogger; private readonly AppSettings _settings; public DeploymentOrchestrator(AppSettings settings) { _settings = settings; - _tlsProbe = new TlsCertificateProbe(settings.TlsTimeoutSeconds, settings.TlsRetryCount); _restartExecutor = new RestartExecutor(settings.SonicConnections); - _sqlRunLogger = new SqlRunLogger(settings.ConnectionString); } - public PreflightValidationResult ValidatePreflight( - string? certificatePath, - CertificateInfo? certificateInfo, - IReadOnlyList selectedTargets) - => _preflightValidator.Validate(certificatePath, certificateInfo, selectedTargets); - public PreflightValidationResult ValidateRestartOnly(IReadOnlyList selectedTargets) => _preflightValidator.ValidateRestartOnly(selectedTargets); - /// - /// Nur ESB-/Container-Neustart – ohne Zertifikatskopieren und ohne TLS-Probe. - /// public async Task RestartOnlyAsync( IReadOnlyList selectedTargets, IProgress? progress, @@ -41,7 +29,7 @@ public sealed class DeploymentOrchestrator List results = []; using RunLogger logger = new(_settings.LogDirectory); - logger.Write($"Neustart-only gestartet für {selectedTargets.Count} Ziel(e)."); + logger.Write($"Neustart gestartet für {selectedTargets.Count} Ziel(e)."); foreach (DeploymentTarget target in selectedTargets) { @@ -60,16 +48,7 @@ public sealed class DeploymentOrchestrator }; 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}"); - } + $"Neustart beendet. Erfolg={runResult.OverallSuccess}; Dauer={(finishedAt - startedAt).TotalSeconds:F1}s; Log={logger.LogFilePath}"); return runResult; } @@ -87,7 +66,8 @@ public sealed class DeploymentOrchestrator (bool restartOk, string restartStatus, string? restartDetail) = await _restartExecutor.ExecuteAsync(target, cancellationToken); - RecordStep(steps, logger, target.Name, "Restart", restartStatus, restartDetail); + steps.Add($"{restartStatus}: {restartDetail}"); + logger.Write($"[{target.Name}] Restart: {restartStatus} | {restartDetail}"); TargetStepResult targetResult = new() { @@ -108,156 +88,6 @@ public sealed class DeploymentOrchestrator progress?.Report(new TargetProgressUpdate(target.Id, restartStatus, restartOk)); return targetResult; } - - public async Task RunAsync( - string certificatePath, - CertificateInfo certificateInfo, - IReadOnlyList selectedTargets, - IProgress? progress, - CancellationToken cancellationToken = default) - { - DateTimeOffset startedAt = DateTimeOffset.Now; - List results = []; - - using RunLogger logger = new(_settings.LogDirectory); - logger.Write($"Deployment gestartet für {selectedTargets.Count} Ziel(e). Zertifikat={Path.GetFileName(certificatePath)}"); - - foreach (DeploymentTarget target in selectedTargets) - { - cancellationToken.ThrowIfCancellationRequested(); - results.Add(await RunTargetAsync(target, certificatePath, certificateInfo, logger, progress, cancellationToken)); - } - - DateTimeOffset finishedAt = DateTimeOffset.Now; - DeploymentRunResult runResult = new() - { - TargetResults = results, - StartedAt = startedAt, - FinishedAt = finishedAt, - CertificateFilePath = certificatePath, - CertificateFingerprint = certificateInfo.FingerprintSha256 - }; - - logger.Write( - $"Deployment beendet. Erfolg={runResult.OverallSuccess}; Dauer={(finishedAt - startedAt).TotalSeconds:F1}s; Log={logger.LogFilePath}"); - - // Ergebnis in SQL-Datenbank protokollieren (wenn ConnectionString konfiguriert) - try - { - await _sqlRunLogger.PersistRunResultAsync(runResult, cancellationToken); - } - catch (Exception ex) - { - logger.Write($"SQL-Protokollierung fehlgeschlagen (nicht kritisch): {ex.Message}"); - } - - return runResult; - } - - private async Task RunTargetAsync( - DeploymentTarget target, - string certificatePath, - CertificateInfo certificateInfo, - RunLogger logger, - IProgress? progress, - CancellationToken cancellationToken) - { - List steps = []; - DateTimeOffset targetStart = DateTimeOffset.Now; - progress?.Report(new TargetProgressUpdate(target.Id, "Läuft…", false)); - - (bool copyOk, string copyStatus, string? copyDetail, _) = - await _deployer.DeployAsync(certificatePath, target, cancellationToken); - - RecordStep(steps, logger, target.Name, "Deploy", copyStatus, copyDetail); - if (!copyOk) - { - return Fail(target, copyStatus, copyDetail, steps, targetStart, progress); - } - - progress?.Report(new TargetProgressUpdate(target.Id, copyStatus, false)); - - (bool restartOk, string restartStatus, string? restartDetail) = - await _restartExecutor.ExecuteAsync(target, cancellationToken); - - RecordStep(steps, logger, target.Name, "Restart", restartStatus, restartDetail); - if (!restartOk) - { - return Fail(target, restartStatus, restartDetail, steps, targetStart, progress, - copySucceeded: copyOk); - } - - progress?.Report(new TargetProgressUpdate(target.Id, restartStatus, false)); - - (bool tlsOk, string tlsStatus, string? tlsDetail, string? observedFingerprint) = await _tlsProbe.ProbeAsync( - target, - certificateInfo.FingerprintSha256, - cancellationToken); - - RecordStep(steps, logger, target.Name, "TLS", tlsStatus, tlsDetail); - - bool success = tlsOk; - string finalStatus = success - ? (string.IsNullOrWhiteSpace(target.TlsHost) ? "Erfolg" : tlsStatus) - : tlsStatus; - - TargetStepResult targetResult = new() - { - TargetId = target.Id, - TargetName = target.Name, - Success = success, - StatusText = finalStatus, - Detail = tlsDetail, - Steps = steps, - StartedAt = targetStart, - FinishedAt = DateTimeOffset.Now, - CopySucceeded = copyOk, - RestartSucceeded = restartOk, - TlsSucceeded = tlsOk, - ObservedFingerprint = observedFingerprint - }; - progress?.Report(new TargetProgressUpdate(target.Id, finalStatus, success)); - return targetResult; - } - - private static void RecordStep( - List steps, - RunLogger logger, - string targetName, - string phase, - string status, - string? detail) - { - steps.Add($"{status}: {detail}"); - logger.Write($"[{targetName}] {phase}: {status} | {detail}"); - } - - private static TargetStepResult Fail( - DeploymentTarget target, - string status, - string? detail, - List steps, - DateTimeOffset startedAt, - IProgress? progress, - bool copySucceeded = false, - bool restartSucceeded = false) - { - progress?.Report(new TargetProgressUpdate(target.Id, status, false)); - return new TargetStepResult - { - TargetId = target.Id, - TargetName = target.Name, - Success = false, - StatusText = status, - Detail = detail, - Steps = steps, - StartedAt = startedAt, - FinishedAt = DateTimeOffset.Now, - CopySucceeded = copySucceeded, - RestartSucceeded = restartSucceeded, - TlsSucceeded = false - }; - } } public readonly record struct TargetProgressUpdate(int TargetId, string StatusText, bool? SuccessHint); diff --git a/ZA.CoreService.ESBCertificateManager/Services/PreflightValidator.cs b/ZA.CoreService.ESBCertificateManager/Services/PreflightValidator.cs index 3bd6933..520faef 100644 --- a/ZA.CoreService.ESBCertificateManager/Services/PreflightValidator.cs +++ b/ZA.CoreService.ESBCertificateManager/Services/PreflightValidator.cs @@ -4,136 +4,37 @@ namespace ZA.CoreService.ESBCertificateManager.Services; public sealed class PreflightValidator { - public PreflightValidationResult Validate( - string? certificatePath, - CertificateInfo? certificateInfo, - IReadOnlyList selectedTargets) - { - PreflightValidationResult result = new(); - - if (string.IsNullOrWhiteSpace(certificatePath) || !File.Exists(certificatePath)) - { - AddIssue(result, "Es ist keine gültige Zertifikatsdatei ausgewählt."); - } - - if (certificateInfo is null) - { - AddIssue(result, "Zertifikatsmetadaten sind nicht geladen."); - } - else if (!certificateInfo.IsCurrentlyValid) - { - AddIssue(result, "Das geladene Zertifikat ist abgelaufen oder ungültig."); - } - - if (selectedTargets.Count == 0) - { - AddIssue(result, "Bitte mindestens ein Ziel anhaken."); - } - - foreach (DeploymentTarget target in selectedTargets) - { - ValidateTarget(result, target, requireDeployPaths: true); - } - - return result; - } - - /// - /// Vorabprüfung nur für ESB-Neustart (ohne Zertifikat / Kopierpfade). - /// public PreflightValidationResult ValidateRestartOnly(IReadOnlyList selectedTargets) { PreflightValidationResult result = new(); if (selectedTargets.Count == 0) { - AddIssue(result, "Bitte mindestens ein Ziel anhaken."); + result.Issues.Add(new ValidationIssue { Message = "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)) - { - AddIssue(result, $"Ziel '{target.Name}': TargetDirectory fehlt.", target.Id); - } - - if (string.IsNullOrWhiteSpace(target.CertificateFileName)) - { - AddIssue(result, $"Ziel '{target.Name}': CertificateFileName fehlt.", target.Id); - } - } - - if (target.RestartType == RestartType.Command - && string.IsNullOrWhiteSpace(target.RestartCommand)) - { - AddIssue(result, $"Ziel '{target.Name}': RestartCommand fehlt bei RestartType=Command.", target.Id); - } - - if (target.RestartType is RestartType.SonicContainer or RestartType.SonicContainerWithXapi) { if (string.IsNullOrWhiteSpace(target.ContainerName)) { - AddIssue(result, $"Ziel '{target.Name}': ContainerName fehlt bei RestartType={target.RestartType}.", target.Id); + result.Issues.Add(new ValidationIssue + { + Message = $"Ziel '{target.Name}': ContainerName fehlt.", + TargetId = target.Id + }); } if (string.IsNullOrWhiteSpace(target.SonicConnectionName)) { - AddIssue(result, $"Ziel '{target.Name}': SonicConnectionName fehlt bei RestartType={target.RestartType}.", target.Id); + result.Issues.Add(new ValidationIssue + { + Message = $"Ziel '{target.Name}': SonicConnectionName fehlt.", + TargetId = target.Id + }); } } - if (target.RestartType == RestartType.SonicContainerWithXapi - && string.IsNullOrWhiteSpace(target.XapiSourcePath)) - { - AddIssue(result, $"Ziel '{target.Name}': XapiSourcePath fehlt bei RestartType=SonicContainerWithXapi.", target.Id); - } - - if (!requireDeployPaths || string.IsNullOrWhiteSpace(target.TargetDirectory)) - { - return; - } - - try - { - string directory = PathResolver.ResolvePath(target.TargetDirectory); - Directory.CreateDirectory(directory); - - string probeFile = Path.Combine(directory, $".write-probe-{Guid.NewGuid():N}"); - File.WriteAllText(probeFile, "ok"); - File.Delete(probeFile); - } - catch (Exception ex) - { - AddIssue(result, $"Ziel '{target.Name}': Verzeichnis nicht beschreibbar ({ex.Message}).", target.Id); - } - } - - private static void AddIssue(PreflightValidationResult result, string message, int? targetId = null) - { - result.Issues.Add(new ValidationIssue - { - TargetId = targetId, - Message = message - }); + return result; } } diff --git a/ZA.CoreService.ESBCertificateManager/Services/RestartExecutor.cs b/ZA.CoreService.ESBCertificateManager/Services/RestartExecutor.cs index 34054cf..1e748be 100644 --- a/ZA.CoreService.ESBCertificateManager/Services/RestartExecutor.cs +++ b/ZA.CoreService.ESBCertificateManager/Services/RestartExecutor.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using ZA.CoreService.ESBCertificateManager.Models; namespace ZA.CoreService.ESBCertificateManager.Services; @@ -12,26 +11,13 @@ public sealed class RestartExecutor _sonicConnections = sonicConnections; } - public Task<(bool Success, string Status, string? Detail)> ExecuteAsync( + public async Task<(bool Success, string Status, string? Detail)> ExecuteAsync( DeploymentTarget target, CancellationToken cancellationToken = default) - => target.RestartType switch - { - RestartType.None => Task.FromResult<(bool, string, string?)>((true, "Neustart übersprungen", "RestartType=None")), - RestartType.Command => ExecuteCommandAsync(target, cancellationToken), - RestartType.SonicContainer => ExecuteSonicRestartAsync(target, importXapi: false, cancellationToken), - RestartType.SonicContainerWithXapi => ExecuteSonicRestartAsync(target, importXapi: true, cancellationToken), - _ => Task.FromResult<(bool, string, string?)>((false, "Unbekannter RestartType", $"RestartType={target.RestartType}")) - }; - - private async Task<(bool Success, string Status, string? Detail)> ExecuteSonicRestartAsync( - DeploymentTarget target, - bool importXapi, - CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(target.ContainerName)) { - return (false, "Sonic-Neustart fehlgeschlagen", $"Ziel '{target.Name}': ContainerName fehlt."); + return (false, "Neustart fehlgeschlagen", $"Ziel '{target.Name}': ContainerName fehlt."); } SonicConnection? connection = _sonicConnections @@ -40,111 +26,10 @@ public sealed class RestartExecutor if (connection is null) { return (false, "Sonic-Verbindung nicht gefunden", - $"Ziel '{target.Name}': SonicConnection '{target.SonicConnectionName}' ist nicht in AppSettings konfiguriert."); + $"Ziel '{target.Name}': SonicConnection '{target.SonicConnectionName}' fehlt in appsettings."); } using SonicManagementClient client = new(connection); - - if (importXapi) - { - if (string.IsNullOrWhiteSpace(target.XapiSourcePath)) - { - return (false, "XApi-Import fehlgeschlagen", - $"Ziel '{target.Name}': XapiSourcePath fehlt bei RestartType=SonicContainerWithXapi."); - } - - return await client.ImportXapiAndRestartAsync(target.ContainerName, target.XapiSourcePath, cancellationToken); - } - return await client.RestartContainerAsync(target.ContainerName, cancellationToken); } - - private async Task<(bool Success, string Status, string? Detail)> ExecuteCommandAsync( - DeploymentTarget target, - CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(target.RestartCommand)) - { - return (true, "Neustart übersprungen", "RestartType=None"); - } - - int timeoutSeconds = Math.Clamp(target.RestartTimeoutSeconds, 1, 600); - - ProcessStartInfo startInfo = new() - { - FileName = target.RestartCommand, - Arguments = target.RestartArguments ?? string.Empty, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - }; - - try - { - using Process process = new() { StartInfo = startInfo }; - if (!process.Start()) - { - return (false, "Neustart fehlgeschlagen", "Prozess konnte nicht gestartet werden."); - } - - Task stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken); - Task stderrTask = process.StandardError.ReadToEndAsync(cancellationToken); - - using CancellationTokenSource timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - timeoutCts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds)); - - try - { - await process.WaitForExitAsync(timeoutCts.Token); - } - catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) - { - try { process.Kill(entireProcessTree: true); } catch { /* ignore */ } - return (false, "Neustart Timeout", $"Timeout nach {timeoutSeconds}s."); - } - - string detail = BuildDetail(process.ExitCode, await stdoutTask, await stderrTask); - return process.ExitCode == 0 - ? (true, "Neustart ok", detail) - : (false, "Neustart fehlgeschlagen", detail); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - return (false, "Neustart fehlgeschlagen", ex.Message); - } - } - - private static string BuildDetail(int exitCode, string stdout, string stderr) - { - string detail = $"ExitCode={exitCode}"; - - stdout = Truncate(stdout).Trim(); - if (!string.IsNullOrWhiteSpace(stdout)) - { - detail += "; out=" + stdout; - } - - stderr = Truncate(stderr).Trim(); - if (!string.IsNullOrWhiteSpace(stderr)) - { - detail += "; err=" + stderr; - } - - return detail; - } - - private static string Truncate(string value, int max = 400) - { - if (string.IsNullOrEmpty(value) || value.Length <= max) - { - return value; - } - - return value[..max] + "…"; - } } diff --git a/ZA.CoreService.ESBCertificateManager/Services/SonicBinRestartExecutor.cs b/ZA.CoreService.ESBCertificateManager/Services/SonicBinRestartExecutor.cs deleted file mode 100644 index 4cfe2d0..0000000 --- a/ZA.CoreService.ESBCertificateManager/Services/SonicBinRestartExecutor.cs +++ /dev/null @@ -1,245 +0,0 @@ -using System.Diagnostics; -using System.Text; -using ZA.CoreService.ESBCertificateManager.Models; - -namespace ZA.CoreService.ESBCertificateManager.Services; - -/// -/// Neustart über Sonic-Server-Scripts: -/// stopcontainer.bat / startcontainer.bat -/// Diese liegen nur in einer vollen MQ/ESB-Server-Installation – nicht in einer -/// reinen Sonic Management Console (SMC/Client). -/// -public sealed class SonicBinRestartExecutor -{ - private readonly SonicConnection _connection; - - public SonicBinRestartExecutor(SonicConnection connection) - { - _connection = connection; - } - - public (bool Ok, string? Error, string? Detail) Probe() - { - string sonicHome = _connection.SonicHome?.Trim() ?? string.Empty; - if (string.IsNullOrWhiteSpace(sonicHome)) - { - return (false, "SonicHome ist leer – in appsettings setzen.", null); - } - - if (!Directory.Exists(sonicHome)) - { - return (false, $"SonicHome existiert nicht: '{sonicHome}'", null); - } - - (string? stop, string? start, string searchNote) = LocateContainerScripts(sonicHome); - if (stop is null || start is null) - { - string binDir = Path.Combine(sonicHome, "bin"); - string binListing = DescribeDirectory(binDir); - bool looksLikeSmcOnly = Directory.Exists(Path.Combine(sonicHome, "lib")) - && !File.Exists(Path.Combine(binDir, "stopcontainer.bat")); - - string why = looksLikeSmcOnly - ? "Das sieht nach einer Sonic Management Console / Client-Installation aus " - + "(lib vorhanden, aber keine Server-Scripts). " - + "stopcontainer.bat gibt es nur auf dem Sonic-SERVER, nicht in der reinen SMC." - : "Server-Scripts wurden unter SonicHome nicht gefunden."; - - return (false, - why + $" Gesucht unter '{sonicHome}'.", - $"{searchNote}\nInhalt von bin: {binListing}\n" - + "Lösung A: SonicHome auf den Server-Installationspfad setzen (dort wo stopcontainer.bat liegt).\n" - + "Lösung B: App lässt automatisch MfApi/SMC-Verbindung versuchen (ConnectionUrl + Login)."); - } - - return (true, null, $"stop={stop}; start={start}"); - } - - public async Task<(bool Success, string Status, string? Detail)> RestartAsync( - string containerName, - CancellationToken cancellationToken = default) - { - (bool probeOk, string? probeError, string? probeDetail) = Probe(); - if (!probeOk) - { - return (false, "Neustart fehlgeschlagen (SonicBin)", - $"{probeError}\n{probeDetail}"); - } - - string sonicHome = _connection.SonicHome.Trim(); - (string? stopBat, string? startBat, _) = LocateContainerScripts(sonicHome); - string bin = Path.GetDirectoryName(stopBat!)!; - - string shortName = containerName.Contains('.') - ? containerName[(containerName.IndexOf('.') + 1)..] - : containerName; - string fullName = containerName.Contains('.') - ? containerName - : $"{_connection.DomainName}.{containerName}"; - - StringBuilder log = new(); - log.AppendLine($"SonicHome={sonicHome}"); - log.AppendLine($"stop={stopBat}"); - log.AppendLine($"start={startBat}"); - log.AppendLine($"Container={fullName} (kurz={shortName})"); - - (bool stopOk, string stopOut, string stopErr, int stopCode) = - await RunBatAsync(stopBat!, fullName, bin, cancellationToken); - log.AppendLine($"STOP ExitCode={stopCode}"); - if (stopOut.Length > 0) log.AppendLine("STOP out: " + Truncate(stopOut)); - if (stopErr.Length > 0) log.AppendLine("STOP err: " + Truncate(stopErr)); - - await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); - - (bool startOk, string startOut, string startErr, int startCode) = - await RunBatAsync(startBat!, fullName, bin, cancellationToken); - log.AppendLine($"START ExitCode={startCode}"); - if (startOut.Length > 0) log.AppendLine("START out: " + Truncate(startOut)); - if (startErr.Length > 0) log.AppendLine("START err: " + Truncate(startErr)); - - if (startCode != 0 - && !string.Equals(shortName, fullName, StringComparison.OrdinalIgnoreCase)) - { - log.AppendLine($"Retry START mit Kurzname '{shortName}'…"); - (_, startOut, startErr, startCode) = - await RunBatAsync(startBat!, shortName, bin, cancellationToken); - log.AppendLine($"START(short) ExitCode={startCode}"); - if (startOut.Length > 0) log.AppendLine("START out: " + Truncate(startOut)); - if (startErr.Length > 0) log.AppendLine("START err: " + Truncate(startErr)); - } - - await Task.Delay( - TimeSpan.FromSeconds(Math.Clamp(_connection.PostRestartDelaySeconds, 0, 120)), - cancellationToken); - - if (startCode != 0) - { - return (false, "Neustart fehlgeschlagen (SonicBin)", - $"startcontainer ExitCode={startCode} (stop ExitCode={stopCode}).\n{log}"); - } - - return (true, $"Container '{fullName}' neugestartet (SonicBin)", log.ToString()); - } - - /// - /// Sucht stop/startcontainer.bat unter SonicHome\bin und rekursiv (max. Tiefe 4). - /// - public static (string? StopBat, string? StartBat, string Note) LocateContainerScripts(string sonicHome) - { - List candidates = []; - - void AddDir(string? dir) - { - if (!string.IsNullOrWhiteSpace(dir) && Directory.Exists(dir) && !candidates.Contains(dir)) - { - candidates.Add(dir); - } - } - - AddDir(Path.Combine(sonicHome, "bin")); - AddDir(Path.Combine(sonicHome, "MQ_HOME", "bin")); - AddDir(Path.Combine(sonicHome, "MQ", "bin")); - - try - { - string? parent = Directory.GetParent(sonicHome)?.FullName; - if (parent is not null) - { - foreach (string child in Directory.EnumerateDirectories(parent)) - { - AddDir(Path.Combine(child, "bin")); - } - } - } - catch - { - // ignore - } - - // Rekursiv nach Dateinamen suchen - try - { - foreach (string file in Directory.EnumerateFiles(sonicHome, "stopcontainer.bat", SearchOption.AllDirectories) - .Take(20)) - { - AddDir(Path.GetDirectoryName(file)); - } - } - catch - { - // ignore permission issues - } - - foreach (string dir in candidates) - { - string stop = Path.Combine(dir, "stopcontainer.bat"); - string start = Path.Combine(dir, "startcontainer.bat"); - if (File.Exists(stop) && File.Exists(start)) - { - return (stop, start, $"Scripts gefunden in '{dir}'"); - } - } - - return (null, null, $"Keine Scripts in {candidates.Count} geprüften bin-Ordnern."); - } - - private static string DescribeDirectory(string dir) - { - if (!Directory.Exists(dir)) - { - return "(Ordner existiert nicht)"; - } - - try - { - string[] names = Directory.GetFileSystemEntries(dir) - .Select(Path.GetFileName) - .Where(n => n is not null) - .Cast() - .OrderBy(n => n) - .Take(25) - .ToArray(); - return names.Length == 0 ? "(leer)" : string.Join(", ", names); - } - catch (Exception ex) - { - return $"(nicht lesbar: {ex.Message})"; - } - } - - private static async Task<(bool Started, string StdOut, string StdErr, int ExitCode)> RunBatAsync( - string batPath, - string argument, - string workingDirectory, - CancellationToken cancellationToken) - { - ProcessStartInfo psi = new() - { - FileName = "cmd.exe", - Arguments = $"/c \"\"{batPath}\" \"{argument}\"\"", - WorkingDirectory = workingDirectory, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - StandardOutputEncoding = Encoding.UTF8, - StandardErrorEncoding = Encoding.UTF8 - }; - - using Process process = new() { StartInfo = psi }; - if (!process.Start()) - { - return (false, string.Empty, "cmd.exe konnte nicht gestartet werden.", -1); - } - - Task stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken); - Task stderrTask = process.StandardError.ReadToEndAsync(cancellationToken); - - await process.WaitForExitAsync(cancellationToken); - return (true, (await stdoutTask).Trim(), (await stderrTask).Trim(), process.ExitCode); - } - - private static string Truncate(string value, int max = 500) - => value.Length <= max ? value : value[..max] + "…"; -} diff --git a/ZA.CoreService.ESBCertificateManager/Services/SonicManagementClient.cs b/ZA.CoreService.ESBCertificateManager/Services/SonicManagementClient.cs index e349c0c..56bb6c3 100644 --- a/ZA.CoreService.ESBCertificateManager/Services/SonicManagementClient.cs +++ b/ZA.CoreService.ESBCertificateManager/Services/SonicManagementClient.cs @@ -1,339 +1,66 @@ -using System.Net; -using System.Net.Http.Headers; -using System.Text; -using System.Text.Json; using ZA.CoreService.ESBCertificateManager.Models; namespace ZA.CoreService.ESBCertificateManager.Services; /// -/// Verwaltet Sonic ESB Container über die Management Console. -/// -/// Modus = MfApi (Standard, laut Aurea CX Messenger Doku-Index → Management Application API): -/// Dieselbe Aktion wie „Restart“ in der Sonic Management Console: -/// JMSConnectorClient → MFProxyFactory.createAgentProxy → IAgentProxy.restart -/// über ConnectionUrl + SMC-User/Pass aus appsettings. -/// stopcontainer.bat wird NICHT verwendet (existiert in vielen SMC-only Installationen nicht). -/// -/// Modus = LocalCmd / WinRm: nur optional, wenn Server-Scripts vorhanden sind. -/// Modus = HttpApi: REST falls vorhanden. +/// Sonic MfApi (wie SMC-Neustart) – ohne WinRM/HTTP/XApi. /// public sealed class SonicManagementClient : IDisposable { private readonly SonicConnection _connection; - private readonly HttpClient? _http; - private readonly WinRmExecutor? _scriptRunner; - private readonly SonicMfApiExecutor? _mfApi; - - // Gecachter HTTP-Basis-Pfad (nur im HttpApi-Modus) - private string? _resolvedContainerBasePath; - - private static readonly string[] CandidateContainerPaths = - [ - "/mf/rest/v1/domains/{domain}/containers", - "/api/v1/domains/{domain}/containers", - "/sonic/management/domains/{domain}/containers", - "/containers" - ]; - - private SonicManagementMode Mode => _connection.EffectiveManagementMode; - - private bool UsesMfApi => Mode == SonicManagementMode.MfApi; - - private bool UsesScripts => - Mode is SonicManagementMode.WinRm or SonicManagementMode.LocalCmd; + private readonly SonicMfApiExecutor _mfApi; public SonicManagementClient(SonicConnection connection) { _connection = connection; - - if (UsesMfApi) - { - _mfApi = new SonicMfApiExecutor(connection); - } - else if (UsesScripts) - { - _scriptRunner = new WinRmExecutor(connection); - } - else - { - HttpClientHandler handler = new() - { - ServerCertificateCustomValidationCallback = - HttpClientHandler.DangerousAcceptAnyServerCertificateValidator - }; - - _http = new HttpClient(handler) - { - BaseAddress = BuildHttpBaseUri(connection.ConnectionUrl, connection.ManagementHttpPort), - Timeout = TimeSpan.FromSeconds(Math.Clamp(connection.TimeoutSeconds, 5, 300)) - }; - - string credentials = Convert.ToBase64String( - Encoding.UTF8.GetBytes($"{connection.Username}:{connection.Password}")); - _http.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue("Basic", credentials); - _http.DefaultRequestHeaders.Accept.Add( - new MediaTypeWithQualityHeaderValue("application/json")); - } + _mfApi = new SonicMfApiExecutor(connection); } - // --------------------------------------------------------------- - // Öffentliche API - // --------------------------------------------------------------- - - /// - /// Prüft die Verbindung zur Management Console. - /// 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); + if (ok) { - (bool ok, string? error) = await _mfApi!.TestConnectionAsync(cancellationToken); - if (ok) - { - return (true, null, - $"MfApi {_connection.ConnectionUrl} Domain={_connection.DomainName}"); - } - - // Ohne lokales Java: Domain-Manager-TCP + KnownContainers reichen für Discovery. - bool javaMissing = error?.Contains("Java nicht gefunden", StringComparison.OrdinalIgnoreCase) == true; - if (javaMissing) - { - SonicBinRestartExecutor bin = new(_connection); - (bool binOk, _, string? binDetail) = bin.Probe(); - if (binOk) - { - return (true, null, - $"SonicBin Fallback (kein Java für MfApi) {binDetail}; {_connection.ConnectionUrl}"); - } - - if (_connection.KnownContainers.Count > 0 - || await IsTcpReachableAsync(_connection.ConnectionUrl, cancellationToken)) - { - return (true, null, - $"MfApi ohne Java – KnownContainers/TCP; Hinweis: {Truncate(error ?? string.Empty, 180)}"); - } - } - - return (false, error, null); + return (true, null, $"MfApi {_connection.ConnectionUrl} Domain={_connection.DomainName}"); } - if (UsesScripts) + if (_connection.KnownContainers.Count > 0 + && await IsTcpReachableAsync(_connection.ConnectionUrl, cancellationToken)) { - if (Mode == SonicManagementMode.LocalCmd) - { - (bool binOk, string? binError, string? binDetail) = new SonicBinRestartExecutor(_connection).Probe(); - return binOk - ? (true, null, $"LocalCmd/SonicBin {binDetail}") - : (false, binError, null); - } - - (bool ok, string? error) = await _scriptRunner!.TestConnectionAsync(cancellationToken); - return ok - ? (true, null, $"WinRM auf {ExtractHost(_connection.ConnectionUrl)}:{_connection.WinRmPort}") - : (false, error, null); + return (true, null, + $"MfApi-Ping fehlgeschlagen, KnownContainers/TCP ok. Hinweis: {Truncate(error)}"); } - try - { - string? path = await ResolveContainerBasePathAsync(cancellationToken); - if (path is null) - { - return (false, - $"Keine Sonic HTTP-API unter {_http!.BaseAddress} gefunden.\n" + - $"Geprüfte Pfade: {string.Join(", ", GetCandidatePaths())}\n" + - $"Tipp: ManagementMode auf 'MfApi' setzen (ConnectionUrl + SMC-Logins).", - null); - } - - return (true, null, path); - } - catch (TaskCanceledException) when (!cancellationToken.IsCancellationRequested) - { - return (false, - $"HTTP-Timeout nach {_connection.TimeoutSeconds}s – Port {_connection.ManagementHttpPort} nicht erreichbar.\n" + - $"Tipp: ManagementMode auf 'MfApi' setzen.", - null); - } - catch (Exception ex) - { - return (false, ex.Message, null); - } + return (false, error ?? "MfApi-Verbindung fehlgeschlagen", null); } - /// - /// Listet alle Container der Domain auf. - /// public async Task<(bool Success, IReadOnlyList ContainerNames, string? Error)> GetContainersAsync( CancellationToken cancellationToken = default) { - if (UsesMfApi) + (bool ok, IReadOnlyList names, string? error) = + await _mfApi.ListContainersAsync(cancellationToken); + + if (ok && names.Count > 0) { - return await GetContainersViaMfApiAsync(cancellationToken); + return (true, names, null); } - if (UsesScripts) + if (_connection.KnownContainers.Count > 0) { - return await GetContainersViaScriptAsync(cancellationToken); + return (true, _connection.KnownContainers, + ok ? null : $"Liste leer/fehlerhaft – KnownContainers. {error}"); } - return await GetContainersViaHttpAsync(cancellationToken); + return (false, [], error ?? "Keine Container gefunden."); } - /// - /// Startet den Container neu. - /// public async Task<(bool Success, string Status, string? Detail)> RestartContainerAsync( string containerName, CancellationToken cancellationToken = default) - { - if (UsesMfApi) - { - return await RestartViaMfApiAsync(containerName, cancellationToken); - } - - if (UsesScripts) - { - if (Mode == SonicManagementMode.LocalCmd) - { - (bool binOk, string binStatus, string? binDetail) = - await new SonicBinRestartExecutor(_connection) - .RestartAsync(containerName, cancellationToken); - - if (binOk) - { - return (true, binStatus, binDetail); - } - - // Reine SMC/Client-Installation ohne Server-bin → wie die Console selbst - // über ConnectionUrl + SMC-Login (MfApi) neu starten. - bool batsMissing = binDetail?.Contains("stopcontainer", StringComparison.OrdinalIgnoreCase) == true - || binStatus.Contains("SonicBin", StringComparison.OrdinalIgnoreCase); - - if (batsMissing) - { - SonicMfApiExecutor mf = new(_connection); - (bool mfOk, string? mfOut, string? mfErr) = - await mf.RestartAsync(containerName, cancellationToken); - if (mfOk) - { - await Task.Delay( - TimeSpan.FromSeconds(Math.Clamp(_connection.PostRestartDelaySeconds, 0, 120)), - cancellationToken); - return (true, $"Container '{containerName}' neugestartet (SMC/MfApi)", - $"Kein stopcontainer.bat unter SonicHome – SMC-Verbindung genutzt.\n{mfOut}"); - } - - return (false, "Neustart fehlgeschlagen", - "Ursache: Unter SonicHome fehlen stopcontainer.bat/startcontainer.bat.\n" + - "Das ist typisch, wenn nur die Sonic Management Console (Client) installiert ist –\n" + - "die Scripts liegen auf dem Sonic-SERVER.\n\n" + - $"SonicBin: {binDetail}\n\n" + - $"SMC/MfApi-Fallback: {mfErr}\n{mfOut}\n\n" + - "Was tun:\n" + - "1) SonicHome auf den Server-Pfad setzen (Ordner mit bin\\stopcontainer.bat), ODER\n" + - "2) Java + Client-JARs unter SonicHome\\lib bereitstellen (wie SMC),\n" + - " ConnectionUrl/User/Pass = dieselben Werte wie beim SMC-Login."); - } - - return (false, binStatus, binDetail); - } - - return await RestartViaScriptAsync(containerName, cancellationToken); - } - - return await RestartViaHttpAsync(containerName, cancellationToken); - } - - /// - /// Importiert XApi-Ressourcen und startet den Container neu. - /// - public async Task<(bool Success, string Status, string? Detail)> ImportXapiAndRestartAsync( - string containerName, - string xapiSourcePath, - CancellationToken cancellationToken = default) - { - if (UsesMfApi) - { - // XApi-Import bleibt script/HTTP; Neustart danach über MfApi. - if (!string.IsNullOrWhiteSpace(_connection.WinRmXapiImportScript)) - { - WinRmExecutor local = new(CloneAsLocalCmd(_connection)); - string script = WinRmExecutor.ApplyScriptTemplate( - _connection.WinRmXapiImportScript, - containerName, - _connection.DomainName, - _connection.SonicHome, - xapiSourcePath, - _connection.ConnectionUrl, - _connection.Username, - _connection.Password); - (bool importOk, string? importOut, string? importErr) = - await local.RunScriptAsync(script, cancellationToken); - if (!importOk) - { - return (false, "XApi-Import fehlgeschlagen", importErr ?? importOut); - } - - (bool restartOk, string restartStatus, string? restartDetail) = - await RestartViaMfApiAsync(containerName, cancellationToken); - return restartOk - ? (true, "XApi importiert + Container neugestartet (MfApi)", - $"Import: {importOut} | Restart: {restartDetail}") - : (false, restartStatus, restartDetail); - } - - return (false, "XApi-Import fehlgeschlagen", - "Im MfApi-Modus ist WinRmXapiImportScript für den Import nötig, " + - "oder ManagementMode vorübergehend auf LocalCmd/WinRm setzen."); - } - - if (UsesScripts) - { - return await ImportXapiViaScriptAsync(containerName, xapiSourcePath, cancellationToken); - } - - return await ImportXapiViaHttpAsync(containerName, xapiSourcePath, cancellationToken); - } - - // --------------------------------------------------------------- - // MfApi (Sonic Domain Manager / IAgentProxy.restart) - // --------------------------------------------------------------- - - private async Task<(bool, IReadOnlyList, string?)> 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); + await _mfApi.RestartAsync(containerName, cancellationToken); if (ok) { @@ -342,26 +69,12 @@ public sealed class SonicManagementClient : IDisposable cancellationToken); return (true, $"Container '{containerName}' neugestartet (MfApi)", - $"Domain={_connection.DomainName}; ConnectionUrl={_connection.ConnectionUrl}\n{output}"); + $"Domain={_connection.DomainName}; URL={_connection.ConnectionUrl}\n{output}"); } - string libHint = string.IsNullOrWhiteSpace(_connection.MfClientLibPath) - ? _connection.SonicHome - : _connection.MfClientLibPath; - return (false, "Neustart fehlgeschlagen (MfApi)", - "Laut Doku (Management Application API / wie SMC-Restart):\n" + - "JMSConnectorClient + MFProxyFactory.createAgentProxy + IAgentProxy.restart\n\n" + $"Container '{containerName}', Domain '{_connection.DomainName}', URL {_connection.ConnectionUrl}\n" + - $"Fehler: {error}\nAusgabe: {output}\n\n" + - "Benötigt (SMC-Installation):\n" + - $"- MfClientLibPath/SonicHome={libHint} (mgmt_client.jar, mfcontext.jar, sonic_Client.jar)\n" + - $"- JavaPath={_connection.JavaPath}\n" + - "- Dieselben ConnectionUrl/User/Pass wie beim SMC-Login\n" + - "SMC-ObjectName Beispiel: proalpha-test.ct-ZADBService:ID=AGENT → ContainerName=ct-ZADBService\n" + - "Hinweis: Bei 'unbounded client connector' nutzt das Tool MBean stop/restart (wie SMC),\n" + - "nicht nur IAgentProxy.restart.\n" + - "Hinweis: stopcontainer.bat wird nicht verwendet (SMC-only)."); + $"Fehler: {error}\n\n{output}"); } private static async Task IsTcpReachableAsync(string connectionUrl, CancellationToken cancellationToken) @@ -369,12 +82,10 @@ public sealed class SonicManagementClient : IDisposable try { Uri uri = new(connectionUrl); - string host = uri.Host; - int port = uri.Port > 0 ? uri.Port : 2506; using System.Net.Sockets.TcpClient tcp = new(); using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(TimeSpan.FromSeconds(5)); - await tcp.ConnectAsync(host, port, cts.Token); + await tcp.ConnectAsync(uri.Host, uri.Port > 0 ? uri.Port : 2506, cts.Token); return true; } catch @@ -383,390 +94,11 @@ public sealed class SonicManagementClient : IDisposable } } - private static SonicConnection CloneAsLocalCmd(SonicConnection source) - => new() - { - Name = source.Name, - DomainName = source.DomainName, - ConnectionUrl = source.ConnectionUrl, - Username = source.Username, - Password = source.Password, - WinRmUsername = source.WinRmUsername, - WinRmPassword = source.WinRmPassword, - ManagementMode = SonicManagementMode.LocalCmd, - SonicHome = source.SonicHome, - JavaHome = source.JavaHome, - JavaPath = source.JavaPath, - MfClientLibPath = source.MfClientLibPath, - KnownContainers = source.KnownContainers, - ManagementHttpPort = source.ManagementHttpPort, - ApiBasePath = source.ApiBasePath, - ContainerListPath = source.ContainerListPath, - ContainerRestartPath = source.ContainerRestartPath, - ContainerStopPath = source.ContainerStopPath, - ContainerStartPath = source.ContainerStartPath, - WinRmPort = source.WinRmPort, - WinRmRestartScript = source.WinRmRestartScript, - WinRmContainerListScript = source.WinRmContainerListScript, - WinRmXapiImportScript = source.WinRmXapiImportScript, - TimeoutSeconds = source.TimeoutSeconds, - PostRestartDelaySeconds = source.PostRestartDelaySeconds - }; - - // --------------------------------------------------------------- - // Script-Implementierungen (WinRM / LocalCmd) - // --------------------------------------------------------------- - - private async Task<(bool, IReadOnlyList, string?)> GetContainersViaScriptAsync( - CancellationToken cancellationToken) - { - string script = WinRmExecutor.ApplyScriptTemplate( - _connection.ResolveContainerListScript(), - containerName: string.Empty, - domainName: _connection.DomainName, - sonicHome: _connection.SonicHome, - connectionUrl: _connection.ConnectionUrl, - username: _connection.Username, - password: _connection.Password); - - (bool ok, string? output, string? error) = - await _scriptRunner!.RunScriptAsync(script, cancellationToken); - - if (!ok) - { - return (false, [], $"Container-Liste fehlgeschlagen ({Mode}): {error}"); - } - - List names = (output ?? string.Empty) - .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - .Where(l => l.Length > 0) - .Where(l => !l.StartsWith("WARNING:", StringComparison.OrdinalIgnoreCase)) - .ToList(); - - // INFO:/WARN:-Zeilen aus dem Script bewusst durchreichen (Diagnose in Discovery) - return (true, names, null); - } - - private async Task<(bool, string, string?)> RestartViaScriptAsync( - string containerName, CancellationToken cancellationToken) - { - string script = WinRmExecutor.ApplyScriptTemplate( - _connection.ResolveRestartScript(), - containerName, - _connection.DomainName, - _connection.SonicHome, - connectionUrl: _connection.ConnectionUrl, - username: _connection.Username, - password: _connection.Password); - - (bool ok, string? output, string? error) = - await _scriptRunner!.RunScriptAsync(script, cancellationToken); - - string mode = Mode.ToString(); - bool verified = (output ?? string.Empty) - .Contains("OK:ContainerRestartVerified", StringComparison.OrdinalIgnoreCase); - - if (!ok || !verified) - { - return (false, $"Neustart fehlgeschlagen ({mode})", - $"Kein verifizierter Prozess-Neustart für Container '{containerName}' " + - $"(Domain '{_connection.DomainName}').\n" + - $"Fehler: {error}\nAusgabe: {output}\nSonicHome={_connection.SonicHome}"); - } - - await Task.Delay( - TimeSpan.FromSeconds(Math.Clamp(_connection.PostRestartDelaySeconds, 0, 120)), - cancellationToken); - - return (true, $"Container '{containerName}' neugestartet ({mode})", - $"Domain={_connection.DomainName}; verifiziert.\n{output}"); - } - - private async Task<(bool, string, string?)> ImportXapiViaScriptAsync( - string containerName, string xapiSourcePath, CancellationToken cancellationToken) - { - if (string.IsNullOrWhiteSpace(_connection.WinRmXapiImportScript)) - { - return (false, "XApi-Import fehlgeschlagen", - "WinRmXapiImportScript ist nicht konfiguriert."); - } - - string script = WinRmExecutor.ApplyScriptTemplate( - _connection.WinRmXapiImportScript, - containerName, - _connection.DomainName, - _connection.SonicHome, - xapiSourcePath, - _connection.ConnectionUrl, - _connection.Username, - _connection.Password); - - (bool importOk, string? importOut, string? importErr) = - await _scriptRunner!.RunScriptAsync(script, cancellationToken); - - if (!importOk) - { - return (false, "XApi-Import fehlgeschlagen", importErr ?? importOut); - } - - (bool restartOk, string restartStatus, string? restartDetail) = - await RestartViaScriptAsync(containerName, cancellationToken); - - return restartOk - ? (true, "XApi importiert + Container neugestartet", - $"Import: {importOut} | Restart: {restartDetail}") - : (false, restartStatus, restartDetail); - } - - // --------------------------------------------------------------- - // HTTP-Implementierungen - // --------------------------------------------------------------- - - private async Task<(bool, IReadOnlyList, string?)> GetContainersViaHttpAsync( - CancellationToken cancellationToken) - { - try - { - string? basePath = await ResolveContainerBasePathAsync(cancellationToken); - if (basePath is null) - { - return (false, [], "Kein HTTP-API-Pfad gefunden. ManagementMode auf 'WinRm' setzen?"); - } - - using HttpResponseMessage response = await _http!.GetAsync(basePath, cancellationToken); - if (!response.IsSuccessStatusCode) - { - return (false, [], $"HTTP {(int)response.StatusCode}: {response.ReasonPhrase}"); - } - - string json = await response.Content.ReadAsStringAsync(cancellationToken); - return (true, ParseContainerNames(json), null); - } - catch (Exception ex) when (ex is not OperationCanceledException) - { - return (false, [], ex.Message); - } - } - - private async Task<(bool, string, string?)> RestartViaHttpAsync( - string containerName, CancellationToken cancellationToken) - { - string encoded = Uri.EscapeDataString(containerName); - - try - { - string? basePath = await ResolveContainerBasePathAsync(cancellationToken); - if (basePath is null) - { - return (false, "Neustart fehlgeschlagen", - "Kein HTTP-API-Pfad gefunden. ManagementMode auf 'WinRm' setzen."); - } - - if (!string.IsNullOrWhiteSpace(_connection.ContainerRestartPath)) - { - string customPath = ApplyTemplate(_connection.ContainerRestartPath, encoded); - (bool ok, _, string? d) = await PostAsync(customPath, cancellationToken); - if (ok) { await DelayAsync(cancellationToken); return (true, "Container neugestartet", d); } - } - - (bool r1Ok, _, string? r1d) = await PostAsync($"{basePath}/{encoded}/restart", cancellationToken); - if (r1Ok) { await DelayAsync(cancellationToken); return (true, "Container neugestartet", r1d); } - - (bool r2Ok, _, string? r2d) = await PutStateAsync(basePath, encoded, "running", cancellationToken); - if (r2Ok) { await DelayAsync(cancellationToken); return (true, "Container neugestartet (State-API)", r2d); } - - return await StopThenStartAsync(basePath, encoded, cancellationToken); - } - catch (OperationCanceledException) { throw; } - catch (Exception ex) { return (false, "Neustart fehlgeschlagen", ex.Message); } - } - - private async Task<(bool, string, string?)> ImportXapiViaHttpAsync( - string containerName, string xapiSourcePath, CancellationToken cancellationToken) - { - if (!File.Exists(xapiSourcePath)) - { - return (false, "XApi-Import fehlgeschlagen", $"Quelldatei nicht gefunden: {xapiSourcePath}"); - } - - string encoded = Uri.EscapeDataString(containerName); - string? basePath = await ResolveContainerBasePathAsync(cancellationToken); - if (basePath is null) return (false, "XApi-Import fehlgeschlagen", "Kein HTTP-API-Pfad."); - - string path = $"{basePath}/{encoded}/xapi/import"; - await using FileStream fs = File.OpenRead(xapiSourcePath); - string mt = xapiSourcePath.EndsWith(".zip", StringComparison.OrdinalIgnoreCase) ? "application/zip" : "application/xml"; - using StreamContent content = new(fs); - content.Headers.ContentType = new MediaTypeHeaderValue(mt); - using HttpResponseMessage rsp = await _http!.PostAsync(path, content, cancellationToken); - - if (!rsp.IsSuccessStatusCode) - { - string body = await rsp.Content.ReadAsStringAsync(cancellationToken); - return (false, "XApi-Import fehlgeschlagen", $"HTTP {(int)rsp.StatusCode}: {Truncate(body)}"); - } - - (bool restartOk, string rs, string? rd) = await RestartViaHttpAsync(containerName, cancellationToken); - return restartOk - ? (true, "XApi importiert + Container neugestartet", rd) - : (false, rs, rd); - } - - // --------------------------------------------------------------- - // HTTP-Pfad-Erkennung - // --------------------------------------------------------------- - - private async Task ResolveContainerBasePathAsync(CancellationToken cancellationToken) - { - if (_resolvedContainerBasePath is not null) return _resolvedContainerBasePath; - - foreach (string p in GetCandidatePaths()) - { - try - { - using HttpResponseMessage r = await _http!.GetAsync(p, cancellationToken); - if (r.IsSuccessStatusCode || r.StatusCode == HttpStatusCode.Unauthorized) - { - _resolvedContainerBasePath = p; - return p; - } - } - catch (HttpRequestException) { } - catch (TaskCanceledException) when (!cancellationToken.IsCancellationRequested) { } - } - - return null; - } - - private IEnumerable GetCandidatePaths() - { - if (!string.IsNullOrWhiteSpace(_connection.ContainerListPath)) - yield return ApplyTemplate(_connection.ContainerListPath, string.Empty).TrimEnd('/'); - - if (!string.IsNullOrWhiteSpace(_connection.ApiBasePath)) - { - string d = Uri.EscapeDataString(_connection.DomainName); - yield return $"{_connection.ApiBasePath.TrimEnd('/')}/domains/{d}/containers"; - } - - foreach (string pattern in CandidateContainerPaths) - yield return pattern.Replace("{domain}", Uri.EscapeDataString(_connection.DomainName), StringComparison.OrdinalIgnoreCase); - } - - // --------------------------------------------------------------- - // HTTP-Aktions-Helfer - // --------------------------------------------------------------- - - private async Task<(bool, string, string?)> PostAsync(string path, CancellationToken ct) - { - using HttpResponseMessage r = await _http!.PostAsync(path, null, ct); - if (r.IsSuccessStatusCode) - return (true, $"OK ({(int)r.StatusCode})", $"POST {path} → {(int)r.StatusCode}"); - string body = await r.Content.ReadAsStringAsync(ct); - return (false, $"Fehler ({(int)r.StatusCode})", $"POST {path} → {(int)r.StatusCode}: {Truncate(body)}"); - } - - private async Task<(bool, string, string?)> PutStateAsync( - string basePath, string encoded, string state, CancellationToken ct) - { - string path = $"{basePath}/{encoded}"; - using StringContent body = new($"{{\"state\":\"{state}\"}}", Encoding.UTF8, "application/json"); - using HttpResponseMessage r = await _http!.PutAsync(path, body, ct); - if (r.IsSuccessStatusCode) - return (true, $"State={state}", $"PUT {path} state={state} → {(int)r.StatusCode}"); - string b = await r.Content.ReadAsStringAsync(ct); - return (false, "State fehlgeschlagen", $"PUT {path} → {(int)r.StatusCode}: {Truncate(b)}"); - } - - private async Task<(bool, string, string?)> StopThenStartAsync( - string basePath, string encoded, CancellationToken ct) - { - string stopPath = string.IsNullOrWhiteSpace(_connection.ContainerStopPath) - ? $"{basePath}/{encoded}/stop" - : ApplyTemplate(_connection.ContainerStopPath, encoded); - - string startPath = string.IsNullOrWhiteSpace(_connection.ContainerStartPath) - ? $"{basePath}/{encoded}/start" - : ApplyTemplate(_connection.ContainerStartPath, encoded); - - (bool sOk, _, string? sd) = await PostAsync(stopPath, ct); - if (!sOk) return (false, "Container-Stop fehlgeschlagen", sd); - - await Task.Delay(TimeSpan.FromSeconds(3), ct); - - (bool stOk, _, string? std) = await PostAsync(startPath, ct); - if (!stOk) return (false, "Container-Start fehlgeschlagen", std); - - await DelayAsync(ct); - return (true, "Container neugestartet (Stop+Start)", $"{sd} | {std}"); - } - - private async Task DelayAsync(CancellationToken ct) - { - int d = Math.Clamp(_connection.PostRestartDelaySeconds, 0, 120); - if (d > 0) await Task.Delay(TimeSpan.FromSeconds(d), ct); - } - - private string ApplyTemplate(string template, string encodedName) - => template - .Replace("{domain}", Uri.EscapeDataString(_connection.DomainName), StringComparison.OrdinalIgnoreCase) - .Replace("{container}", encodedName, StringComparison.OrdinalIgnoreCase); - - private static Uri BuildHttpBaseUri(string connectionUrl, int httpPort) - { - try { return new Uri($"http://{new Uri(connectionUrl).Host}:{httpPort}"); } - catch { return new Uri(connectionUrl); } - } - - private static string ExtractHost(string connectionUrl) - { - try { return new Uri(connectionUrl).Host; } - catch { return connectionUrl; } - } - - private static List ParseContainerNames(string json) - { - try - { - using JsonDocument doc = JsonDocument.Parse(json); - JsonElement root = doc.RootElement; - List names = []; - - IEnumerable elements = root.ValueKind == JsonValueKind.Array - ? root.EnumerateArray() - : root.ValueKind == JsonValueKind.Object - ? new[] { "containers", "data", "items", "result" } - .Where(k => root.TryGetProperty(k, out _)) - .SelectMany(k => - { - root.TryGetProperty(k, out JsonElement a); - return a.ValueKind == JsonValueKind.Array - ? a.EnumerateArray() - : Enumerable.Empty(); - }) - : []; - - foreach (JsonElement el in elements) - { - string? name = el.ValueKind == JsonValueKind.String - ? el.GetString() - : new[] { "name", "containerName", "id", "configId" } - .Where(k => el.TryGetProperty(k, out _)) - .Select(k => { el.TryGetProperty(k, out JsonElement p); return p.GetString(); }) - .FirstOrDefault(); - if (name is not null) names.Add(name); - } - - return names; - } - catch { return []; } - } - - private static string Truncate(string v, int max = 400) - => v.Length <= max ? v : v[..max] + "…"; + private static string Truncate(string? s, int max = 180) + => string.IsNullOrWhiteSpace(s) ? string.Empty + : s.Length <= max ? s : s[..max] + "…"; public void Dispose() { - _http?.Dispose(); } } diff --git a/ZA.CoreService.ESBCertificateManager/Services/TlsCertificateProbe.cs b/ZA.CoreService.ESBCertificateManager/Services/TlsCertificateProbe.cs deleted file mode 100644 index 8d808c6..0000000 --- a/ZA.CoreService.ESBCertificateManager/Services/TlsCertificateProbe.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System.Net.Security; -using System.Net.Sockets; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using ZA.CoreService.ESBCertificateManager.Models; - -namespace ZA.CoreService.ESBCertificateManager.Services; - -public sealed class TlsCertificateProbe -{ - private readonly int _timeoutSeconds; - private readonly int _retryCount; - - public TlsCertificateProbe(int timeoutSeconds = 8, int retryCount = 2) - { - _timeoutSeconds = Math.Clamp(timeoutSeconds, 1, 60); - _retryCount = Math.Clamp(retryCount, 0, 5); - } - - public async Task<(bool Success, string Status, string? Detail, string? ObservedFingerprint)> ProbeAsync( - DeploymentTarget target, - string expectedFingerprintSha256, - CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(target.TlsHost)) - { - return (true, "TLS übersprungen", "Kein TlsHost konfiguriert.", null); - } - - int port = target.TlsPort is > 0 and <= 65535 ? target.TlsPort.Value : 443; - - // TlsServerName überschreibt den SNI-Hostnamen wenn gesetzt (wichtig bei IP-Adressen) - string serverName = string.IsNullOrWhiteSpace(target.TlsServerName) - ? target.TlsHost - : target.TlsServerName; - - string expected = NormalizeFingerprint(expectedFingerprintSha256); - - Exception? lastError = null; - - for (int attempt = 0; attempt <= _retryCount; attempt++) - { - cancellationToken.ThrowIfCancellationRequested(); - - try - { - return await ProbeOnceAsync( - target.TlsHost, - port, - serverName, - expected, - cancellationToken); - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) - { - lastError = ex; - if (attempt < _retryCount) - { - await Task.Delay(400, cancellationToken); - } - } - } - - return ( - false, - "TLS nicht erreichbar", - lastError?.Message ?? $"Keine Verbindung zu {target.TlsHost}:{port}", - null); - } - - private async Task<(bool Success, string Status, string? Detail, string? ObservedFingerprint)> ProbeOnceAsync( - string host, - int port, - string serverName, - string expectedFingerprint, - CancellationToken cancellationToken) - { - using TcpClient client = new(); - using CancellationTokenSource timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - timeoutCts.CancelAfter(TimeSpan.FromSeconds(_timeoutSeconds)); - - await client.ConnectAsync(host, port, timeoutCts.Token); - - await using SslStream sslStream = new( - client.GetStream(), - leaveInnerStreamOpen: false, - userCertificateValidationCallback: static (_, _, _, _) => true); - - await sslStream.AuthenticateAsClientAsync( - new SslClientAuthenticationOptions - { - TargetHost = serverName, // SNI: muss zum CN/SAN im Zertifikat passen - EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls12 - | System.Security.Authentication.SslProtocols.Tls13 - }, - timeoutCts.Token); - - if (sslStream.RemoteCertificate is null) - { - return (false, "TLS Fail", "Kein Remote-Zertifikat erhalten.", null); - } - - using X509Certificate2 remote = new(sslStream.RemoteCertificate); - string actual = Convert.ToHexString(SHA256.HashData(remote.RawData)); - - if (string.IsNullOrEmpty(expectedFingerprint)) - { - // Kein erwarteter Fingerprint konfiguriert – nur Konnektivität prüfen - return (true, "TLS Pass (kein Fingerprint-Vergleich)", $"{host}:{port} erreichbar. Fingerprint={actual}", actual); - } - - if (string.Equals(actual, expectedFingerprint, StringComparison.OrdinalIgnoreCase)) - { - return (true, "TLS Pass", $"{host}:{port} Fingerprint stimmt überein.", actual); - } - - return ( - false, - "TLS Fail", - $"{host}:{port} Fingerprint weicht ab. Erwartet={expectedFingerprint}, Ist={actual}", - actual); - } - - private static string NormalizeFingerprint(string fingerprint) - { - return fingerprint - .Replace(":", string.Empty, StringComparison.Ordinal) - .Replace(" ", string.Empty, StringComparison.Ordinal) - .ToUpperInvariant(); - } -} diff --git a/ZA.CoreService.ESBCertificateManager/Services/WinRmExecutor.cs b/ZA.CoreService.ESBCertificateManager/Services/WinRmExecutor.cs deleted file mode 100644 index bd9def5..0000000 --- a/ZA.CoreService.ESBCertificateManager/Services/WinRmExecutor.cs +++ /dev/null @@ -1,268 +0,0 @@ -using System.Diagnostics; -using System.Text; -using ZA.CoreService.ESBCertificateManager.Models; - -namespace ZA.CoreService.ESBCertificateManager.Services; - -/// -/// Führt PowerShell-Befehle lokal oder via WinRM (Invoke-Command) auf dem Sonic-Server aus. -/// Schreibt Skripte als .ps1 (UTF-8 ohne BOM) und startet sie mit -File, -/// um den bekannten stdin/BOM-Fehler zu vermeiden -/// ("$ErrorActionPreference wurde nicht als Name eines Cmdlet erkannt"). -/// -public sealed class WinRmExecutor -{ - private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); - - private readonly SonicConnection _connection; - - public WinRmExecutor(SonicConnection connection) - { - _connection = connection; - } - - private bool HasExplicitWinRmCredentials - => !string.IsNullOrWhiteSpace(_connection.WinRmUsername); - - public async Task<(bool Success, string? Error)> TestConnectionAsync( - CancellationToken cancellationToken = default) - { - if (_connection.EffectiveManagementMode == 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, sessionError ?? "WinRM-Verbindung fehlgeschlagen."); - } - - public async Task<(bool Success, string? Output, string? Error)> RunScriptAsync( - string scriptBlock, - CancellationToken cancellationToken = default) - { - string fullScript = _connection.EffectiveManagementMode == SonicManagementMode.LocalCmd - ? BuildLocalScript(scriptBlock) - : BuildRemoteScript(ExtractHost(_connection.ConnectionUrl), scriptBlock); - - string tempFile = Path.Combine( - Path.GetTempPath(), - $"esb-winrm-{Guid.NewGuid():N}.ps1"); - - await File.WriteAllTextAsync(tempFile, fullScript, Utf8NoBom, cancellationToken); - - try - { - ProcessStartInfo psi = new() - { - FileName = "powershell.exe", - Arguments = "-NonInteractive -NoProfile -ExecutionPolicy Bypass -File \"" + tempFile + "\"", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - StandardOutputEncoding = Encoding.UTF8, - StandardErrorEncoding = Encoding.UTF8 - }; - - using Process process = new() { StartInfo = psi }; - - if (!process.Start()) - { - return (false, null, "PowerShell-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, $"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); - } - - string rawError = stderr.Length > 0 - ? stderr - : $"PowerShell ExitCode={process.ExitCode}"; - - string error = _connection.EffectiveManagementMode == SonicManagementMode.WinRm - ? FormatWinRmFailure(rawError) - : Truncate(rawError); - - return (false, stdout.Length > 0 ? stdout : null, error); - } - finally - { - try { File.Delete(tempFile); } catch { /* ignore */ } - } - } - - private static string BuildLocalScript(string scriptBlock) - => "$ErrorActionPreference = 'Stop'\n" + scriptBlock; - - /// - /// Remote-Payload als Base64 einbetten und per [scriptblock]::Create ausführen. - /// Vermeidet verschachteltes ScriptBlock-Brace-Nesting und lokale $-Expansion. - /// WinRM-Credentials: nur / WinRmPassword. - /// Leer = aktueller Windows-Benutzer (ohne -Credential). - /// - private string BuildRemoteScript(string host, string scriptBlock) - { - string escapedHost = host.Replace("'", "''"); - string remoteB64 = Convert.ToBase64String(Encoding.Unicode.GetBytes(scriptBlock)); - - StringBuilder sb = new(); - sb.Append("$ErrorActionPreference = 'Stop'\n"); - - if (HasExplicitWinRmCredentials) - { - string escapedPwd = (_connection.WinRmPassword ?? string.Empty).Replace("'", "''"); - string escapedUser = _connection.WinRmUsername.Replace("'", "''"); - sb.Append($"$secPwd = ConvertTo-SecureString '{escapedPwd}' -AsPlainText -Force\n"); - sb.Append($"$cred = New-Object System.Management.Automation.PSCredential('{escapedUser}', $secPwd)\n"); - } - - sb.Append($"$remoteB64 = '{remoteB64}'\n"); - sb.Append("$remoteScript = [System.Text.Encoding]::Unicode.GetString("); - sb.Append("[System.Convert]::FromBase64String($remoteB64))\n"); - sb.Append("$sb = [scriptblock]::Create($remoteScript)\n"); - sb.Append($"Invoke-Command -ComputerName '{escapedHost}' -Port {_connection.WinRmPort} "); - - if (HasExplicitWinRmCredentials) - { - sb.Append("-Credential $cred "); - } - - sb.Append("-ScriptBlock $sb -ErrorAction Stop"); - return sb.ToString(); - } - - /// - /// Erkennt typische WinRM-/Windows-Auth-Fehler und liefert eine klare Handlungsanweisung. - /// Username/Password in appsettings sind Sonic-SMC – nicht Windows/WinRM. - /// - private string FormatWinRmFailure(string rawError) - { - string truncated = Truncate(rawError); - if (!LooksLikeWinRmAuthFailure(rawError)) - { - return truncated; - } - - string authHint = HasExplicitWinRmCredentials - ? "WinRmUsername/WinRmPassword prüfen (Windows-Konto mit WinRM-Rechten auf dem Zielrechner)." - : "Aktueller Windows-Benutzer hat keine WinRM-Berechtigung auf dem Zielrechner " + - "(oder Kerberos/CredSSP fehlt). WinRmUsername/WinRmPassword setzen " + - "oder App unter einem berechtigten Windows-Konto starten."; - - return - "WinRM-Authentifizierung fehlgeschlagen: Windows-Anmeldedaten falsch oder fehlend.\n" + - "Hinweis: Username/Password in appsettings sind Sonic-SMC-/Domain-Manager-Logins – " + - "NICHT für WinRM/Windows.\n" + - authHint + "\n" + - "Alternativen:\n" + - " • ManagementMode=LocalCmd setzen und die App direkt auf dem Sonic-Server starten (kein WinRM).\n" + - " • WinRmUsername/WinRmPassword mit gültigem Windows-Konto befüllen.\n" + - $"Details: {truncated}"; - } - - private static bool LooksLikeWinRmAuthFailure(string error) - { - if (string.IsNullOrEmpty(error)) - { - return false; - } - - ReadOnlySpan markers = - [ - "Benutzername oder das Kennwort ist falsch", - "username or password is incorrect", - "Access is denied", - "Zugriff verweigert", - "Logon failure", - "Anmeldefehler", - "PSRemotingTransportException", - "UnauthorizedAccess", - "WinRM cannot process the request", - "der remotecomputer hat den netzwerkdatenverkehr verweigert" - ]; - - foreach (string marker in markers) - { - if (error.Contains(marker, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; - } - - public static string ApplyScriptTemplate( - string template, - string containerName, - string domainName = "", - string sonicHome = "", - string xapiPath = "", - string connectionUrl = "", - string username = "", - string password = "") - => 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) - .Replace("{connectionUrl}", connectionUrl.Replace("'", "''"), StringComparison.OrdinalIgnoreCase) - .Replace("{username}", username.Replace("'", "''"), StringComparison.OrdinalIgnoreCase) - .Replace("{password}", password.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] + "…"; -} diff --git a/ZA.CoreService.ESBCertificateManager/Sql/001_CreateSchema.sql b/ZA.CoreService.ESBCertificateManager/Sql/001_CreateSchema.sql deleted file mode 100644 index 7f0e864..0000000 --- a/ZA.CoreService.ESBCertificateManager/Sql/001_CreateSchema.sql +++ /dev/null @@ -1,157 +0,0 @@ -/* - ESB Certificate Manager – Zielkonfiguration (SQL Server) - Offline vorbereitet; zur späteren Nutzung auf dem Ziel-SQL-Server ausführen. -*/ - -IF OBJECT_ID(N'dbo.SonicConnection', N'U') IS NULL -BEGIN - CREATE TABLE dbo.SonicConnection - ( - Id INT NOT NULL IDENTITY(1, 1), - Name NVARCHAR(128) NOT NULL, -- Eindeutiger Bezeichner, referenziert von DeploymentTarget - DomainName NVARCHAR(128) NOT NULL, -- Sonic-Domain-Name, z.B. "proalpha-test" - ManagementUrl NVARCHAR(512) NOT NULL, -- HTTP-URL der Management Console, z.B. "http://host:8080" - ApiBasePath NVARCHAR(128) NOT NULL CONSTRAINT DF_SonicConnection_ApiBasePath DEFAULT (N'/api/v1'), - Username NVARCHAR(128) NOT NULL, - -- Passwort wird in der Anwendung verschlüsselt gespeichert; hier nur als Verweis - PasswordHash NVARCHAR(512) NOT NULL CONSTRAINT DF_SonicConnection_PwdHash DEFAULT (N''), - TimeoutSeconds INT NOT NULL CONSTRAINT DF_SonicConnection_Timeout DEFAULT (30), - PostRestartDelaySecs INT NOT NULL CONSTRAINT DF_SonicConnection_Delay DEFAULT (15), - IsActive BIT NOT NULL CONSTRAINT DF_SonicConnection_IsActive DEFAULT (1), - CONSTRAINT PK_SonicConnection PRIMARY KEY CLUSTERED (Id), - CONSTRAINT UQ_SonicConnection_Name UNIQUE (Name) - ); -END -GO - -IF OBJECT_ID(N'dbo.DeploymentTarget', N'U') IS NULL -BEGIN - CREATE TABLE dbo.DeploymentTarget - ( - Id INT NOT NULL IDENTITY(1, 1), - Name NVARCHAR(128) NOT NULL, - Environment NVARCHAR(64) NOT NULL, - IsActive BIT NOT NULL CONSTRAINT DF_DeploymentTarget_IsActive DEFAULT (1), - TargetDirectory NVARCHAR(512) NOT NULL, - CertificateFileName NVARCHAR(260) NOT NULL, - ContainerName NVARCHAR(128) NOT NULL CONSTRAINT DF_DeploymentTarget_Container DEFAULT (N''), - RestartType NVARCHAR(32) NOT NULL CONSTRAINT DF_DeploymentTarget_RestartType DEFAULT (N'None'), - RestartCommand NVARCHAR(512) NOT NULL CONSTRAINT DF_DeploymentTarget_RestartCmd DEFAULT (N''), - RestartArguments NVARCHAR(1024) NOT NULL CONSTRAINT DF_DeploymentTarget_RestartArgs DEFAULT (N''), - RestartTimeoutSeconds INT NOT NULL CONSTRAINT DF_DeploymentTarget_RestartTimeout DEFAULT (60), - -- Sonic ESB Management - SonicConnectionName NVARCHAR(128) NOT NULL CONSTRAINT DF_DeploymentTarget_SonicConn DEFAULT (N''), - XapiSourcePath NVARCHAR(512) NOT NULL CONSTRAINT DF_DeploymentTarget_XapiSrc DEFAULT (N''), - -- TLS-Probe - TlsHost NVARCHAR(255) NOT NULL CONSTRAINT DF_DeploymentTarget_TlsHost DEFAULT (N''), - TlsPort INT NULL, - SortOrder INT NOT NULL CONSTRAINT DF_DeploymentTarget_SortOrder DEFAULT (0), - CONSTRAINT PK_DeploymentTarget PRIMARY KEY CLUSTERED (Id), - CONSTRAINT CK_DeploymentTarget_RestartType CHECK ( - RestartType IN (N'None', N'Command', N'SonicContainer', N'SonicContainerWithXapi') - ) - ); -END -ELSE -BEGIN - -- Neue Spalten zu bestehender Tabelle hinzufügen (idempotent) - IF NOT EXISTS (SELECT 1 FROM sys.columns - WHERE object_id = OBJECT_ID(N'dbo.DeploymentTarget') - AND name = N'SonicConnectionName') - BEGIN - ALTER TABLE dbo.DeploymentTarget - ADD SonicConnectionName NVARCHAR(128) NOT NULL - CONSTRAINT DF_DeploymentTarget_SonicConn DEFAULT (N''); - END - - IF NOT EXISTS (SELECT 1 FROM sys.columns - WHERE object_id = OBJECT_ID(N'dbo.DeploymentTarget') - AND name = N'XapiSourcePath') - BEGIN - ALTER TABLE dbo.DeploymentTarget - ADD XapiSourcePath NVARCHAR(512) NOT NULL - CONSTRAINT DF_DeploymentTarget_XapiSrc DEFAULT (N''); - END - - -- CHECK-Constraint um neue RestartType-Werte erweitern - IF EXISTS (SELECT 1 FROM sys.check_constraints - WHERE parent_object_id = OBJECT_ID(N'dbo.DeploymentTarget') - AND name = N'CK_DeploymentTarget_RestartType') - BEGIN - ALTER TABLE dbo.DeploymentTarget DROP CONSTRAINT CK_DeploymentTarget_RestartType; - ALTER TABLE dbo.DeploymentTarget ADD CONSTRAINT CK_DeploymentTarget_RestartType - CHECK (RestartType IN (N'None', N'Command', N'SonicContainer', N'SonicContainerWithXapi')); - END -END -GO - -IF OBJECT_ID(N'dbo.DeploymentRunHistory', N'U') IS NULL -BEGIN - CREATE TABLE dbo.DeploymentRunHistory - ( - Id BIGINT NOT NULL IDENTITY(1, 1), - StartedAtUtc DATETIME2(3) NOT NULL, - FinishedAtUtc DATETIME2(3) NULL, - UserName NVARCHAR(128) NOT NULL, - MachineName NVARCHAR(128) NOT NULL CONSTRAINT DF_DeploymentRunHistory_Machine DEFAULT (N''), - OverallSuccess BIT NULL, - Summary NVARCHAR(2000) NULL, - CONSTRAINT PK_DeploymentRunHistory PRIMARY KEY CLUSTERED (Id) - ); -END -ELSE -BEGIN - IF NOT EXISTS (SELECT 1 FROM sys.columns - WHERE object_id = OBJECT_ID(N'dbo.DeploymentRunHistory') - AND name = N'MachineName') - BEGIN - ALTER TABLE dbo.DeploymentRunHistory - ADD MachineName NVARCHAR(128) NOT NULL - CONSTRAINT DF_DeploymentRunHistory_Machine DEFAULT (N''); - END -END -GO - -IF OBJECT_ID(N'dbo.DeploymentRunDetail', N'U') IS NULL -BEGIN - CREATE TABLE dbo.DeploymentRunDetail - ( - Id BIGINT NOT NULL IDENTITY(1, 1), - RunId BIGINT NOT NULL, - TargetId INT NOT NULL, - TargetName NVARCHAR(128) NOT NULL, - Success BIT NOT NULL, - StatusText NVARCHAR(256) NOT NULL, - Detail NVARCHAR(2000) NULL, - Steps NVARCHAR(MAX) NULL, - CONSTRAINT PK_DeploymentRunDetail PRIMARY KEY CLUSTERED (Id), - CONSTRAINT FK_DeploymentRunDetail_Run - FOREIGN KEY (RunId) REFERENCES dbo.DeploymentRunHistory (Id) - ); -END -GO - -/* -Beispiel-Insert: - -INSERT INTO dbo.SonicConnection (Name, DomainName, ManagementUrl, Username, PasswordHash) -VALUES (N'DE-Test', N'proalpha-test', N'http://dekun-painwbdet:8080', N'Administrator', N''); - -INSERT INTO dbo.DeploymentTarget -( - Name, Environment, IsActive, TargetDirectory, CertificateFileName, - ContainerName, RestartType, SonicConnectionName, - TlsHost, TlsPort, SortOrder -) -VALUES -( - N'DE-Test Container A', N'TEST', 1, N'\\share\esb\certs\a', N'esb-cert.cer', - N'sonic-container-a', N'SonicContainer', N'DE-Test', - N'dekun-painwbdet', 13070, 10 -), -( - N'DE-Test Container B (XApi)', N'TEST', 1, N'\\share\esb\certs\b', N'esb-cert.cer', - N'sonic-container-b', N'SonicContainerWithXapi', N'DE-Test', - N'dekun-painwbdet', 13070, 20 -); -*/ diff --git a/ZA.CoreService.ESBCertificateManager/Sql/002_DeploymentRunModel.sql b/ZA.CoreService.ESBCertificateManager/Sql/002_DeploymentRunModel.sql deleted file mode 100644 index f94eac2..0000000 --- a/ZA.CoreService.ESBCertificateManager/Sql/002_DeploymentRunModel.sql +++ /dev/null @@ -1,193 +0,0 @@ -/* - ESB Certificate Manager – vollständiges Datenbankschema v2 - Idempotent; kann auf einem leeren Schema oder nach 001_CreateSchema.sql ausgeführt werden. - - Tabellen: - dbo.SonicConnection – Sonic-ESB-Management-Instanzen - dbo.DeploymentTargets – Deployment-Ziele mit allen Konfigurationsfeldern - dbo.DeploymentRuns – Ein Eintrag pro Deployment-Lauf - dbo.DeploymentTargetResults – Detailergebnis pro Ziel und Lauf - - Always Encrypted (optional): - Zur Nutzung von Always Encrypted auf der Spalte PasswordHash in SonicConnection - die Blöcke unterhalb des Kommentars "-- ALWAYS ENCRYPTED" auskommentieren - und den Schlüsselnamen anpassen. -*/ - --- ============================================================ --- SonicConnection --- ============================================================ -IF OBJECT_ID(N'dbo.SonicConnection', N'U') IS NULL -BEGIN - CREATE TABLE dbo.SonicConnection - ( - Id INT NOT NULL IDENTITY(1, 1), - Name NVARCHAR(128) NOT NULL, -- Eindeutiger Bezeichner, referenziert von DeploymentTargets - DomainName NVARCHAR(128) NOT NULL, -- Sonic-Domain, z.B. "proalpha-test" - ConnectionUrl NVARCHAR(512) NOT NULL, -- Sonic-Broker-URL, z.B. "tcp://dekun-painwbdet:13070" - ManagementHttpPort INT NOT NULL CONSTRAINT DF_SonicConnection_HttpPort DEFAULT (8080), - ApiBasePath NVARCHAR(128) NOT NULL CONSTRAINT DF_SonicConnection_ApiBase DEFAULT (N'/api/v1'), - Username NVARCHAR(128) NOT NULL, - PasswordHash NVARCHAR(512) NOT NULL CONSTRAINT DF_SonicConnection_PwdHash DEFAULT (N''), - -- ALWAYS ENCRYPTED: PasswordEncrypted NVARCHAR(512) ENCRYPTED WITH ( - -- COLUMN_ENCRYPTION_KEY = CEK_SonicPwd, - -- ENCRYPTION_TYPE = DETERMINISTIC, - -- ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256' - -- ) NULL, - TimeoutSeconds INT NOT NULL CONSTRAINT DF_SonicConnection_Timeout DEFAULT (30), - PostRestartDelaySecs INT NOT NULL CONSTRAINT DF_SonicConnection_Delay DEFAULT (15), - IsActive BIT NOT NULL CONSTRAINT DF_SonicConnection_Active DEFAULT (1), - CONSTRAINT PK_SonicConnection PRIMARY KEY CLUSTERED (Id), - CONSTRAINT UQ_SonicConnection_Name UNIQUE (Name) - ); -END -ELSE -BEGIN - -- ConnectionUrl-Spalte nachrüsten (Migration von ManagementUrl) - IF NOT EXISTS (SELECT 1 FROM sys.columns - WHERE object_id = OBJECT_ID(N'dbo.SonicConnection') AND name = N'ConnectionUrl') - BEGIN - ALTER TABLE dbo.SonicConnection ADD ConnectionUrl NVARCHAR(512) NOT NULL - CONSTRAINT DF_SonicConnection_ConnUrl DEFAULT (N''); - END - - IF NOT EXISTS (SELECT 1 FROM sys.columns - WHERE object_id = OBJECT_ID(N'dbo.SonicConnection') AND name = N'ManagementHttpPort') - BEGIN - ALTER TABLE dbo.SonicConnection ADD ManagementHttpPort INT NOT NULL - CONSTRAINT DF_SonicConnection_HttpPort DEFAULT (8080); - END -END -GO - --- ============================================================ --- DeploymentTargets --- ============================================================ -IF OBJECT_ID(N'dbo.DeploymentTargets', N'U') IS NULL -BEGIN - CREATE TABLE dbo.DeploymentTargets - ( - Id INT NOT NULL IDENTITY(1, 1), - Name NVARCHAR(100) NOT NULL, - Environment NVARCHAR(64) NOT NULL CONSTRAINT DF_DT_Env DEFAULT (N''), - IsActive BIT NOT NULL CONSTRAINT DF_DT_IsActive DEFAULT (1), - - -- Zertifikat-Ablage - CertificateTargetPath NVARCHAR(500) NOT NULL, - CertificateFileName NVARCHAR(260) NOT NULL CONSTRAINT DF_DT_CertFile DEFAULT (N''), - - -- Neustart-Konfiguration - RestartType NVARCHAR(30) NOT NULL CONSTRAINT DF_DT_RestartType DEFAULT (N'None'), - RestartHost NVARCHAR(255) NULL, - RestartCommand NVARCHAR(2000) NULL, - RestartArguments NVARCHAR(1024) NOT NULL CONSTRAINT DF_DT_RestartArgs DEFAULT (N''), - RestartTimeoutSeconds INT NOT NULL CONSTRAINT DF_DT_RestartTimeout DEFAULT (60), - - -- Sonic ESB - SonicConnectionName NVARCHAR(128) NOT NULL CONSTRAINT DF_DT_SonicConn DEFAULT (N''), - ContainerName NVARCHAR(128) NOT NULL CONSTRAINT DF_DT_Container DEFAULT (N''), - XapiSourcePath NVARCHAR(512) NOT NULL CONSTRAINT DF_DT_XapiSrc DEFAULT (N''), - - -- TLS-Probe - TlsHost NVARCHAR(255) NOT NULL CONSTRAINT DF_DT_TlsHost DEFAULT (N''), - TlsPort INT NOT NULL CONSTRAINT DF_DT_TlsPort DEFAULT (443), - TlsServerName NVARCHAR(255) NOT NULL CONSTRAINT DF_DT_TlsServerName DEFAULT (N''), - ExpectedFingerprint NVARCHAR(128) NULL, - - SortOrder INT NOT NULL CONSTRAINT DF_DT_SortOrder DEFAULT (0), - - CONSTRAINT PK_DeploymentTargets PRIMARY KEY CLUSTERED (Id), - CONSTRAINT CK_DeploymentTargets_RestartType CHECK ( - RestartType IN (N'None', N'Command', N'SonicContainer', N'SonicContainerWithXapi') - ) - ); -END -ELSE -BEGIN - -- Neue Spalten idempotent nachrüsten - IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'dbo.DeploymentTargets') AND name = N'TlsServerName') - ALTER TABLE dbo.DeploymentTargets ADD TlsServerName NVARCHAR(255) NOT NULL CONSTRAINT DF_DT_TlsServerName DEFAULT (N''); - - IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'dbo.DeploymentTargets') AND name = N'ExpectedFingerprint') - ALTER TABLE dbo.DeploymentTargets ADD ExpectedFingerprint NVARCHAR(128) NULL; - - IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'dbo.DeploymentTargets') AND name = N'SonicConnectionName') - ALTER TABLE dbo.DeploymentTargets ADD SonicConnectionName NVARCHAR(128) NOT NULL CONSTRAINT DF_DT_SonicConn DEFAULT (N''); - - IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'dbo.DeploymentTargets') AND name = N'ContainerName') - ALTER TABLE dbo.DeploymentTargets ADD ContainerName NVARCHAR(128) NOT NULL CONSTRAINT DF_DT_Container DEFAULT (N''); - - IF NOT EXISTS (SELECT 1 FROM sys.columns WHERE object_id = OBJECT_ID(N'dbo.DeploymentTargets') AND name = N'XapiSourcePath') - ALTER TABLE dbo.DeploymentTargets ADD XapiSourcePath NVARCHAR(512) NOT NULL CONSTRAINT DF_DT_XapiSrc DEFAULT (N''); -END -GO - --- ============================================================ --- DeploymentRuns (ein Datensatz pro Deployment-Lauf) --- ============================================================ -IF OBJECT_ID(N'dbo.DeploymentRuns', N'U') IS NULL -BEGIN - CREATE TABLE dbo.DeploymentRuns - ( - Id UNIQUEIDENTIFIER NOT NULL, - StartedAtUtc DATETIME2(3) NOT NULL, - FinishedAtUtc DATETIME2(3) NULL, - SourceFile NVARCHAR(500) NOT NULL, - SourceFingerprint NVARCHAR(128) NOT NULL, - StartedBy NVARCHAR(255) NOT NULL, - MachineName NVARCHAR(255) NOT NULL CONSTRAINT DF_DR_Machine DEFAULT (N''), - OverallStatus NVARCHAR(30) NOT NULL, -- 'Running' | 'Success' | 'PartialFailure' | 'Failure' - ErrorMessage NVARCHAR(MAX) NULL, - CONSTRAINT PK_DeploymentRuns PRIMARY KEY CLUSTERED (Id) - ); -END -GO - --- ============================================================ --- DeploymentTargetResults (ein Datensatz pro Ziel und Lauf) --- ============================================================ -IF OBJECT_ID(N'dbo.DeploymentTargetResults', N'U') IS NULL -BEGIN - CREATE TABLE dbo.DeploymentTargetResults - ( - Id INT NOT NULL IDENTITY(1, 1), - DeploymentRunId UNIQUEIDENTIFIER NOT NULL, - TargetId INT NOT NULL, - TargetName NVARCHAR(128) NOT NULL, - StartedAtUtc DATETIME2(3) NOT NULL, - FinishedAtUtc DATETIME2(3) NULL, - CopySucceeded BIT NOT NULL CONSTRAINT DF_DTR_Copy DEFAULT (0), - RestartSucceeded BIT NOT NULL CONSTRAINT DF_DTR_Restart DEFAULT (0), - TlsSucceeded BIT NOT NULL CONSTRAINT DF_DTR_Tls DEFAULT (0), - ObservedFingerprint NVARCHAR(128) NULL, - Status NVARCHAR(30) NOT NULL, - ErrorMessage NVARCHAR(MAX) NULL, - CONSTRAINT PK_DeploymentTargetResults PRIMARY KEY CLUSTERED (Id), - CONSTRAINT FK_DTR_Run FOREIGN KEY (DeploymentRunId) REFERENCES dbo.DeploymentRuns (Id) - ); - - CREATE NONCLUSTERED INDEX IX_DTR_RunId ON dbo.DeploymentTargetResults (DeploymentRunId); -END -GO - --- ============================================================ --- Beispieldaten --- ============================================================ -/* -INSERT INTO dbo.SonicConnection (Name, DomainName, ConnectionUrl, Username, PasswordHash) -VALUES (N'DE-Test', N'proalpha-test', N'tcp://dekun-painwbdet:13070', N'Administrator', N''); - -INSERT INTO dbo.DeploymentTargets - (Name, Environment, IsActive, CertificateTargetPath, CertificateFileName, - RestartType, SonicConnectionName, ContainerName, - TlsHost, TlsPort, TlsServerName, SortOrder) -VALUES - (N'DE-Test Container A', N'TEST', 1, - N'\\dekun-painwbdet\sonic\certs', N'server.pfx', - N'SonicContainer', N'DE-Test', N'sonic-container-a', - N'dekun-painwbdet', 443, N'esb-test.firma.local', 10), - (N'DE-Test Container B (XApi)', N'TEST', 1, - N'\\dekun-painwbdet\sonic\certs', N'server.pfx', - N'SonicContainerWithXapi', N'DE-Test', N'sonic-container-b', - N'dekun-painwbdet', 443, N'esb-test.firma.local', 20); -*/ diff --git a/ZA.CoreService.ESBCertificateManager/ZA.CoreService.ESBCertificateManager.csproj b/ZA.CoreService.ESBCertificateManager/ZA.CoreService.ESBCertificateManager.csproj index c4ae89c..9885c5f 100644 --- a/ZA.CoreService.ESBCertificateManager/ZA.CoreService.ESBCertificateManager.csproj +++ b/ZA.CoreService.ESBCertificateManager/ZA.CoreService.ESBCertificateManager.csproj @@ -9,7 +9,6 @@ - @@ -24,12 +23,6 @@ PreserveNewest - - PreserveNewest - - - PreserveNewest - Always