From 12eebfc78a733fab0ebfcdec91403ce40b83fcb8 Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 24 Jul 2026 11:06:10 +0200 Subject: [PATCH] Simplify app to certificate recognition and MfApi restart only. Remove XApi, WinRM, LocalCmd, HTTP API, TLS probe, SQL deploy model, and deploy UI. Co-authored-by: Cursor --- .../Data/SqlRunLogger.cs | 167 -- .../Data/SqlTargetRepository.cs | 111 - .../Data/TargetRepositoryFactory.cs | 32 +- .../Data/targets.sample.json | 31 - ZA.CoreService.ESBCertificateManager/Form1.cs | 2510 ++++------------- .../Models/AppSettings.cs | 8 - .../Models/DeploymentRunResult.cs | 9 +- .../Models/DeploymentTarget.cs | 57 +- .../Models/SonicConnection.cs | 382 +-- .../Services/CertificateDeployer.cs | 98 - .../Services/DeploymentOrchestrator.cs | 242 +- .../Services/PreflightValidator.cs | 123 +- .../Services/RestartExecutor.cs | 121 +- .../Services/SonicBinRestartExecutor.cs | 245 -- .../Services/SonicContainerDiscovery.cs | 256 +- .../Services/SonicManagementClient.cs | 726 +---- .../Services/TlsCertificateProbe.cs | 135 - .../Services/WinRmExecutor.cs | 268 -- .../Sql/001_CreateSchema.sql | 157 -- .../Sql/002_DeploymentRunModel.sql | 193 -- .../Tools/verify-mfapi-classpath.bat | 28 - ...A.CoreService.ESBCertificateManager.csproj | 7 - .../appsettings.json | 18 +- 23 files changed, 735 insertions(+), 5189 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 delete mode 100644 ZA.CoreService.ESBCertificateManager/Tools/verify-mfapi-classpath.bat 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..1f6ab93 100644 --- a/ZA.CoreService.ESBCertificateManager/Data/targets.sample.json +++ b/ZA.CoreService.ESBCertificateManager/Data/targets.sample.json @@ -4,39 +4,8 @@ "Name": "DE-Test / ct-ZADBService", "Environment": "TEST", "IsActive": true, - "TargetDirectory": "DeploySandbox/target-esb", - "CertificateFileName": "esb-cert.cer", "ContainerName": "ct-ZADBService", - "RestartType": "SonicContainer", - "RestartCommand": "", - "RestartArguments": "", - "RestartTimeoutSeconds": 120, "SonicConnectionName": "DE-Test", - "XapiSourcePath": "", - "TlsHost": "", - "TlsPort": null, - "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..1cf5907 100644 --- a/ZA.CoreService.ESBCertificateManager/Form1.cs +++ b/ZA.CoreService.ESBCertificateManager/Form1.cs @@ -5,1959 +5,645 @@ using ZA.CoreService.ESBCertificateManager.Data; using ZA.CoreService.ESBCertificateManager.Models; using ZA.CoreService.ESBCertificateManager.Services; -namespace ZA.CoreService.ESBCertificateManager +namespace ZA.CoreService.ESBCertificateManager; + +public partial class Form1 : Form { - public partial class Form1 : Form + private readonly Color BackgroundColor = Color.FromArgb(10, 18, 34); + private readonly Color SidebarColor = Color.FromArgb(7, 23, 45); + private readonly Color CardColor = Color.FromArgb(16, 38, 65); + private readonly Color BorderColor = Color.FromArgb(36, 74, 117); + private readonly Color BlueColor = Color.FromArgb(0, 110, 182); + private readonly Color GoldColor = Color.FromArgb(208, 171, 57); + private readonly Color TextColor = Color.FromArgb(241, 245, 249); + private readonly Color MutedTextColor = Color.FromArgb(169, 184, 200); + private readonly Color GreenColor = Color.FromArgb(60, 203, 127); + private readonly Color RedColor = Color.FromArgb(239, 106, 106); + + private readonly AppSettings _settings; + private readonly DeploymentOrchestrator _orchestrator; + private readonly SonicContainerDiscovery _sonicDiscovery; + + private TextBox txtCertificatePath = null!; + private Label lblCertificateSubject = null!; + private Label lblCertificateIssuer = null!; + private Label lblCertificateExpiry = null!; + private Label lblCertificateFingerprint = null!; + private Label lblCertificateValidity = null!; + private Label lblStatus = null!; + private Label lblSonicStatus = null!; + private ComboBox cmbSonicConnection = null!; + private DataGridView dgvTargets = null!; + private Button btnRestartOnly = null!; + private Button btnCancelRun = null!; + + private CertificateInfo? _loadedCertificateInfo; + private IReadOnlyList _loadedTargets = []; + private bool _isOperationRunning; + private CancellationTokenSource? _runCts; + + public Form1() { - // --------------------------------------------------------- - // ZIEHL-ABEGG-inspirierte Dark-Mode-Farbpalette - // --------------------------------------------------------- + InitializeComponent(); + _settings = AppSettingsLoader.Load(); + _orchestrator = new DeploymentOrchestrator(_settings); + _sonicDiscovery = new SonicContainerDiscovery(_settings.SonicConnections); + BuildUi(); + Shown += async (_, _) => await LoadTargetsAsync(); + } - // Große Hintergrundflächen - private readonly Color BackgroundColor = Color.FromArgb(10, 18, 34); // #0A1222 - private readonly Color SidebarColor = Color.FromArgb(7, 23, 45); // #07172D - private readonly Color CardColor = Color.FromArgb(16, 38, 65); // #102641 - private readonly Color CardHoverColor = Color.FromArgb(23, 55, 94); // #17375E - private readonly Color BorderColor = Color.FromArgb(36, 74, 117); // #244A75 + private void BuildUi() + { + SuspendLayout(); + Text = "ESB Certificate Manager"; + FormBorderStyle = FormBorderStyle.Sizable; + StartPosition = FormStartPosition.CenterScreen; + MinimumSize = new Size(960, 640); + Size = new Size(1100, 720); + BackColor = BackgroundColor; + ForeColor = TextColor; + Font = new Font("Segoe UI", 9.5f); - // ZIEHL-ABEGG-nahe Markenakzente - private readonly Color BlueColor = Color.FromArgb(0, 110, 182); // #006EB6 - private readonly Color BrightBlueColor = Color.FromArgb(0, 139, 210); // #008BD2 - private readonly Color GoldColor = Color.FromArgb(208, 171, 57); // #D0AB39 - private readonly Color LightGoldColor = Color.FromArgb(226, 196, 93); // #E2C45D - - // Schrift und Statusfarben - private readonly Color TextColor = Color.FromArgb(241, 245, 249); // #F1F5F9 - private readonly Color MutedTextColor = Color.FromArgb(169, 184, 200); // #A9B8C8 - private readonly Color GreenColor = Color.FromArgb(60, 203, 127); // #3CCB7F - private readonly Color RedColor = Color.FromArgb(239, 106, 106); // #EF6A6A - - private readonly AppSettings _settings; - private readonly DeploymentOrchestrator _orchestrator; - private readonly SonicContainerDiscovery _sonicDiscovery; - - private Label lblStatus = null!; - private Label lblStatusDot = null!; - private TextBox txtCertificatePath = null!; - private DataGridView dgvTargets = null!; - private ComboBox cmbSonicConnection = null!; - private Button btnLoadFromSonic = null!; - private Label lblSonicStatus = null!; - - private Panel navProgressPanel = null!; - - private Panel titleBar = null!; - private Button btnMinimize = null!; - private Button btnMaximize = null!; - private Button btnClose = null!; - private Button btnValidate = null!; - private Button btnDeploy = null!; - private Button btnRestartOnly = null!; - private Button btnCancelRun = null!; - - private bool isMaximized = false; - private Point dragStartPoint; - - private Label lblCertificateSubject = null!; - private Label lblCertificateIssuer = null!; - private Label lblCertificateExpiry = null!; - private Label lblCertificateFingerprint = null!; - private Label lblCertificateValidity = null!; - - private Button btnSelectFile = null!; - private bool isCertificateFileLoaded; - private CertificateInfo? _loadedCertificateInfo; - private IReadOnlyList _loadedTargets = []; - private bool _isOperationRunning; - private CancellationTokenSource? _runCts; - private int nvbarlaststate = 1; - - public Form1() + Panel sidebar = new() { - InitializeComponent(); + Dock = DockStyle.Left, + Width = 220, + BackColor = SidebarColor, + Padding = new Padding(16) + }; + sidebar.Controls.Add(new Label + { + Text = "ESB Certificate\nManager", + Dock = DockStyle.Top, + Height = 70, + Font = new Font("Segoe UI Semibold", 14f), + ForeColor = GoldColor + }); + sidebar.Controls.Add(new Label + { + Text = "1. Zertifikat erkennen\n2. Container neu starten", + Dock = DockStyle.Top, + Height = 80, + ForeColor = MutedTextColor + }); - _settings = AppSettingsLoader.Load(); - _orchestrator = new DeploymentOrchestrator(_settings); - _sonicDiscovery = new SonicContainerDiscovery(_settings.SonicConnections); + Panel main = new() + { + Dock = DockStyle.Fill, + Padding = new Padding(20), + BackColor = BackgroundColor + }; - BuildDesign(); - Shown += async (_, _) => await LoadTargetsAsync(); + TableLayoutPanel layout = new() + { + Dock = DockStyle.Fill, + ColumnCount = 1, + RowCount = 4, + BackColor = BackgroundColor + }; + layout.RowStyles.Add(new RowStyle(SizeType.Absolute, 160)); + layout.RowStyles.Add(new RowStyle(SizeType.Absolute, 56)); + layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); + layout.RowStyles.Add(new RowStyle(SizeType.Absolute, 70)); + + layout.Controls.Add(BuildCertificateCard(), 0, 0); + layout.Controls.Add(BuildSonicBar(), 0, 1); + layout.Controls.Add(BuildTargetsCard(), 0, 2); + layout.Controls.Add(BuildActionBar(), 0, 3); + + main.Controls.Add(layout); + Controls.Add(main); + Controls.Add(sidebar); + ResumeLayout(); + } + + private Panel BuildCertificateCard() + { + Panel card = MakeCard(); + card.Controls.Add(SectionTitle("Zertifikat erkennen", 12)); + + txtCertificatePath = new TextBox + { + Left = 16, + Top = 48, + Width = 620, + Height = 28, + ReadOnly = true, + BackColor = Color.FromArgb(12, 28, 48), + ForeColor = TextColor, + BorderStyle = BorderStyle.FixedSingle + }; + + Button btnSelect = MakeButton("Datei wählen…", BlueColor); + btnSelect.Left = 650; + btnSelect.Top = 46; + btnSelect.Width = 140; + btnSelect.Click += (_, _) => TrySelectCertificateFile(); + + lblCertificateSubject = MetaLabel(16, 90, "Subject: –"); + lblCertificateIssuer = MetaLabel(16, 112, "Issuer: –"); + lblCertificateExpiry = MetaLabel(400, 90, "Gültig bis: –"); + lblCertificateFingerprint = MetaLabel(400, 112, "SHA-256: –"); + lblCertificateValidity = MetaLabel(16, 134, "Status: –"); + + card.Controls.AddRange([ + txtCertificatePath, btnSelect, + lblCertificateSubject, lblCertificateIssuer, + lblCertificateExpiry, lblCertificateFingerprint, lblCertificateValidity + ]); + return card; + } + + private Panel BuildSonicBar() + { + Panel bar = MakeCard(); + Label lbl = new() + { + Text = "Sonic:", + Left = 16, + Top = 16, + AutoSize = true, + ForeColor = MutedTextColor + }; + + cmbSonicConnection = new ComboBox + { + Left = 70, + Top = 12, + Width = 200, + DropDownStyle = ComboBoxStyle.DropDownList, + BackColor = Color.FromArgb(12, 28, 48), + ForeColor = TextColor, + FlatStyle = FlatStyle.Flat + }; + foreach (SonicConnection c in _settings.SonicConnections) + { + cmbSonicConnection.Items.Add(c.Name); } - private Button CreateWindowButton(string text) + if (cmbSonicConnection.Items.Count > 0) { - return new Button - { - Text = text, - Width = 46, - Height = 34, - FlatStyle = FlatStyle.Flat, - FlatAppearance = - { - BorderSize = 0, - MouseOverBackColor = CardHoverColor - }, - BackColor = SidebarColor, - ForeColor = MutedTextColor, - Font = new Font("Segoe UI", 11), - Cursor = Cursors.Hand, - TabStop = false - }; + cmbSonicConnection.SelectedIndex = 0; } - private void ToggleMaximizeWindow() + Button btnLoad = MakeButton("Container laden", BlueColor); + btnLoad.Left = 290; + btnLoad.Top = 10; + btnLoad.Width = 140; + btnLoad.Click += async (_, _) => await LoadFromSonicAsync(); + + lblSonicStatus = new Label { - if (isMaximized) - { - WindowState = FormWindowState.Normal; - isMaximized = false; - } - else - { - WindowState = FormWindowState.Maximized; - isMaximized = true; - } + Left = 450, + Top = 16, + AutoSize = true, + ForeColor = MutedTextColor, + Text = "MfApi" + }; + + bar.Controls.AddRange([lbl, cmbSonicConnection, btnLoad, lblSonicStatus]); + return bar; + } + + private Panel BuildTargetsCard() + { + Panel card = MakeCard(); + card.Controls.Add(SectionTitle("Ziele / Container", 12)); + + dgvTargets = new DataGridView + { + Left = 16, + Top = 44, + Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right, + Width = 780, + Height = 220, + BackgroundColor = Color.FromArgb(12, 28, 48), + ForeColor = TextColor, + GridColor = BorderColor, + BorderStyle = BorderStyle.None, + RowHeadersVisible = false, + AllowUserToAddRows = false, + AllowUserToDeleteRows = false, + ReadOnly = false, + SelectionMode = DataGridViewSelectionMode.FullRowSelect, + AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill, + EnableHeadersVisualStyles = false + }; + dgvTargets.ColumnHeadersDefaultCellStyle.BackColor = SidebarColor; + dgvTargets.ColumnHeadersDefaultCellStyle.ForeColor = GoldColor; + dgvTargets.DefaultCellStyle.BackColor = Color.FromArgb(12, 28, 48); + dgvTargets.DefaultCellStyle.ForeColor = TextColor; + dgvTargets.DefaultCellStyle.SelectionBackColor = BlueColor; + + dgvTargets.Columns.Add(new DataGridViewCheckBoxColumn + { + Name = "Selected", + HeaderText = "", + Width = 40, + FillWeight = 15 + }); + dgvTargets.Columns.Add(new DataGridViewTextBoxColumn { Name = "Name", HeaderText = "Name", ReadOnly = true }); + dgvTargets.Columns.Add(new DataGridViewTextBoxColumn { Name = "Container", HeaderText = "Container", ReadOnly = true }); + dgvTargets.Columns.Add(new DataGridViewTextBoxColumn { Name = "Connection", HeaderText = "Verbindung", ReadOnly = true }); + dgvTargets.Columns.Add(new DataGridViewTextBoxColumn { Name = "Status", HeaderText = "Status", ReadOnly = true }); + dgvTargets.Columns.Add(new DataGridViewTextBoxColumn + { + Name = "TargetId", + Visible = false + }); + + card.Resize += (_, _) => + { + dgvTargets.Width = Math.Max(200, card.ClientSize.Width - 32); + dgvTargets.Height = Math.Max(80, card.ClientSize.Height - 60); + }; + + card.Controls.Add(dgvTargets); + return card; + } + + private Panel BuildActionBar() + { + Panel bar = MakeCard(); + + btnRestartOnly = MakeButton("ESB neu starten", GoldColor); + btnRestartOnly.ForeColor = Color.Black; + btnRestartOnly.Left = 16; + btnRestartOnly.Top = 14; + btnRestartOnly.Width = 180; + btnRestartOnly.Click += async (_, _) => await RunRestartOnlyAsync(); + + btnCancelRun = MakeButton("Abbrechen", RedColor); + btnCancelRun.Left = 210; + btnCancelRun.Top = 14; + btnCancelRun.Width = 120; + btnCancelRun.Enabled = false; + btnCancelRun.Click += (_, _) => _runCts?.Cancel(); + + lblStatus = new Label + { + Left = 350, + Top = 20, + AutoSize = true, + ForeColor = MutedTextColor, + Text = "Bereit." + }; + + bar.Controls.AddRange([btnRestartOnly, btnCancelRun, lblStatus]); + return bar; + } + + private async Task LoadTargetsAsync() + { + try + { + (ITargetRepository repo, string msg) = await TargetRepositoryFactory.CreateAsync(_settings); + _loadedTargets = await repo.GetActiveTargetsAsync(); + BindTargets(_loadedTargets); + SetStatus(msg, false); + } + catch (Exception ex) + { + SetStatus($"Ziele laden fehlgeschlagen: {ex.Message}", true); + } + } + + private async Task LoadFromSonicAsync() + { + if (cmbSonicConnection.SelectedItem is not string name) + { + SetStatus("Keine Sonic-Verbindung gewählt.", true); + return; } - private void TitleBar_MouseDown(object? sender, MouseEventArgs e) + SetStatus($"Lade Container von '{name}'…", false); + SonicDiscoveryResult result = await _sonicDiscovery.DiscoverAsync(name, _loadedTargets); + if (!result.Success) { - if (e.Button == MouseButtons.Left) - { - dragStartPoint = e.Location; - } + SetStatus(result.ErrorMessage ?? "Discovery fehlgeschlagen", true); + return; } - private void TitleBar_MouseMove(object? sender, MouseEventArgs e) + _loadedTargets = result.DiscoveredTargets; + BindTargets(_loadedTargets); + lblSonicStatus.Text = $"Domain={result.DomainName}; {result.DiscoveredTargets.Count} Container"; + SetStatus(result.ErrorMessage ?? $"Container geladen ({result.DiscoveredTargets.Count}).", false); + } + + private void BindTargets(IReadOnlyList targets) + { + dgvTargets.Rows.Clear(); + foreach (DeploymentTarget t in targets) { - if (e.Button == MouseButtons.Left) - { - Left += e.X - dragStartPoint.X; - Top += e.Y - dragStartPoint.Y; - } + int row = dgvTargets.Rows.Add(true, t.Name, t.ContainerName, t.SonicConnectionName, "bereit", t.Id); + dgvTargets.Rows[row].Tag = t; } - private Panel BuildTitleBar() + } + + private List GetSelectedTargets() + => dgvTargets.Rows.Cast() + .Where(r => !r.IsNewRow && r.Cells["Selected"].Value is true) + .Select(r => r.Tag) + .OfType() + .ToList(); + + private async Task RunRestartOnlyAsync() + { + if (_isOperationRunning) { - Panel panel = new Panel - { - Height = 34, - BackColor = SidebarColor - }; - - Label appIcon = new Label - { - Text = "◆", - ForeColor = GoldColor, - Font = new Font("Segoe UI Symbol", 12, FontStyle.Bold), - AutoSize = true, - Location = new Point(12, 8) - }; - - Label appName = new Label - { - Text = "ESB Certificate Manager", - ForeColor = MutedTextColor, - Font = new Font("Segoe UI", 9), - AutoSize = true, - Location = new Point(31, 9) - }; - - btnClose = CreateWindowButton("×"); - btnMaximize = CreateWindowButton("□"); - btnMinimize = CreateWindowButton("−"); - - btnClose.ForeColor = TextColor; - btnClose.FlatAppearance.MouseOverBackColor = Color.FromArgb(190, 35, 45); - - btnClose.Click += (_, _) => Close(); - - btnMinimize.Click += (_, _) => - { - WindowState = FormWindowState.Minimized; - }; - - btnMaximize.Click += (_, _) => - { - ToggleMaximizeWindow(); - }; - - panel.Resize += (_, _) => - { - btnClose.Left = panel.Width - btnClose.Width; - btnMaximize.Left = btnClose.Left - btnMaximize.Width; - btnMinimize.Left = btnMaximize.Left - btnMinimize.Width; - }; - - panel.MouseDown += TitleBar_MouseDown; - panel.MouseMove += TitleBar_MouseMove; - appIcon.MouseDown += TitleBar_MouseDown; - appIcon.MouseMove += TitleBar_MouseMove; - appName.MouseDown += TitleBar_MouseDown; - appName.MouseMove += TitleBar_MouseMove; - - panel.Controls.Add(appIcon); - panel.Controls.Add(appName); - panel.Controls.Add(btnMinimize); - panel.Controls.Add(btnMaximize); - panel.Controls.Add(btnClose); - - return panel; - } - private void BuildDesign() - { - Text = "ESB Certificate Manager"; - StartPosition = FormStartPosition.CenterScreen; - - MinimumSize = new Size(1100, 700); - Size = new Size(1400, 850); - - BackColor = BackgroundColor; - Font = new Font("Segoe UI", 10); - ForeColor = TextColor; - - // Entfernt die weiße Windows-Standardtitelleiste. - FormBorderStyle = FormBorderStyle.None; - - // Eigene dunkle Titelleiste. - titleBar = BuildTitleBar(); - titleBar.Dock = DockStyle.Top; - Controls.Add(titleBar); - - Panel sidebar = new Panel - { - Dock = DockStyle.Left, - Width = 205, - BackColor = SidebarColor - }; - - Panel content = new Panel - { - Dock = DockStyle.Fill, - BackColor = BackgroundColor, - Padding = new Padding(38, 48, 38, 34), - AutoScroll = true - }; - - // Wichtig für Docking: - // Erst der Fill-Bereich, dann die feste Sidebar. - Controls.Add(content); - Controls.Add(sidebar); - - // Dezente vertikale Linie rechts an der Sidebar. - Panel sidebarBorder = new Panel - { - Dock = DockStyle.Right, - Width = 1, - BackColor = BorderColor - }; - - sidebar.Controls.Add(sidebarBorder); - - BuildSidebar(sidebar); - BuildContent(content); + return; } - private void BuildSidebar(Panel sidebar) + List selected = GetSelectedTargets(); + PreflightValidationResult preflight = _orchestrator.ValidateRestartOnly(selected); + if (!preflight.IsValid) { - - // ------------------------- - // 1. Logo / Produktbereich - // ------------------------- - Panel logoPanel = new Panel - { - Dock = DockStyle.Top, - Height = 165, - Padding = new Padding(18, 18, 18, 8) - }; - - PictureBox pictureLogo = new PictureBox - { - Location = new Point(18, 14), - Size = new Size(165, 58), - SizeMode = PictureBoxSizeMode.Zoom, - BackColor = Color.Transparent - }; - - string logoPath = Path.Combine( - Application.StartupPath, - "Assets", - "logo.png"); - - if (File.Exists(logoPath)) - { - pictureLogo.Image = Image.FromFile(logoPath); - } - - Label logoSubtitle = new Label - { - Text = "ESB CERTIFICATE MANAGER", - ForeColor = TextColor, - Font = new Font("Segoe UI", 8, FontStyle.Bold), - AutoSize = true, - Location = new Point(18, 88) - }; - - - Panel logoSeparator = new Panel - { - BackColor = BorderColor, - Location = new Point(22, 138), - Size = new Size(168, 1) - }; - - logoPanel.Controls.Add(pictureLogo); - logoPanel.Controls.Add(logoSubtitle); - logoPanel.Controls.Add(logoSeparator); - - // ------------------------------------ - // 2. Linker Ablauf: keine Navigation, - // sondern Fortschritt im Deployment - // ------------------------------------ - Panel progressPanel = new Panel - { - Dock = DockStyle.Top, - Height = 360, - Padding = new Padding(18, 8, 18, 8) - }; - navProgressPanel = progressPanel; - - Label progressTitle = new Label - { - Text = "DEPLOYMENT-ABLAUF", - ForeColor = MutedTextColor, - Font = new Font("Segoe UI", 7.5f, FontStyle.Bold), - AutoSize = true, - Location = new Point(18, 12) - }; - - AddStep( - progressPanel, - y: 48, - number: "1", - title: "Zertifikat", - description: "Datei auswählen", - isActive: true, - hasNextStep: true); - - AddStep( - progressPanel, - y: 116, - number: "2", - title: "Ziele", - description: "Systeme auswählen", - isActive: false, - hasNextStep: true); - - AddStep( - progressPanel, - y: 184, - number: "3", - title: "Bereitstellen", - description: "Kopieren & Neustart", - isActive: false, - hasNextStep: true); - - AddStep( - progressPanel, - y: 252, - number: "4", - title: "TLS-Prüfung", - description: "Zertifikat bestätigen", - isActive: false, - hasNextStep: false); - - progressPanel.Controls.Add(progressTitle); - - // ----------------------------- - // 3. Footer unten: Version - // ----------------------------- - Panel footerPanel = new Panel - { - Dock = DockStyle.Bottom, - Height = 70, - Padding = new Padding(18, 8, 12, 8) - }; - - Panel footerSeparator = new Panel - { - Dock = DockStyle.Top, - Height = 1, - BackColor = BorderColor - }; - - Label versionLabel = new Label - { - Text = "INTERNAL TOOL", - ForeColor = MutedTextColor, - Font = new Font("Segoe UI", 7.5f, FontStyle.Bold), - AutoSize = true, - Location = new Point(18, 17) - }; - - Label versionNumber = new Label - { - Text = "Version 0.1.0", - ForeColor = BlueColor, - Font = new Font("Segoe UI", 8), - AutoSize = true, - Location = new Point(18, 37) - }; - - footerPanel.Controls.Add(footerSeparator); - footerPanel.Controls.Add(versionLabel); - footerPanel.Controls.Add(versionNumber); - - // Dock-Reihenfolge: Bottom, Top, Top - sidebar.Controls.Add(footerPanel); - sidebar.Controls.Add(progressPanel); - sidebar.Controls.Add(logoPanel); - } - - private void AddStep( - Panel parent, - int y, - string number, - string title, - string description, - bool isActive, - bool hasNextStep) - { - Color stepColor = isActive ? GoldColor : MutedTextColor; - Color titleColor = isActive ? TextColor : MutedTextColor; - - Label circle = new Label - { - Name = $"stepCircle{number}", - Text = number, - Location = new Point(18, y), - Size = new Size(25, 25), - TextAlign = ContentAlignment.MiddleCenter, - BackColor = isActive ? GoldColor : Color.FromArgb(25, 48, 76), - ForeColor = isActive ? SidebarColor : MutedTextColor, - Font = new Font("Segoe UI", 8, FontStyle.Bold) - }; - - Label titleLabel = new Label - { - Name = $"stepTitle{number}", - Text = title, - ForeColor = titleColor, - Font = new Font("Segoe UI", 9.5f, FontStyle.Bold), - AutoSize = true, - Location = new Point(56, y - 1) - }; - - Label descriptionLabel = new Label - { - Name = $"stepDescription{number}", - Text = description, - ForeColor = MutedTextColor, - Font = new Font("Segoe UI", 8), - AutoSize = true, - Location = new Point(56, y + 16) - }; - - parent.Controls.Add(circle); - parent.Controls.Add(titleLabel); - parent.Controls.Add(descriptionLabel); - - if (hasNextStep) - { - Panel line = new Panel - { - BackColor = BorderColor, - Location = new Point(30, y + 26), - Size = new Size(1, 40) - }; - - parent.Controls.Add(line); - line.SendToBack(); - } - } - private void UpdateToNextStep(int state) - { - - Control[] foundCircles = navProgressPanel.Controls.Find($"stepCircle{state}", true); - if (foundCircles.Length == 0) return; - Control[] foundTitels = navProgressPanel.Controls.Find($"stepTitle{state}", true); - if (foundTitels.Length == 0) return; - Label titel = (Label)foundTitels[0]; - Label circle = (Label)foundCircles[0]; - - - Control[] foundlastTitels = navProgressPanel.Controls.Find($"stepTitle{nvbarlaststate}", true); - if (foundlastTitels.Length == 0) return; - Label lasttitel = (Label)foundlastTitels[0]; - if(nvbarlaststate > state) - { - Control[] foundlastCircles = navProgressPanel.Controls.Find($"stepCircle{nvbarlaststate}", true); - if (foundlastCircles.Length == 0) return; - Label lastcircle = (Label)foundlastCircles[0]; - lastcircle.BackColor = Color.FromArgb(25, 48, 76); - lastcircle.ForeColor = MutedTextColor; - lasttitel.ForeColor = MutedTextColor; - } - else - { - lasttitel.ForeColor = MutedTextColor; - } - - - circle.BackColor = GoldColor; - circle.ForeColor = SidebarColor; - titel.ForeColor = TextColor; - nvbarlaststate = state; - } - - private Button CreateNavButton(string text, bool isActive) - { - Button button = new Button - { - Text = text, - Height = 43, - Dock = DockStyle.Top, - FlatStyle = FlatStyle.Flat, - FlatAppearance = - { - BorderSize = 0, - MouseOverBackColor = Color.FromArgb(37, 52, 74) - }, - BackColor = isActive ? Color.FromArgb(35, 58, 92) : SidebarColor, - ForeColor = isActive ? TextColor : MutedTextColor, - Font = new Font("Segoe UI", 10, isActive ? FontStyle.Bold : FontStyle.Regular), - TextAlign = ContentAlignment.MiddleLeft, - Padding = new Padding(14, 0, 0, 0), - Cursor = Cursors.Hand - }; - - button.Click += (_, _) => - { - SetStatus($"Navigation gewählt: {text.Trim()}", isError: false); - }; - - return button; - } - - private void BuildContent(Panel content) - { - // Status unten zuerst hinzufügen, weil Dock-Reihenfolge relevant ist - Panel statusBar = BuildStatusBar(); - statusBar.Dock = DockStyle.Bottom; - content.Controls.Add(statusBar); - - Panel actionPanel = BuildActionPanel(); - actionPanel.Dock = DockStyle.Bottom; - actionPanel.Height = 65; - content.Controls.Add(actionPanel); - - Panel targetsCard = BuildTargetsCard(); - targetsCard.Dock = DockStyle.Top; - targetsCard.Height = 380; - targetsCard.Margin = new Padding(0, 18, 0, 0); - content.Controls.Add(targetsCard); - - Panel topCards = BuildTopCards(); - topCards.Dock = DockStyle.Top; - topCards.Height = 225; - topCards.Margin = new Padding(0, 20, 0, 0); - content.Controls.Add(topCards); - - Panel header = BuildHeader(); - header.Dock = DockStyle.Top; - header.Height = 87; - content.Controls.Add(header); - } - - private Panel BuildHeader() - { - Panel header = new Panel - { - BackColor = BackgroundColor - }; - - Label title = new Label - { - Text = "Zertifikat bereitstellen", - ForeColor = TextColor, - Font = new Font("Segoe UI", 23, FontStyle.Bold), - AutoSize = true, - Location = new Point(0, 0) - }; - - Label subtitle = new Label - { - Text = "Verteile ein Zertifikat auf konfigurierte Ziele und prüfe anschließend die TLS-Verbindung.", - ForeColor = MutedTextColor, - Font = new Font("Segoe UI", 10), - AutoSize = true, - Location = new Point(3, 43) - }; - - Label environmentBadge = new Label - { - Text = " UMGEBUNG ", - ForeColor = GoldColor, - BackColor = Color.FromArgb(65, 53, 17), - Font = new Font("Segoe UI", 8, FontStyle.Bold), - AutoSize = true, - Padding = new Padding(7, 6, 7, 6), - Anchor = AnchorStyles.Top | AnchorStyles.Right - }; - - environmentBadge.Location = new Point(900, 9); - - header.Resize += (_, _) => - { - environmentBadge.Left = header.Width - environmentBadge.Width; - }; - - header.Controls.Add(title); - header.Controls.Add(subtitle); - header.Controls.Add(environmentBadge); - - return header; - } - - private Panel BuildTopCards() - { - Panel container = new Panel - { - BackColor = BackgroundColor - }; - - Panel sourceCard = CreateCard(); - sourceCard.Dock = DockStyle.Left; - sourceCard.Width = 520; - - Panel detailsCard = CreateCard(); - detailsCard.Dock = DockStyle.Fill; - detailsCard.Margin = new Padding(18, 0, 0, 0); - - container.Controls.Add(detailsCard); - container.Controls.Add(sourceCard); - - BuildSourceCard(sourceCard); - BuildDetailsCard(detailsCard); - - return container; - } - private void BuildSourceCard(Panel card) - { - Label step = CreateSmallTitle("SCHRITT 1"); - step.Location = new Point(22, 18); - - Label title = CreateCardTitle("Zertifikatsquelle"); - title.Location = new Point(22, 40); - - Label description = CreateMutedLabel( - "Wähle die Zertifikatsdatei aus der zentralen Ablage."); - description.Location = new Point(22, 73); - - txtCertificatePath = new TextBox - { - Location = new Point(22, 112), - Size = new Size(320, 38), - BackColor = Color.FromArgb(20, 30, 48), - ForeColor = TextColor, - BorderStyle = BorderStyle.FixedSingle, - Font = new Font("Segoe UI", 9), - Text = string.Empty, - PlaceholderText = "Keine Datei ausgewählt", - ReadOnly = true - }; - - btnSelectFile = CreateSecondaryButton("Datei auswählen"); - btnSelectFile.Location = new Point(354, 111); - btnSelectFile.Size = new Size(140, 39); - SetFileButtonState(fileLoaded: false); - - btnSelectFile.Click += (_, _) => - { - if (isCertificateFileLoaded) - { - ClearSelectedCertificate(); - return; - } - - TrySelectCertificateFile(); - }; - - Label hint = new Label - { - Text = "Unterstützte Formate: .cer, .crt, .pem, .pfx", - ForeColor = MutedTextColor, - Font = new Font("Segoe UI", 8.5f), - AutoSize = true, - Location = new Point(22, 164) - }; - - card.Controls.Add(step); - card.Controls.Add(title); - card.Controls.Add(description); - card.Controls.Add(txtCertificatePath); - card.Controls.Add(btnSelectFile); - card.Controls.Add(hint); - } - private void TrySelectCertificateFile() - { - using OpenFileDialog dialog = new OpenFileDialog - { - Title = "Zertifikatsdatei auswählen", - Filter = - "Unterstützte Zertifikatsdateien (*.cer;*.crt;*.pem;*.pfx)|*.cer;*.crt;*.pem;*.pfx|" + - "CER-Zertifikate (*.cer)|*.cer|" + - "CRT-Zertifikate (*.crt)|*.crt|" + - "PEM-Zertifikate (*.pem)|*.pem|" + - "PFX-Zertifikate (*.pfx)|*.pfx", - FilterIndex = 1, - Multiselect = false, - CheckFileExists = true, - CheckPathExists = true, - RestoreDirectory = true, - AddExtension = true, - DereferenceLinks = true - }; - - // Bei Abbruch bleibt der bisherige Zustand unverändert. - if (dialog.ShowDialog(this) != DialogResult.OK) - { - return; - } - - string selectedPath = dialog.FileName; - - if (!IsSupportedCertificateFile(selectedPath)) - { - MessageBox.Show( - this, - "Bitte wähle eine Zertifikatsdatei in einem der folgenden Formate aus:\n" + - ".cer, .crt, .pem oder .pfx", - "Nicht unterstütztes Dateiformat", - MessageBoxButtons.OK, - MessageBoxIcon.Warning); - return; - } - - CertificateInfo? certificateInfo = TryReadSelectedCertificate(selectedPath); - if (certificateInfo == null) - { - return; - } - - txtCertificatePath.Text = selectedPath; - _loadedCertificateInfo = certificateInfo; - SetStatus($"Zertifikat ausgewählt: {Path.GetFileName(selectedPath)}", isError: false); - DisplayCertificateInfo(certificateInfo); - UpdateToNextStep(2); - SetFileButtonState(fileLoaded: true); - RefreshActionButtonStates(); - } - - private CertificateInfo? TryReadSelectedCertificate(string selectedPath) - { - bool isPfx = Path.GetExtension(selectedPath) - .Equals(".pfx", StringComparison.OrdinalIgnoreCase); - - try - { - // Zuerst ohne Passwort versuchen - return ReadCertificate(selectedPath, null); - } - catch (CryptographicException) when (isPfx) - { - // Passwort nur abfragen, wenn das Laden ohne Passwort fehlgeschlagen ist - string? pfxPassword = PromptForPfxPassword(); - if (pfxPassword == null) - { - return null; - } - - try - { - return ReadCertificate(selectedPath, pfxPassword); - } - catch (CryptographicException) - { - MessageBox.Show( - "Das angegebene Passwort ist falsch oder die Datei ist beschädigt.", - "Fehler beim Lesen des Zertifikats", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - return null; - } - } - catch (Exception ex) - { - MessageBox.Show( - this, - $"Das Zertifikat konnte nicht gelesen werden.\n\n{ex.Message}", - "Fehler beim Lesen des Zertifikats", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - return null; - } - } - - private void ClearSelectedCertificate() - { - txtCertificatePath.Text = string.Empty; - _loadedCertificateInfo = null; - SetStatus("Keine Datei ausgewählt", isError: false); - - lblCertificateSubject.Text = "-"; - lblCertificateIssuer.Text = "-"; - lblCertificateExpiry.Text = "-"; - lblCertificateFingerprint.Text = "-"; - SetValidityBadge( - "○ NICHT GELADEN", - MutedTextColor, - Color.FromArgb(39, 52, 73)); - - UpdateToNextStep(1); - SetFileButtonState(fileLoaded: false); - RefreshActionButtonStates(); - } - - private void SetFileButtonState(bool fileLoaded) - { - isCertificateFileLoaded = fileLoaded; - - if (fileLoaded) - { - btnSelectFile.Text = "Datei auswerfen"; - btnSelectFile.BackColor = Color.FromArgb(72, 31, 38); - btnSelectFile.ForeColor = Color.FromArgb(255, 210, 210); - btnSelectFile.FlatAppearance.BorderColor = RedColor; - btnSelectFile.FlatAppearance.MouseOverBackColor = Color.FromArgb(96, 42, 50); - return; - } - - btnSelectFile.Text = "Datei auswählen"; - btnSelectFile.BackColor = Color.FromArgb(39, 52, 73); - btnSelectFile.ForeColor = TextColor; - btnSelectFile.FlatAppearance.BorderColor = BorderColor; - btnSelectFile.FlatAppearance.MouseOverBackColor = Color.FromArgb(54, 69, 94); - } - - private void SetValidityBadge(string text, Color foreColor, Color backColor) - { - lblCertificateValidity.Text = text; - lblCertificateValidity.ForeColor = foreColor; - lblCertificateValidity.BackColor = backColor; - RepositionValidityBadge(); - } - - private void RepositionValidityBadge() - { - if (lblCertificateValidity?.Parent == null) - { - return; - } - - lblCertificateValidity.Left = - lblCertificateValidity.Parent.Width - lblCertificateValidity.Width - 22; - } - - private static string? PromptForPfxPassword() - { - using Form dialog = new Form - { - Text = "PFX-Passwort eingeben", - Size = new Size(360, 150), - StartPosition = FormStartPosition.CenterParent, - FormBorderStyle = FormBorderStyle.FixedDialog, - MinimizeBox = false, - MaximizeBox = false - }; - - Label label = new Label - { - Text = "Passwort für die PFX-Datei:", - Location = new Point(12, 15), - AutoSize = true - }; - - TextBox txtPwd = new TextBox - { - Location = new Point(12, 38), - Width = 320, - UseSystemPasswordChar = true - }; - - Button btnOk = new Button - { - Text = "OK", - DialogResult = DialogResult.OK, - Location = new Point(176, 72), - Width = 75 - }; - - Button btnCancel = new Button - { - Text = "Abbrechen", - DialogResult = DialogResult.Cancel, - Location = new Point(257, 72), - Width = 75 - }; - - dialog.Controls.AddRange([label, txtPwd, btnOk, btnCancel]); - dialog.AcceptButton = btnOk; - dialog.CancelButton = btnCancel; - - return dialog.ShowDialog() == DialogResult.OK ? txtPwd.Text : null; - } - - private static CertificateInfo ReadCertificate(string certificatePath, string? pfxPassword = null) - { - if (string.IsNullOrWhiteSpace(certificatePath)) - { - throw new ArgumentException( - "Es wurde kein Zertifikatspfad angegeben.", - nameof(certificatePath)); - } - - if (!File.Exists(certificatePath)) - { - throw new FileNotFoundException( - "Die ausgewählte Zertifikatsdatei wurde nicht gefunden.", - certificatePath); - } - - string extension = Path.GetExtension(certificatePath); - - using X509Certificate2 certificate = - extension.Equals(".pfx", StringComparison.OrdinalIgnoreCase) - ? new X509Certificate2( - certificatePath, - pfxPassword, - X509KeyStorageFlags.EphemeralKeySet) - : new X509Certificate2(certificatePath); - - string subject = certificate.GetNameInfo( - X509NameType.SimpleName, - forIssuer: false); - - string issuer = certificate.GetNameInfo( - X509NameType.SimpleName, - forIssuer: true); - - if (string.IsNullOrWhiteSpace(subject)) - { - subject = certificate.Subject; - } - - if (string.IsNullOrWhiteSpace(issuer)) - { - issuer = certificate.Issuer; - } - - string fingerprint = certificate.GetCertHashString( - HashAlgorithmName.SHA256); - - fingerprint = FormatFingerprint(fingerprint); - - DateTimeOffset validFrom = - new DateTimeOffset(certificate.NotBefore); - - DateTimeOffset validUntil = - new DateTimeOffset(certificate.NotAfter); - - DateTimeOffset now = DateTimeOffset.Now; - - bool isCurrentlyValid = - now >= validFrom && - now <= validUntil; - - return new CertificateInfo - { - Subject = subject, - Issuer = issuer, - ValidFrom = validFrom, - ValidUntil = validUntil, - FingerprintSha256 = fingerprint, - IsCurrentlyValid = isCurrentlyValid - }; - } - - private void DisplayCertificateInfo(CertificateInfo certificateInfo) - { - lblCertificateSubject.Text = certificateInfo.Subject; - lblCertificateIssuer.Text = certificateInfo.Issuer; - lblCertificateExpiry.Text = - certificateInfo.ValidUntil.ToLocalTime().ToString("dd.MM.yyyy HH:mm"); - lblCertificateFingerprint.Text = certificateInfo.FingerprintSha256; - - if (certificateInfo.IsCurrentlyValid) - { - SetValidityBadge("● GÜLTIG", GreenColor, Color.FromArgb(20, 62, 46)); - return; - } - - SetValidityBadge( - "● ABGELAUFEN / NICHT GÜLTIG", - RedColor, - Color.FromArgb(72, 31, 38)); - } - private static string FormatFingerprint(string fingerprint) - { - if (string.IsNullOrWhiteSpace(fingerprint)) - { - return string.Empty; - } - - return string.Join( - ":", - Enumerable.Range(0, fingerprint.Length / 2) - .Select(index => fingerprint.Substring(index * 2, 2))); - } - private static bool IsSupportedCertificateFile(string filePath) - { - if (string.IsNullOrWhiteSpace(filePath)) - { - return false; - } - - if (!File.Exists(filePath)) - { - return false; - } - - string extension = Path.GetExtension(filePath); - - string[] supportedExtensions = - { - ".cer", - ".crt", - ".pem", - ".pfx" - }; - - return supportedExtensions.Contains( - extension, - StringComparer.OrdinalIgnoreCase); - } - private void BuildDetailsCard(Panel card) - { - Label step = CreateSmallTitle("ERKANNTE INFORMATIONEN"); - step.Location = new Point(22, 18); - - Label title = CreateCardTitle("Zertifikatsdetails"); - title.Location = new Point(22, 40); - - lblCertificateValidity = new Label - { - Text = "○ NICHT GELADEN", - ForeColor = MutedTextColor, - BackColor = Color.FromArgb(39, 52, 73), - Font = new Font("Segoe UI", 8, FontStyle.Bold), - AutoSize = true, - Padding = new Padding(8, 5, 8, 5), - Anchor = AnchorStyles.Top | AnchorStyles.Right - }; - - lblCertificateValidity.Location = new Point(300, 40); - - card.Resize += (_, _) => RepositionValidityBadge(); - - Label subjectLabel = CreateMutedLabel("SUBJECT / CN"); - subjectLabel.Location = new Point(22, 84); - - lblCertificateSubject = CreateValueLabel("-"); - lblCertificateSubject.Location = new Point(22, 102); - lblCertificateSubject.AutoEllipsis = true; - lblCertificateSubject.MaximumSize = new Size(210, 0); - - Label issuerLabel = CreateMutedLabel("AUSSTELLER"); - issuerLabel.Location = new Point(22, 133); - - lblCertificateIssuer = CreateValueLabel("-"); - lblCertificateIssuer.Location = new Point(22, 151); - lblCertificateIssuer.AutoEllipsis = true; - lblCertificateIssuer.MaximumSize = new Size(210, 0); - - Label expiryLabel = CreateMutedLabel("GÜLTIG BIS"); - expiryLabel.Location = new Point(22, 182); - - lblCertificateExpiry = CreateValueLabel("-"); - lblCertificateExpiry.Location = new Point(22, 200); - - Label fingerprintLabel = CreateMutedLabel("SHA-256 FINGERPRINT"); - fingerprintLabel.Location = new Point(245, 84); - - lblCertificateFingerprint = CreateValueLabel("-"); - lblCertificateFingerprint.Location = new Point(245, 102); - lblCertificateFingerprint.AutoSize = false; - lblCertificateFingerprint.Size = new Size(310, 48); - - card.Controls.Add(step); - card.Controls.Add(title); - card.Controls.Add(lblCertificateValidity); - - card.Controls.Add(subjectLabel); - card.Controls.Add(lblCertificateSubject); - - card.Controls.Add(issuerLabel); - card.Controls.Add(lblCertificateIssuer); - - card.Controls.Add(expiryLabel); - card.Controls.Add(lblCertificateExpiry); - - card.Controls.Add(fingerprintLabel); - card.Controls.Add(lblCertificateFingerprint); - } - - private Panel BuildTargetsCard() - { - Panel card = CreateCard(); - - Label step = CreateSmallTitle("SCHRITT 2"); - step.Location = new Point(22, 18); - - Label title = CreateCardTitle("Bereitstellungsziele"); - title.Location = new Point(22, 40); - - Label description = CreateMutedLabel( - "Wähle aus, auf welche Systeme das Zertifikat verteilt werden soll."); - description.Location = new Point(22, 72); - - dgvTargets = new DataGridView - { - Location = new Point(22, 148), - Size = new Size(1000, 205), - Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right, - BackgroundColor = CardColor, - BorderStyle = BorderStyle.None, - EnableHeadersVisualStyles = false, - AllowUserToAddRows = false, - AllowUserToDeleteRows = false, - AllowUserToResizeRows = false, - AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill, - RowHeadersVisible = false, - SelectionMode = DataGridViewSelectionMode.FullRowSelect, - MultiSelect = false, - GridColor = BorderColor - }; - - dgvTargets.ColumnHeadersDefaultCellStyle = new DataGridViewCellStyle - { - BackColor = Color.FromArgb(24, 34, 52), - ForeColor = MutedTextColor, - Font = new Font("Segoe UI", 8.5f, FontStyle.Bold), - Alignment = DataGridViewContentAlignment.MiddleLeft - }; - - dgvTargets.DefaultCellStyle = new DataGridViewCellStyle - { - BackColor = CardColor, - ForeColor = TextColor, - SelectionBackColor = Color.FromArgb(40, 57, 82), - SelectionForeColor = TextColor, - Font = new Font("Segoe UI", 9), - Padding = new Padding(4, 0, 4, 0) - }; - - dgvTargets.AlternatingRowsDefaultCellStyle = new DataGridViewCellStyle - { - BackColor = Color.FromArgb(28, 39, 57), - ForeColor = TextColor - }; - - dgvTargets.ColumnHeadersHeight = 34; - dgvTargets.RowTemplate.Height = 32; - - dgvTargets.Columns.Add(new DataGridViewCheckBoxColumn - { - Name = "Selected", - HeaderText = "", - FillWeight = 25 - }); - - dgvTargets.Columns.Add("Target", "Ziel"); - dgvTargets.Columns.Add("Environment", "Umgebung"); - dgvTargets.Columns.Add("Container", "Container / Service"); - dgvTargets.Columns.Add("Tls", "TLS-Prüfung"); - dgvTargets.Columns.Add("Status", "Status"); - - dgvTargets.CurrentCellDirtyStateChanged += (_, _) => - { - if (dgvTargets.IsCurrentCellDirty - && dgvTargets.CurrentCell is DataGridViewCheckBoxCell) - { - dgvTargets.CommitEdit(DataGridViewDataErrorContexts.Commit); - } - }; - - dgvTargets.CellValueChanged += (_, e) => - { - if (e.ColumnIndex >= 0 - && dgvTargets.Columns[e.ColumnIndex].Name == "Selected") - { - RefreshActionButtonStates(); - } - }; - - card.Resize += (_, _) => - { - dgvTargets.Width = card.Width - 44; - }; - - card.Controls.Add(step); - card.Controls.Add(title); - card.Controls.Add(description); - card.Controls.Add(dgvTargets); - - // ---- Sonic Management Console Discovery ---- - BuildSonicDiscoveryRow(card); - - return card; - } - - private Panel BuildActionPanel() - { - Panel panel = new Panel - { - BackColor = BackgroundColor - }; - - btnCancelRun = CreateSecondaryButton("Abbrechen"); - btnCancelRun.Size = new Size(120, 42); - btnCancelRun.Anchor = AnchorStyles.Top | AnchorStyles.Right; - btnCancelRun.Enabled = false; - btnCancelRun.Click += (_, _) => - { - _runCts?.Cancel(); - SetStatus("Abbruch angefordert…", isError: false); - }; - - btnValidate = CreateSecondaryButton("Prüfung durchführen"); - btnValidate.Size = new Size(175, 42); - btnValidate.Anchor = AnchorStyles.Top | AnchorStyles.Right; - btnValidate.Click += async (_, _) => await RunValidationAsync(); - - btnRestartOnly = CreateSecondaryButton("ESB neu starten"); - btnRestartOnly.Size = new Size(150, 42); - btnRestartOnly.Anchor = AnchorStyles.Top | AnchorStyles.Right; - btnRestartOnly.Click += async (_, _) => await RunRestartOnlyAsync(); - - btnDeploy = CreatePrimaryButton("Deployment starten"); - btnDeploy.Size = new Size(170, 42); - btnDeploy.Anchor = AnchorStyles.Top | AnchorStyles.Right; - btnDeploy.Click += async (_, _) => await RunDeploymentAsync(); - - panel.Controls.Add(btnCancelRun); - panel.Controls.Add(btnValidate); - panel.Controls.Add(btnRestartOnly); - panel.Controls.Add(btnDeploy); - - panel.Resize += (_, _) => - { - btnDeploy.Left = panel.Width - btnDeploy.Width; - btnRestartOnly.Left = btnDeploy.Left - btnRestartOnly.Width - 12; - btnValidate.Left = btnRestartOnly.Left - btnValidate.Width - 12; - btnCancelRun.Left = btnValidate.Left - btnCancelRun.Width - 12; - }; - - RefreshActionButtonStates(); - return panel; - } - - private Panel BuildStatusBar() - { - Panel panel = new Panel - { - Height = 28, - BackColor = BackgroundColor - }; - - lblStatusDot = new Label - { - Text = "●", - ForeColor = GreenColor, - Font = new Font("Segoe UI", 10), - AutoSize = true, - Location = new Point(0, 4) - }; - - lblStatus = new Label - { - Text = "Warte auf Zertifikatsauswahl", - ForeColor = MutedTextColor, - Font = new Font("Segoe UI", 9), - AutoSize = true, - Location = new Point(18, 5) - }; - - panel.Controls.Add(lblStatusDot); - panel.Controls.Add(lblStatus); - - return panel; - } - - private async Task LoadTargetsAsync() - { - try - { - SetStatus("Lade Bereitstellungsziele…", isError: false); - (ITargetRepository repository, string loadMessage) = - await TargetRepositoryFactory.CreateAsync(_settings); - - _loadedTargets = await repository.GetActiveTargetsAsync(); - BindTargetsToGrid(_loadedTargets); - SetStatus(loadMessage, isError: false); - } - catch (Exception ex) - { - _loadedTargets = []; - dgvTargets.Rows.Clear(); - SetStatus($"Ziele konnten nicht geladen werden: {ex.Message}", isError: true); - } - finally - { - RefreshActionButtonStates(); - } - } - - private void BindTargetsToGrid(IReadOnlyList targets) - { - dgvTargets.Rows.Clear(); - - foreach (DeploymentTarget target in targets) - { - int tlsPort = target.TlsPort is > 0 ? target.TlsPort.Value : 443; - string tls = string.IsNullOrWhiteSpace(target.TlsHost) - ? "—" - : $"{target.TlsHost}:{tlsPort}"; - - int rowIndex = dgvTargets.Rows.Add( - false, - target.Name, - target.Environment, - target.ContainerName, - tls, - "Bereit"); - - dgvTargets.Rows[rowIndex].Tag = target; - } - } - - private List GetSelectedTargets() - { - List selected = []; - - foreach (DataGridViewRow row in dgvTargets.Rows) - { - if (row.Cells["Selected"].Value is true && row.Tag is DeploymentTarget target) - { - selected.Add(target); - } - } - - return selected; - } - - private void SetTargetRowStatus(int targetId, string status) - { - foreach (DataGridViewRow row in dgvTargets.Rows) - { - if (row.Tag is DeploymentTarget target && target.Id == targetId) - { - row.Cells["Status"].Value = status; - break; - } - } - } - - private void RefreshActionButtonStates() - { - if (btnValidate is null || btnDeploy is null || btnCancelRun is null) - { - return; - } - - bool idle = !_isOperationRunning; - bool hasCertificate = isCertificateFileLoaded && _loadedCertificateInfo is not null; - bool hasTargets = _loadedTargets.Count > 0; - bool hasSelection = dgvTargets is not null && GetSelectedTargets().Count > 0; - - if (btnSelectFile is not null) - { - btnSelectFile.Enabled = idle; - } - - btnCancelRun.Enabled = _isOperationRunning; - btnValidate.Enabled = idle && hasCertificate && hasTargets; - btnDeploy.Enabled = idle && hasCertificate && hasSelection; - if (btnRestartOnly is not null) - { - btnRestartOnly.Enabled = idle && hasSelection; - } - - if (dgvTargets is not null) - { - dgvTargets.Enabled = idle; - } - - if (btnLoadFromSonic is not null) - { - btnLoadFromSonic.Enabled = idle && _sonicDiscovery.HasConnections; - } - } - - private void SetStatus(string message, bool isError) - { - if (lblStatus is null) - { - return; - } - - lblStatus.Text = message; - lblStatus.ForeColor = isError ? RedColor : MutedTextColor; - if (lblStatusDot is not null) - { - lblStatusDot.ForeColor = isError ? RedColor : (_isOperationRunning ? GoldColor : GreenColor); - } - } - - private void SetOperationRunning(bool running) - { - _isOperationRunning = running; - RefreshActionButtonStates(); - } - - private void ShowPreflightIssues(PreflightValidationResult result, string statusMessage, string caption) - { - string details = string.Join(Environment.NewLine, result.Issues.Select(i => "• " + i.Message)); - SetStatus(statusMessage, isError: true); - MessageBox.Show(this, details, caption, MessageBoxButtons.OK, MessageBoxIcon.Warning); - } - - private Task RunValidationAsync() - { - if (_isOperationRunning) - { - return Task.CompletedTask; - } - - List selected = GetSelectedTargets(); - PreflightValidationResult result = _orchestrator.ValidatePreflight( - txtCertificatePath.Text, - _loadedCertificateInfo, - selected); - - HashSet failedTargetIds = result.Issues - .Where(i => i.TargetId is not null) - .Select(i => i.TargetId!.Value) - .ToHashSet(); - - foreach (DataGridViewRow row in dgvTargets.Rows) - { - if (row.Tag is not DeploymentTarget target) - { - continue; - } - - if (failedTargetIds.Contains(target.Id)) - { - SetTargetRowStatus(target.Id, "Prüfung fehlgeschlagen"); - } - else if (row.Cells["Selected"].Value is true) - { - SetTargetRowStatus(target.Id, "Bereit"); - } - } - - if (!result.IsValid) - { - ShowPreflightIssues(result, "Vorabprüfung fehlgeschlagen.", "Vorabprüfung"); - return Task.CompletedTask; - } - - UpdateToNextStep(3); - SetStatus($"Vorabprüfung ok – {selected.Count} Ziel(e) bereit.", isError: false); MessageBox.Show( this, - $"Alle Voraussetzungen sind erfüllt.\nAusgewählte Ziele: {selected.Count}", - "Vorabprüfung", + string.Join(Environment.NewLine, preflight.Issues.Select(i => i.Message)), + "Neustart blockiert", MessageBoxButtons.OK, - MessageBoxIcon.Information); - return Task.CompletedTask; + MessageBoxIcon.Warning); + return; } - private async Task RunRestartOnlyAsync() - { - if (_isOperationRunning) - { - return; - } - - List selected = GetSelectedTargets(); - PreflightValidationResult preflight = _orchestrator.ValidateRestartOnly(selected); - - if (!preflight.IsValid) - { - ShowPreflightIssues( - preflight, - "Neustart blockiert – Vorabprüfung fehlgeschlagen.", - "ESB neu starten"); - return; - } - - string targetNames = string.Join(", ", selected.Select(t => - string.IsNullOrWhiteSpace(t.ContainerName) ? t.Name : t.ContainerName)); - - DialogResult confirm = MessageBox.Show( + string names = string.Join(", ", selected.Select(t => t.ContainerName)); + if (MessageBox.Show( this, - $"Container wirklich neu starten?\n\n" + - $"Ziele: {targetNames}\n\n" + - "Laut Sonic/CX-Messenger Doku (Management Application API):\n" + - "IAgentProxy.restart – dieselbe Aktion wie Restart in der SMC.\n" + - $"Verbindung: appsettings ConnectionUrl + User/Pass.\n" + - "Kein stopcontainer.bat nötig.\n\n" + - "Voraussetzung: Java + Client-JARs unter SonicHome\\lib (SMC-Installation).", + $"Container wirklich neu starten?\n\n{names}", "ESB neu starten", MessageBoxButtons.YesNo, - MessageBoxIcon.Warning); + MessageBoxIcon.Warning) != DialogResult.Yes) + { + return; + } - if (confirm != DialogResult.Yes) + _runCts?.Dispose(); + _runCts = new CancellationTokenSource(); + SetOperationRunning(true); + SetStatus($"Neustart läuft ({selected.Count})…", false); + + Progress progress = new(u => + { + SetTargetRowStatus(u.TargetId, u.StatusText); + SetStatus(u.StatusText, u.SuccessHint == false); + }); + + try + { + DeploymentRunResult run = await _orchestrator.RestartOnlyAsync(selected, progress, _runCts.Token); + foreach (TargetStepResult r in run.TargetResults) { - return; + SetTargetRowStatus(r.TargetId, r.StatusText); } + string details = string.Join( + Environment.NewLine + Environment.NewLine, + run.TargetResults.Select(r => + $"{r.TargetName}: {r.StatusText}" + + (string.IsNullOrWhiteSpace(r.Detail) ? string.Empty : Environment.NewLine + r.Detail))); + + SetStatus( + run.OverallSuccess ? "Neustart erfolgreich." : "Neustart fehlgeschlagen.", + !run.OverallSuccess); + + MessageBox.Show( + this, + details, + run.OverallSuccess ? "Neustart erfolgreich" : "Neustart fehlgeschlagen", + MessageBoxButtons.OK, + run.OverallSuccess ? MessageBoxIcon.Information : MessageBoxIcon.Warning); + } + catch (OperationCanceledException) + { + SetStatus("Neustart abgebrochen.", true); + } + catch (Exception ex) + { + SetStatus(ex.Message, true); + MessageBox.Show(this, ex.Message, "Neustart", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + SetOperationRunning(false); _runCts?.Dispose(); - _runCts = new CancellationTokenSource(); - SetOperationRunning(true); - UpdateToNextStep(3); - SetStatus($"ESB-Neustart 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.RestartOnlyAsync( - selected, - progress, - _runCts.Token); - - foreach (TargetStepResult targetResult in runResult.TargetResults) - { - SetTargetRowStatus(targetResult.TargetId, targetResult.StatusText); - } - - string details = string.Join( - Environment.NewLine + Environment.NewLine, - runResult.TargetResults.Select(r => - $"{r.TargetName}: {r.StatusText}" + - (string.IsNullOrWhiteSpace(r.Detail) ? string.Empty : Environment.NewLine + r.Detail))); - - SetStatus( - runResult.OverallSuccess - ? $"Neustart verifiziert ({runResult.TargetResults.Count} Ziel(e))." - : "Neustart fehlgeschlagen oder nicht verifiziert.", - isError: !runResult.OverallSuccess); - - MessageBox.Show( - this, - details, - runResult.OverallSuccess ? "Neustart verifiziert" : "Neustart fehlgeschlagen", - MessageBoxButtons.OK, - runResult.OverallSuccess ? MessageBoxIcon.Information : MessageBoxIcon.Warning); - } - catch (OperationCanceledException) - { - SetStatus("Neustart abgebrochen.", isError: true); - } - catch (Exception ex) - { - SetStatus($"Neustart fehlgeschlagen: {ex.Message}", isError: true); - MessageBox.Show(this, ex.Message, "ESB neu starten", MessageBoxButtons.OK, MessageBoxIcon.Error); - } - finally - { - SetOperationRunning(false); - _runCts?.Dispose(); - _runCts = null; - } + _runCts = null; } + } - private async Task RunDeploymentAsync() + private void SetTargetRowStatus(int targetId, string status) + { + foreach (DataGridViewRow row in dgvTargets.Rows) { - if (_isOperationRunning || _loadedCertificateInfo is null) + if (row.Tag is DeploymentTarget t && t.Id == targetId) { - 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; - } - } - - private Panel CreateCard() - { - return new Panel - { - BackColor = CardColor, - BorderStyle = BorderStyle.FixedSingle, - Padding = new Padding(20) - }; - } - - private Label CreateSmallTitle(string text) - { - return new Label - { - Text = text, - ForeColor = BlueColor, - Font = new Font("Segoe UI", 8, FontStyle.Bold), - AutoSize = true - }; - } - - private Label CreateCardTitle(string text) - { - return new Label - { - Text = text, - ForeColor = TextColor, - Font = new Font("Segoe UI", 14, FontStyle.Bold), - AutoSize = true - }; - } - - private Label CreateMutedLabel(string text) - { - return new Label - { - Text = text, - ForeColor = MutedTextColor, - Font = new Font("Segoe UI", 9), - AutoSize = true - }; - } - - private Label CreateValueLabel(string text) - { - return new Label - { - Text = text, - ForeColor = TextColor, - Font = new Font("Segoe UI", 9.5f, FontStyle.Regular), - AutoSize = true - }; - } - - private Button CreatePrimaryButton(string text) - { - Button button = new Button - { - Text = text, - BackColor = GoldColor, - ForeColor = Color.FromArgb(30, 25, 10), - FlatStyle = FlatStyle.Flat, - FlatAppearance = - { - BorderSize = 0, - MouseOverBackColor = Color.FromArgb(250, 204, 21) - }, - Font = new Font("Segoe UI", 9.5f, FontStyle.Bold), - Cursor = Cursors.Hand - }; - - return button; - } - - private Button CreateSecondaryButton(string text) - { - Button button = new Button - { - Text = text, - BackColor = Color.FromArgb(39, 52, 73), - ForeColor = TextColor, - FlatStyle = FlatStyle.Flat, - FlatAppearance = - { - BorderColor = BorderColor, - BorderSize = 1, - MouseOverBackColor = Color.FromArgb(54, 69, 94) - }, - Font = new Font("Segoe UI", 9, FontStyle.Bold), - Cursor = Cursors.Hand - }; - - return button; - } - - // --------------------------------------------------------------- - // Sonic Management Console – Container-Discovery - // --------------------------------------------------------------- - - private void BuildSonicDiscoveryRow(Panel card) - { - Label sonicLabel = new Label - { - Text = "Sonic Management:", - ForeColor = MutedTextColor, - Font = new Font("Segoe UI", 8.5f, FontStyle.Bold), - AutoSize = true, - Location = new Point(22, 108) - }; - - cmbSonicConnection = new ComboBox - { - Location = new Point(160, 104), - Size = new Size(220, 26), - DropDownStyle = ComboBoxStyle.DropDownList, - BackColor = Color.FromArgb(20, 30, 48), - ForeColor = TextColor, - FlatStyle = FlatStyle.Flat, - Font = new Font("Segoe UI", 9) - }; - - foreach (SonicConnection conn in _sonicDiscovery.Connections) - { - cmbSonicConnection.Items.Add(conn.Name); - } - - btnLoadFromSonic = CreateSecondaryButton("Container laden"); - btnLoadFromSonic.Location = new Point(392, 103); - btnLoadFromSonic.Size = new Size(148, 28); - btnLoadFromSonic.Enabled = _sonicDiscovery.HasConnections; - btnLoadFromSonic.Click += async (_, _) => await LoadFromSonicAsync(); - - lblSonicStatus = new Label - { - ForeColor = MutedTextColor, - Font = new Font("Segoe UI", 8.5f), - AutoSize = true, - MaximumSize = new Size(720, 0), - Location = new Point(552, 100) - }; - - cmbSonicConnection.SelectedIndexChanged += (_, _) => RefreshSonicStatusLine(); - - if (cmbSonicConnection.Items.Count > 0) - { - cmbSonicConnection.SelectedIndex = 0; - } - else - { - RefreshSonicStatusLine(); - } - - card.Controls.Add(sonicLabel); - card.Controls.Add(cmbSonicConnection); - card.Controls.Add(btnLoadFromSonic); - card.Controls.Add(lblSonicStatus); - } - - private void RefreshSonicStatusLine(string? suffix = null) - { - if (!_sonicDiscovery.HasConnections) - { - lblSonicStatus.Text = "Keine Sonic-Verbindung in appsettings konfiguriert"; - lblSonicStatus.ForeColor = RedColor; - return; - } - - string? selectedName = cmbSonicConnection?.SelectedItem?.ToString(); - SonicConnection conn = _sonicDiscovery.Connections - .FirstOrDefault(c => string.Equals(c.Name, selectedName, StringComparison.OrdinalIgnoreCase)) - ?? _sonicDiscovery.Connections[0]; - - string sonicHome = string.IsNullOrWhiteSpace(conn.SonicHome) ? "(leer)" : conn.SonicHome.Trim(); - (string home, string? javaExe) = new SonicMfApiExecutor(conn).ResolveRuntimePaths(); - string javaDisplay = string.IsNullOrWhiteSpace(javaExe) - ? "java=? (unter SonicHome/JRE setzen)" - : $"java={javaExe}"; - string baseText = - $"{conn.ManagementModeDisplay} | SonicHome={home} | {javaDisplay}"; - if (!string.IsNullOrWhiteSpace(suffix)) - { - baseText = $"{suffix} | {baseText}"; - } - - lblSonicStatus.Text = baseText; - lblSonicStatus.ForeColor = string.IsNullOrWhiteSpace(javaExe) ? GoldColor : GreenColor; - } - - private static string TruncateStatus(string? text, int max = 120) - { - if (string.IsNullOrWhiteSpace(text)) - { - return "unbekannt"; - } - - string oneLine = text.Replace('\r', ' ').Replace('\n', ' ').Trim(); - return oneLine.Length <= max ? oneLine : oneLine[..max] + "…"; - } - - private async Task LoadFromSonicAsync() - { - if (_isOperationRunning) return; - - string? selectedConnection = cmbSonicConnection?.SelectedItem?.ToString(); - if (string.IsNullOrWhiteSpace(selectedConnection)) return; - - btnLoadFromSonic.Enabled = false; - lblSonicStatus.Text = $"Verbinde mit {selectedConnection}…"; - lblSonicStatus.ForeColor = GoldColor; - - try - { - using CancellationTokenSource cts = new(TimeSpan.FromSeconds(30)); - SonicDiscoveryResult result = await _sonicDiscovery.DiscoverAsync( - selectedConnection, - _loadedTargets, - cts.Token); - - if (!result.Success) - { - RefreshSonicStatusLine($"Fehler: {TruncateStatus(result.ErrorMessage)}"); - lblSonicStatus.ForeColor = RedColor; - SetStatus($"Sonic-Discovery fehlgeschlagen: {result.ErrorMessage}", isError: true); - return; - } - - // Grid mit entdeckten + konfigurierten Containern befüllen - _loadedTargets = result.DiscoveredTargets; - BindTargetsToGrid(result.DiscoveredTargets); - - int total = result.DiscoveredTargets.Count; - int remote = result.RawContainerNames.Count; - string domain = string.IsNullOrWhiteSpace(result.DomainName) - ? selectedConnection - : result.DomainName; - - if (total == 0) - { - RefreshSonicStatusLine($"Verb. ok – 0 Container '{domain}'"); - lblSonicStatus.ForeColor = GoldColor; - SetStatus( - result.ErrorMessage - ?? $"Sonic Domain '{domain}': keine Container. KnownContainers/SonicHome in appsettings prüfen.", - isError: true); - } - else if (remote == 0) - { - RefreshSonicStatusLine($"Verb. ok – 0 remote, {total} Config '{domain}'"); - lblSonicStatus.ForeColor = GoldColor; - SetStatus( - $"Domain '{domain}': Remote leer – {total} Ziel(e) aus appsettings/KnownContainers. " - + (result.ErrorMessage ?? string.Empty), - isError: false); - } - else - { - RefreshSonicStatusLine($"Domain '{domain}': {remote} remote, {total} gesamt"); - SetStatus($"Sonic Domain '{domain}': {total} Ziel(e) geladen ({remote} remote).", isError: false); - } - - MarkUnconfiguredRows(); - } - catch (OperationCanceledException) - { - RefreshSonicStatusLine("Timeout"); - lblSonicStatus.ForeColor = RedColor; - SetStatus($"Sonic-Discovery Timeout ({selectedConnection})", isError: true); - } - catch (Exception ex) - { - RefreshSonicStatusLine($"Fehler: {TruncateStatus(ex.Message)}"); - lblSonicStatus.ForeColor = RedColor; - SetStatus($"Sonic-Discovery Fehler: {ex.Message}", isError: true); - } - finally - { - btnLoadFromSonic.Enabled = _sonicDiscovery.HasConnections && !_isOperationRunning; - RefreshActionButtonStates(); - } - } - - /// - /// Färbt Zeilen mit unvollständiger Konfiguration (kein TargetDirectory) orange ein - /// damit erkennbar ist, dass diese Container noch konfiguriert werden müssen. - /// - private void MarkUnconfiguredRows() - { - foreach (DataGridViewRow row in dgvTargets.Rows) - { - if (row.Tag is not DeploymentTarget target) continue; - - if (string.IsNullOrWhiteSpace(target.TargetDirectory)) - { - row.DefaultCellStyle.ForeColor = GoldColor; - if (row.Cells["Status"] is DataGridViewCell statusCell) - { - statusCell.Value = "⚠ Pfad fehlt"; - } - } + row.Cells["Status"].Value = status; + break; } } } + + private void SetOperationRunning(bool running) + { + _isOperationRunning = running; + btnRestartOnly.Enabled = !running; + btnCancelRun.Enabled = running; + dgvTargets.Enabled = !running; + } + + private void SetStatus(string text, bool isError) + { + lblStatus.Text = text; + lblStatus.ForeColor = isError ? RedColor : MutedTextColor; + } + + private void TrySelectCertificateFile() + { + using OpenFileDialog dialog = new() + { + Title = "Zertifikat wählen", + Filter = "Zertifikate (*.cer;*.crt;*.pem;*.pfx)|*.cer;*.crt;*.pem;*.pfx|Alle Dateien (*.*)|*.*" + }; + + if (dialog.ShowDialog(this) != DialogResult.OK) + { + return; + } + + string path = dialog.FileName; + if (!IsSupportedCertificateFile(path)) + { + MessageBox.Show(this, "Dateityp wird nicht unterstützt (.cer/.crt/.pem/.pfx).", "Zertifikat", + MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + try + { + CertificateInfo info = ReadCertificateWithOptionalPassword(path); + txtCertificatePath.Text = path; + _loadedCertificateInfo = info; + DisplayCertificateInfo(info); + SetStatus("Zertifikat erkannt.", false); + } + catch (Exception ex) + { + _loadedCertificateInfo = null; + SetStatus($"Zertifikat ungültig: {ex.Message}", true); + MessageBox.Show(this, ex.Message, "Zertifikat", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private CertificateInfo ReadCertificateWithOptionalPassword(string path) + { + string ext = Path.GetExtension(path); + if (!ext.Equals(".pfx", StringComparison.OrdinalIgnoreCase)) + { + return ReadCertificate(path, null); + } + + try + { + return ReadCertificate(path, null); + } + catch (CryptographicException) + { + string? pwd = PromptPassword(); + if (pwd is null) + { + throw new InvalidOperationException("PFX-Passwort abgebrochen."); + } + + return ReadCertificate(path, pwd); + } + } + + private string? PromptPassword() + { + using Form dialog = new() + { + Text = "PFX-Passwort", + FormBorderStyle = FormBorderStyle.FixedDialog, + StartPosition = FormStartPosition.CenterParent, + ClientSize = new Size(360, 140), + MaximizeBox = false, + MinimizeBox = false, + BackColor = CardColor, + ForeColor = TextColor + }; + Label label = new() { Text = "Passwort:", Left = 16, Top = 20, AutoSize = true }; + TextBox txt = new() + { + Left = 16, + Top = 48, + Width = 320, + UseSystemPasswordChar = true + }; + Button ok = new() { Text = "OK", DialogResult = DialogResult.OK, Left = 160, Top = 90, Width = 80 }; + Button cancel = new() { Text = "Abbrechen", DialogResult = DialogResult.Cancel, Left = 250, Top = 90, Width = 90 }; + dialog.Controls.AddRange([label, txt, ok, cancel]); + dialog.AcceptButton = ok; + dialog.CancelButton = cancel; + return dialog.ShowDialog(this) == DialogResult.OK ? txt.Text : null; + } + + private void DisplayCertificateInfo(CertificateInfo info) + { + lblCertificateSubject.Text = "Subject: " + info.Subject; + lblCertificateIssuer.Text = "Issuer: " + info.Issuer; + lblCertificateExpiry.Text = "Gültig bis: " + info.ValidUntil.ToLocalTime().ToString("dd.MM.yyyy HH:mm"); + lblCertificateFingerprint.Text = "SHA-256: " + info.FingerprintSha256; + lblCertificateValidity.Text = info.IsCurrentlyValid ? "Status: GÜLTIG" : "Status: UNGÜLTIG / ABGELAUFEN"; + lblCertificateValidity.ForeColor = info.IsCurrentlyValid ? GreenColor : RedColor; + } + + private static CertificateInfo ReadCertificate(string certificatePath, string? pfxPassword) + { + string extension = Path.GetExtension(certificatePath); + using X509Certificate2 certificate = + extension.Equals(".pfx", StringComparison.OrdinalIgnoreCase) + ? new X509Certificate2(certificatePath, pfxPassword, X509KeyStorageFlags.EphemeralKeySet) + : new X509Certificate2(certificatePath); + + string subject = certificate.GetNameInfo(X509NameType.SimpleName, forIssuer: false); + string issuer = certificate.GetNameInfo(X509NameType.SimpleName, forIssuer: true); + if (string.IsNullOrWhiteSpace(subject)) subject = certificate.Subject; + if (string.IsNullOrWhiteSpace(issuer)) issuer = certificate.Issuer; + + string fingerprint = FormatFingerprint(certificate.GetCertHashString(HashAlgorithmName.SHA256)); + DateTimeOffset validFrom = new(certificate.NotBefore); + DateTimeOffset validUntil = new(certificate.NotAfter); + DateTimeOffset now = DateTimeOffset.Now; + + return new CertificateInfo + { + Subject = subject, + Issuer = issuer, + ValidFrom = validFrom, + ValidUntil = validUntil, + FingerprintSha256 = fingerprint, + IsCurrentlyValid = now >= validFrom && now <= validUntil + }; + } + + private static string FormatFingerprint(string fingerprint) + => string.IsNullOrWhiteSpace(fingerprint) + ? string.Empty + : string.Join(":", Enumerable.Range(0, fingerprint.Length / 2) + .Select(i => fingerprint.Substring(i * 2, 2))); + + private static bool IsSupportedCertificateFile(string filePath) + { + if (!File.Exists(filePath)) + { + return false; + } + + string ext = Path.GetExtension(filePath); + return ext.Equals(".cer", StringComparison.OrdinalIgnoreCase) + || ext.Equals(".crt", StringComparison.OrdinalIgnoreCase) + || ext.Equals(".pem", StringComparison.OrdinalIgnoreCase) + || ext.Equals(".pfx", StringComparison.OrdinalIgnoreCase); + } + + private Panel MakeCard() + => new() + { + Dock = DockStyle.Fill, + BackColor = CardColor, + Padding = new Padding(8), + Margin = new Padding(0, 0, 0, 10) + }; + + private Label SectionTitle(string text, int top) + => new() + { + Text = text, + Left = 16, + Top = top, + AutoSize = true, + Font = new Font("Segoe UI Semibold", 11f), + ForeColor = GoldColor + }; + + private Label MetaLabel(int left, int top, string text) + => new() + { + Text = text, + Left = left, + Top = top, + AutoSize = true, + ForeColor = MutedTextColor + }; + + private Button MakeButton(string text, Color back) + => new() + { + Text = text, + Height = 32, + FlatStyle = FlatStyle.Flat, + BackColor = back, + ForeColor = TextColor, + FlatAppearance = { BorderSize = 0 }, + Cursor = Cursors.Hand + }; } diff --git a/ZA.CoreService.ESBCertificateManager/Models/AppSettings.cs b/ZA.CoreService.ESBCertificateManager/Models/AppSettings.cs index 68e8635..da5d5d0 100644 --- a/ZA.CoreService.ESBCertificateManager/Models/AppSettings.cs +++ b/ZA.CoreService.ESBCertificateManager/Models/AppSettings.cs @@ -2,16 +2,8 @@ namespace ZA.CoreService.ESBCertificateManager.Models; public sealed class AppSettings { - public string ConnectionString { get; set; } = string.Empty; public bool UseOfflineSampleData { get; set; } = true; public string SampleTargetsPath { get; set; } = "Data/targets.sample.json"; public string LogDirectory { get; set; } = "Logs"; - public int TlsTimeoutSeconds { get; set; } = 8; - public int TlsRetryCount { get; set; } = 2; - - /// - /// Verbindungskonfigurationen für Progress Sonic ESB Management Instanzen. - /// Jeder Eintrag entspricht einer Sonic-Domain (z.B. einer Umgebung oder Tochtergesellschaft). - /// public List SonicConnections { get; set; } = []; } diff --git a/ZA.CoreService.ESBCertificateManager/Models/DeploymentRunResult.cs b/ZA.CoreService.ESBCertificateManager/Models/DeploymentRunResult.cs index e0d49b7..cc89dd9 100644 --- a/ZA.CoreService.ESBCertificateManager/Models/DeploymentRunResult.cs +++ b/ZA.CoreService.ESBCertificateManager/Models/DeploymentRunResult.cs @@ -7,8 +7,6 @@ public sealed class DeploymentRunResult public bool OverallSuccess => TargetResults.All(r => r.Success); public DateTimeOffset StartedAt { get; init; } public DateTimeOffset FinishedAt { get; init; } - public string CertificateFilePath { get; init; } = string.Empty; - public string CertificateFingerprint { get; init; } = string.Empty; } public sealed class TargetStepResult @@ -18,13 +16,8 @@ public sealed class TargetStepResult public bool Success { get; init; } public required string StatusText { get; init; } public string? Detail { get; init; } - public IReadOnlyList Steps { get; init; } = []; public DateTimeOffset StartedAt { get; init; } public DateTimeOffset FinishedAt { get; init; } - public bool CopySucceeded { get; init; } - public bool RestartSucceeded { get; init; } - public bool TlsSucceeded { get; init; } - public string? ObservedFingerprint { get; init; } } public sealed class ValidationIssue @@ -38,3 +31,5 @@ public sealed class PreflightValidationResult public bool IsValid => Issues.Count == 0; public List Issues { get; } = []; } + +public readonly record struct TargetProgressUpdate(int TargetId, string StatusText, bool? SuccessHint); diff --git a/ZA.CoreService.ESBCertificateManager/Models/DeploymentTarget.cs b/ZA.CoreService.ESBCertificateManager/Models/DeploymentTarget.cs index f651cf3..f2e699e 100644 --- a/ZA.CoreService.ESBCertificateManager/Models/DeploymentTarget.cs +++ b/ZA.CoreService.ESBCertificateManager/Models/DeploymentTarget.cs @@ -4,64 +4,9 @@ public sealed class DeploymentTarget { public int Id { get; init; } public required string Name { get; init; } - public required string Environment { get; init; } + public string Environment { get; init; } = "TEST"; public bool IsActive { get; init; } = true; - public required string TargetDirectory { get; init; } - public required string CertificateFileName { get; init; } - - /// Name des Sonic-ESB-Containers (z.B. "sonic-container-a"). public string ContainerName { get; init; } = string.Empty; - - public RestartType RestartType { get; init; } = RestartType.None; - - // --- Command-basierter Neustart --- - public string RestartCommand { get; init; } = string.Empty; - public string RestartArguments { get; init; } = string.Empty; - public int RestartTimeoutSeconds { get; init; } = 60; - - // --- Sonic-ESB-Management-Neustart --- - /// - /// Referenz auf den Namen einer in AppSettings. - /// Pflichtfeld bei RestartType = SonicContainer oder SonicContainerWithXapi. - /// public string SonicConnectionName { get; init; } = string.Empty; - - /// - /// Pfad zur XApi-Ressourcendatei (.xml/.zip), die vor dem Neustart importiert wird. - /// Pflichtfeld bei RestartType = SonicContainerWithXapi. - /// - public string XapiSourcePath { get; init; } = string.Empty; - - // --- TLS-Probe nach Deployment --- - public string TlsHost { get; init; } = string.Empty; - public int? TlsPort { get; init; } - - /// - /// Hostname, der im TLS-Handshake als ServerName (SNI) verwendet wird. - /// Wichtig wenn TlsHost eine IP-Adresse ist, das Zertifikat aber einen DNS-Namen trägt. - /// Ist leer, wird TlsHost als ServerName verwendet. - /// - public string TlsServerName { get; init; } = string.Empty; - - /// - /// Erwarteter SHA-256-Fingerprint des Zertifikats nach dem Deployment (ohne Trennzeichen). - /// Wenn gesetzt, schlägt der TLS-Probe fehl wenn der Fingerprint abweicht. - /// - public string? ExpectedFingerprint { get; init; } - public int SortOrder { get; init; } } - -public enum RestartType -{ - None = 0, - - /// Neustart über einen lokalen Betriebssystem-Prozess (RestartCommand). - Command = 1, - - /// Container-Neustart wie in der Sonic Management Console (stop/startcontainer bzw. SMC-API). - SonicContainer = 2, - - /// XApi-Ressourcen importieren und danach Container neu starten (Sonic ESB). - SonicContainerWithXapi = 3 -} diff --git a/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs b/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs index 8f2d30f..48d77d7 100644 --- a/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs +++ b/ZA.CoreService.ESBCertificateManager/Models/SonicConnection.cs @@ -1,398 +1,22 @@ namespace ZA.CoreService.ESBCertificateManager.Models; /// -/// Verbindungskonfiguration für eine Progress Sonic ESB Management Instanz. -/// Name = Verbindungs-Alias (hier oft gleich ContainerName, z.B. "DE-Test"). -/// DomainName = Sonic-Domain (z.B. "proalpha-test"). -/// Container wird in KnownContainers / DeploymentTarget.ContainerName geführt (z.B. "DE-Test"). +/// SMC-/Domain-Manager-Verbindung für MfApi-Neustart. +/// Name = Alias (z.B. DE-Test), ContainerName separat (z.B. ct-ZADBService). /// public sealed class SonicConnection { - /// - /// Alias der Verbindung; referenziert von . - /// public required string Name { get; init; } - - /// Sonic-Domain, z.B. "proalpha-test" (nicht der Containername). public required string DomainName { get; init; } - - /// - /// Dieselbe Broker-/Management-URL wie in der Sonic Management Console (SMC), - /// z.B. "tcp://dekun-painwbdet:13070". Keine separate Domain-Console nötig. - /// public required string ConnectionUrl { get; init; } - - /// Login wie in der Sonic Management Console (nicht für WinRM). public required string Username { get; init; } - - /// Passwort wie in der Sonic Management Console (nicht für WinRM). public required string Password { get; init; } - /// - /// Windows-Konto für WinRM (Invoke-Command -Credential). - /// Leer = aktueller Prozess-Benutzer ohne explizite Credentials. - /// Nicht mit / (Sonic SMC) verwechseln. - /// - public string WinRmUsername { get; init; } = string.Empty; - - /// Windows-Passwort für WinRM; nur relevant wenn gesetzt ist. - public string WinRmPassword { get; init; } = string.Empty; - - /// - /// MfApi = Sonic MF Management Runtime API über ConnectionUrl + SMC-Logins (Standard); - /// WinRm / LocalCmd = optionaler Fallback über stopcontainer/startcontainer; - /// HttpApi = REST (falls vorhanden). - /// - /// - /// Standard: MfApi = Management Application API (wie SMC): - /// JMSConnectorClient + MFProxyFactory.createAgentProxy + IAgentProxy.restart. - /// Laut CX-Messenger-Doku-Index: Docs2017/api/mgmt_api (nicht stopcontainer.bat). - /// LocalCmd nur wenn Server-Scripts existieren (bei reiner SMC oft nicht der Fall). - /// - public SonicManagementMode ManagementMode { get; init; } = SonicManagementMode.MfApi; - - /// - /// Effektiver Modus: bei WinRm und lokalem ConnectionUrl-Host wird LocalCmd erzwungen. - /// MfApi bleibt unverändert (nutzt Domain-Manager-Verbindung). - /// - public SonicManagementMode EffectiveManagementMode - => ManagementMode == SonicManagementMode.WinRm && IsConnectionHostLocal(ConnectionUrl) - ? SonicManagementMode.LocalCmd - : ManagementMode; - - /// True wenn WinRm konfiguriert war, aber wegen lokalem Host auf LocalCmd umgestellt wurde. - public bool IsLocalCmdAutoForced - => ManagementMode == SonicManagementMode.WinRm - && EffectiveManagementMode == SonicManagementMode.LocalCmd; - - /// Anzeigetext für UI (inkl. Auto-Erkennung). - public string ManagementModeDisplay - => IsLocalCmdAutoForced - ? "LocalCmd (Host lokal erkannt)" - : EffectiveManagementMode switch - { - SonicManagementMode.MfApi => "MfApi (SMC Management Application API)", - SonicManagementMode.LocalCmd => "LocalCmd (nur wenn stopcontainer.bat existiert)", - _ => EffectiveManagementMode.ToString() - }; - - /// - /// True wenn der Host aus ConnectionUrl dieser Maschine entspricht - /// (localhost / 127.0.0.1 / ::1 / Computername). - /// - public static bool IsConnectionHostLocal(string? connectionUrl) - { - if (string.IsNullOrWhiteSpace(connectionUrl)) - { - return false; - } - - string host; - try - { - host = new Uri(connectionUrl).Host; - } - catch - { - host = connectionUrl.Trim(); - } - - if (string.IsNullOrWhiteSpace(host)) - { - return false; - } - - if (host.Equals("localhost", StringComparison.OrdinalIgnoreCase) - || host.Equals("127.0.0.1", StringComparison.OrdinalIgnoreCase) - || host.Equals("::1", StringComparison.OrdinalIgnoreCase) - || host.Equals("[::1]", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - string machine = Environment.MachineName; - if (host.Equals(machine, StringComparison.OrdinalIgnoreCase) - || host.StartsWith(machine + ".", StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - try - { - string dnsName = System.Net.Dns.GetHostName(); - if (!string.IsNullOrWhiteSpace(dnsName) - && (host.Equals(dnsName, StringComparison.OrdinalIgnoreCase) - || host.StartsWith(dnsName + ".", StringComparison.OrdinalIgnoreCase))) - { - return true; - } - } - catch - { - // DNS optional - } - - return false; - } - - /// - /// Sonic-/MQ-/ESB-Installationsroot, z.B. "C:\Sonic\MQ10.0" (Client-JARs oft unter lib). - /// Bei ESB-Home: ggf. MfClientLibPath auf MQ\lib setzen. - /// - public string SonicHome { get; init; } = @"C:\Sonic\MQ10.0"; - - /// - /// Optional: JRE/JDK-Home (z.B. C:\Sonic\MQ10.0\jre) oder Pfad zu java.exe. - /// Leer = automatische Suche (JAVA_HOME/JRE_HOME, setenv.bat, rekursiv unter SonicHome, Registry, …). - /// + public string SonicHome { get; init; } = @"C:\DEV\MQ10.0"; public string JavaHome { get; init; } = string.Empty; - - /// - /// Optionaler direkter Pfad zu java.exe (überschreibt JavaHome wenn gesetzt). - /// public string JavaPath { get; init; } = string.Empty; - - /// - /// Optionaler Pfad zu Sonic-Client-JARs für MfApi (mgmt_client.jar etc.). - /// Leer = SonicHome\lib, MQ_HOME\lib, ESB_HOME\lib bzw. nested MQ*/lib. - /// public string MfClientLibPath { get; init; } = string.Empty; - - /// - /// Container-Namen (kurz), z.B. ["DE-Test"]. Nicht die Domain. - /// public List KnownContainers { get; init; } = []; - - public int ManagementHttpPort { get; init; } = 8080; - public string ApiBasePath { get; init; } = "/api/v1"; - public string ContainerListPath { get; init; } = string.Empty; - public string ContainerRestartPath { get; init; } = string.Empty; - public string ContainerStopPath { get; init; } = string.Empty; - public string ContainerStartPath { get; init; } = string.Empty; - - public int WinRmPort { get; init; } = 5985; - public string WinRmRestartScript { get; init; } = string.Empty; - public string WinRmContainerListScript { get; init; } = string.Empty; - public string WinRmXapiImportScript { get; init; } = string.Empty; - public int TimeoutSeconds { get; init; } = 120; public int PostRestartDelaySeconds { get; init; } = 20; - - public string ResolveRestartScript() - => string.IsNullOrWhiteSpace(WinRmRestartScript) - ? DefaultRestartScript - : WinRmRestartScript; - - public string ResolveContainerListScript() - => string.IsNullOrWhiteSpace(WinRmContainerListScript) - ? DefaultContainerListScript - : WinRmContainerListScript; - - /// - /// Offizieller MF-Container-Neustart laut Aurea CX Messenger Doku: - /// SonicHome\bin\stopcontainer.bat Domain.Container - /// SonicHome\bin\startcontainer.bat Domain.Container - /// Danach Prozess-Verifikation (alte PIDs weg, neue PIDs da). - /// - public const string DefaultRestartScript = - """ - $ErrorActionPreference = 'Stop' - $sonicHome = '{sonicHome}' - $domain = '{domain}' - $container = '{container}' - - $bin = Join-Path $sonicHome 'bin' - $stopBat = Join-Path $bin 'stopcontainer.bat' - $startBat = Join-Path $bin 'startcontainer.bat' - - # Kurzname = Container (z.B. DE-Test), Full = Domain.Container (z.B. proalpha-test.DE-Test) - $shortName = if ($container -like '*.*') { ($container -split '\.', 2)[1] } else { $container } - $fullName = if ($container -like '*.*') { $container } else { "$domain.$container" } - $markers = @($shortName, $fullName, "$domain.$shortName") | Select-Object -Unique - - function Get-ContainerPids { - $pids = @() - Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { - $cmd = $_.CommandLine - if (-not $cmd) { return $false } - if ($_.Name -notmatch 'java|javaw|sonic') { return $false } - foreach ($m in $markers) { - if ($cmd -like "*$m*") { return $true } - } - return $false - } | - ForEach-Object { $pids += [int]$_.ProcessId } - return @($pids | Select-Object -Unique) - } - - function Wait-PidsGone([int[]]$pids, [int]$seconds) { - $deadline = (Get-Date).AddSeconds($seconds) - while ((Get-Date) -lt $deadline) { - $alive = @($pids | Where-Object { Get-Process -Id $_ -ErrorAction SilentlyContinue }) - if ($alive.Count -eq 0) { return $true } - Start-Sleep -Seconds 2 - } - return $false - } - - function Wait-NewPids([int[]]$oldPids, [int]$seconds) { - $deadline = (Get-Date).AddSeconds($seconds) - while ((Get-Date) -lt $deadline) { - $now = @(Get-ContainerPids) - $fresh = @($now | Where-Object { $oldPids -notcontains $_ }) - if ($fresh.Count -gt 0) { return ,$fresh } - Start-Sleep -Seconds 2 - } - return ,@() - } - - function Invoke-ContainerBat([string]$bat, [string]$name) { - if (-not (Test-Path -LiteralPath $bat)) { - throw "Sonic BAT fehlt: $bat (SonicHome pruefen)" - } - $arg = '/c "' + $bat + '" "' + $name + '"' - $p = Start-Process -FilePath 'cmd.exe' -ArgumentList $arg -Wait -PassThru -NoNewWindow - return [int]$p.ExitCode - } - - Write-Output "INFO:Domain=$domain Container=$shortName Full=$fullName" - Write-Output "INFO:RestartVia=stopcontainer/startcontainer (MF Container)" - $before = @(Get-ContainerPids) - Write-Output "INFO:PIDsBefore=$($before -join ',')" - - if ($before.Count -eq 0) { - Write-Output "WARN:No running Java/Sonic process for '$shortName' - starting anyway" - } - - # 1) Offiziell: stopcontainer.bat Domain.Container (danach Kurzname als Fallback) - $stopOk = $false - foreach ($n in @($fullName, $shortName)) { - Write-Output "INFO:stopcontainer $n" - $code = Invoke-ContainerBat -bat $stopBat -name $n - Write-Output "INFO:stopcontainer ExitCode=$code Name=$n" - if ($code -eq 0) { $stopOk = $true; break } - } - if (-not $stopOk) { - Write-Output "WARN:stopcontainer non-zero; will force-stop remaining PIDs if any" - } - - Start-Sleep -Seconds 3 - $still = @(Get-ContainerPids) - - # 2) Force-Stop falls BAT den Prozess nicht beendet (sonst kein echter Restart in SMC) - if ($still.Count -gt 0) { - Write-Output "INFO:Force-Stop PIDs=$($still -join ',')" - foreach ($procId in $still) { - Stop-Process -Id $procId -Force -ErrorAction SilentlyContinue - } - } - - if ($before.Count -gt 0) { - if (-not (Wait-PidsGone -pids $before -seconds 45)) { - throw "Container process still running after stop (PIDs=$($before -join ','))" - } - Write-Output "INFO:Old PIDs gone" - } - - Start-Sleep -Seconds 3 - - # 3) Offiziell: startcontainer.bat Domain.Container - $started = $false - foreach ($n in @($fullName, $shortName)) { - Write-Output "INFO:startcontainer $n" - $code = Invoke-ContainerBat -bat $startBat -name $n - Write-Output "INFO:startcontainer ExitCode=$code Name=$n" - if ($code -eq 0) { $started = $true; break } - } - - if (-not $started) { - throw "startcontainer failed for $fullName / $shortName" - } - - $after = @(Wait-NewPids -oldPids $before -seconds 60) - if ($after.Count -eq 0) { - $any = @(Get-ContainerPids) - if ($any.Count -eq 0) { - throw "After start no process for container '$shortName'. Check SMC / SonicHome / ContainerName." - } - Write-Output "INFO:PIDsAfter=$($any -join ',')" - } - else { - Write-Output "INFO:PIDsAfter=$($after -join ',')" - } - - Write-Output "OK:ContainerRestartVerified Domain=$domain Container=$shortName" - """; - - public const string DefaultContainerListScript = - """ - $ErrorActionPreference = 'Continue' - $sonicHome = '{sonicHome}' - $domain = '{domain}' - $names = New-Object System.Collections.Generic.List[string] - - function Add-Name([string]$n) { - if ([string]::IsNullOrWhiteSpace($n)) { return } - $n = $n.Trim() - if (-not $names.Contains($n)) { [void]$names.Add($n) } - } - - if (-not (Test-Path -LiteralPath $sonicHome)) { - Write-Output "WARN:SonicHomeNichtGefunden:$sonicHome" - } - else { - Write-Output "INFO:SonicHomeOk:$sonicHome Domain=$domain" - - Get-ChildItem -LiteralPath $sonicHome -Directory -ErrorAction SilentlyContinue | - ForEach-Object { - if ($_.Name -like '*.cache') { - Add-Name ($_.Name -replace '\.cache$', '') - } - Get-ChildItem -LiteralPath $_.FullName -Directory -ErrorAction SilentlyContinue | - Where-Object { $_.Name -like '*.cache' } | - ForEach-Object { Add-Name ($_.Name -replace '\.cache$', '') } - } - - Get-ChildItem -LiteralPath $sonicHome -Recurse -Filter 'container.xml' -File -ErrorAction SilentlyContinue | - Select-Object -First 40 | - ForEach-Object { - Add-Name (Split-Path $_.DirectoryName -Leaf) - } - } - - Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | - Where-Object { $_.CommandLine -match 'sonic|mf\.framework|container\.xml' } | - ForEach-Object { - if ($_.CommandLine -match '([A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+)\.cache') { - Add-Name $Matches[1] - } - elseif ($_.CommandLine -match '\\([A-Za-z0-9_\-]+)\\container\.xml') { - Add-Name $Matches[1] - } - } - - # Kurzform Domain.Container → Container - foreach ($n in @($names.ToArray())) { - if ($n -like '*.*') { - $parts = $n -split '\.', 2 - if ($parts.Count -eq 2) { Add-Name $parts[1] } - } - } - - if ($names.Count -eq 0) { - Write-Output "WARN:KeineContainerGefunden Domain=$domain SonicHome=$sonicHome" - } - - $names | Sort-Object -Unique - """; -} - -public enum SonicManagementMode -{ - HttpApi = 0, - WinRm = 1, - LocalCmd = 2, - /// Sonic MF Management API über Domain-Manager (ConnectionUrl + SMC-Credentials). - MfApi = 3 } 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..da19a8d 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; +/// +/// Nur Container-Neustart (kein Deploy, kein TLS, kein XApi). +/// 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,12 +29,30 @@ 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) { cancellationToken.ThrowIfCancellationRequested(); - results.Add(await RestartTargetOnlyAsync(target, logger, progress, cancellationToken)); + DateTimeOffset targetStart = DateTimeOffset.Now; + progress?.Report(new TargetProgressUpdate(target.Id, $"Neustart {target.ContainerName}…", null)); + + (bool ok, string status, string? detail) = + await _restartExecutor.ExecuteAsync(target, cancellationToken); + + logger.Write($"[{target.Name}] {status} | {detail}"); + progress?.Report(new TargetProgressUpdate(target.Id, status, ok)); + + results.Add(new TargetStepResult + { + TargetId = target.Id, + TargetName = target.Name, + Success = ok, + StatusText = status, + Detail = detail, + StartedAt = targetStart, + FinishedAt = DateTimeOffset.Now + }); } DateTimeOffset finishedAt = DateTimeOffset.Now; @@ -54,210 +60,12 @@ public sealed class DeploymentOrchestrator { TargetResults = results, StartedAt = startedAt, - FinishedAt = finishedAt, - CertificateFilePath = string.Empty, - CertificateFingerprint = string.Empty + FinishedAt = finishedAt }; 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; } - - private async Task RestartTargetOnlyAsync( - DeploymentTarget target, - RunLogger logger, - IProgress? progress, - CancellationToken cancellationToken) - { - List steps = []; - DateTimeOffset targetStart = DateTimeOffset.Now; - progress?.Report(new TargetProgressUpdate(target.Id, "Neustart…", false)); - - (bool restartOk, string restartStatus, string? restartDetail) = - await _restartExecutor.ExecuteAsync(target, cancellationToken); - - RecordStep(steps, logger, target.Name, "Restart", restartStatus, restartDetail); - - TargetStepResult targetResult = new() - { - TargetId = target.Id, - TargetName = target.Name, - Success = restartOk, - StatusText = restartStatus, - Detail = restartDetail, - Steps = steps, - StartedAt = targetStart, - FinishedAt = DateTimeOffset.Now, - CopySucceeded = true, - RestartSucceeded = restartOk, - TlsSucceeded = true, - ObservedFingerprint = null - }; - - progress?.Report(new TargetProgressUpdate(target.Id, restartStatus, restartOk)); - return targetResult; - } - - public async Task 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/SonicContainerDiscovery.cs b/ZA.CoreService.ESBCertificateManager/Services/SonicContainerDiscovery.cs index d03fe4b..8e73a8e 100644 --- a/ZA.CoreService.ESBCertificateManager/Services/SonicContainerDiscovery.cs +++ b/ZA.CoreService.ESBCertificateManager/Services/SonicContainerDiscovery.cs @@ -2,10 +2,6 @@ using ZA.CoreService.ESBCertificateManager.Models; namespace ZA.CoreService.ESBCertificateManager.Services; -/// -/// Fragt konfigurierte Sonic-Verbindungen nach Containern ab -/// und merged Remote-Treffer mit KnownContainers / Sample-Zielen. -/// public sealed class SonicContainerDiscovery { private readonly IReadOnlyList _connections; @@ -29,228 +25,84 @@ public sealed class SonicContainerDiscovery if (connection is null) { return SonicDiscoveryResult.Failed(connectionName, - $"Sonic-Verbindung '{connectionName}' ist nicht in AppSettings konfiguriert."); + $"Sonic-Verbindung '{connectionName}' fehlt in appsettings."); } using SonicManagementClient client = new(connection); + (bool reachable, string? pingError, _) = await client.CheckConnectionAsync(cancellationToken); - (bool reachable, string? pingError, string? resolvedPath) = await client.CheckConnectionAsync(cancellationToken); - - List diagnostics = []; List remoteContainers = []; - bool listOk = false; - string? listError = null; + List diagnostics = []; if (!reachable) { - // Trotzdem KnownContainers nutzen – Neustart kann über SonicBin (stop/startcontainer) gehen. - diagnostics.Add($"Management-Ping: {pingError}"); - if (connection.KnownContainers.Count == 0 - && !knownTargets.Any(t => - string.Equals(t.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase) - && !string.IsNullOrWhiteSpace(t.ContainerName))) - { - return SonicDiscoveryResult.Failed(connectionName, - $"Management-Konsole nicht erreichbar: {pingError}"); - } + diagnostics.Add($"Ping: {pingError}"); } else { - (listOk, IReadOnlyList remoteNames, listError) = + (bool listOk, IReadOnlyList remoteNames, string? listError) = await client.GetContainersAsync(cancellationToken); - - foreach (string line in remoteNames) + if (listOk) { - if (line.StartsWith("WARN:", StringComparison.OrdinalIgnoreCase) - || line.StartsWith("INFO:", StringComparison.OrdinalIgnoreCase)) - { - diagnostics.Add(line); - continue; - } - - remoteContainers.Add(line); + remoteContainers.AddRange(remoteNames.Where(n => + !n.StartsWith("WARN:", StringComparison.OrdinalIgnoreCase) + && !n.StartsWith("INFO:", StringComparison.OrdinalIgnoreCase))); } - - if (!listOk) + else if (!string.IsNullOrWhiteSpace(listError)) { - diagnostics.Add($"Listen-Fehler: {listError}"); - remoteContainers = []; + diagnostics.Add(listError); } } - // Fallback: explizit in appsettings hinterlegte Container - List mergedNames = MergeContainerNames( - remoteContainers, - connection.KnownContainers, - knownTargets - .Where(t => string.Equals(t.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase)) - .Select(t => t.ContainerName) - .Where(n => !string.IsNullOrWhiteSpace(n))); + List merged = []; + foreach (string name in remoteContainers + .Concat(connection.KnownContainers) + .Concat(knownTargets + .Where(t => string.Equals(t.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase)) + .Select(t => t.ContainerName)) + .Where(n => !string.IsNullOrWhiteSpace(n))) + { + if (!merged.Any(m => string.Equals(m, name, StringComparison.OrdinalIgnoreCase))) + { + merged.Add(name); + } + } - List discovered = BuildTargets(connection, mergedNames, knownTargets); + if (merged.Count == 0) + { + return SonicDiscoveryResult.Failed(connectionName, + string.Join(" | ", diagnostics.DefaultIfEmpty("Keine Container gefunden."))); + } - string? hint = BuildHint(connection, remoteContainers.Count, connection.KnownContainers.Count, diagnostics, listOk, listError); + int id = 1; + List targets = merged.Select(name => + { + DeploymentTarget? known = knownTargets.FirstOrDefault(t => + string.Equals(t.ContainerName, name, StringComparison.OrdinalIgnoreCase) + && string.Equals(t.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase)); + + return new DeploymentTarget + { + Id = known?.Id ?? id++, + Name = known?.Name ?? $"{connection.Name} / {name}", + Environment = known?.Environment ?? "TEST", + IsActive = true, + ContainerName = name, + SonicConnectionName = connection.Name, + SortOrder = known?.SortOrder ?? id * 10 + }; + }).ToList(); return new SonicDiscoveryResult { ConnectionName = connectionName, DomainName = connection.DomainName, Success = true, - ErrorMessage = hint, - DiscoveredTargets = discovered, - RawContainerNames = remoteContainers, - ResolvedPath = resolvedPath + ErrorMessage = diagnostics.Count == 0 ? null : string.Join(" | ", diagnostics), + DiscoveredTargets = targets, + RawContainerNames = remoteContainers }; } - - private static List MergeContainerNames( - IEnumerable remote, - IEnumerable knownConfigured, - IEnumerable knownFromTargets) - { - List result = []; - - foreach (string name in remote.Concat(knownConfigured).Concat(knownFromTargets)) - { - if (string.IsNullOrWhiteSpace(name)) - { - continue; - } - - if (!result.Any(existing => ContainerNamesMatch(existing, name, domain: null))) - { - result.Add(name.Trim()); - } - } - - return result; - } - - private static string? BuildHint( - SonicConnection connection, - int remoteCount, - int knownConfigCount, - List diagnostics, - bool listOk, - string? listError) - { - List parts = []; - - if (!listOk && !string.IsNullOrWhiteSpace(listError)) - { - parts.Add(listError!); - } - - if (remoteCount == 0) - { - parts.Add( - $"Remote hat 0 Container geliefert (Domain '{connection.DomainName}', SonicHome='{connection.SonicHome}')."); - - if (knownConfigCount > 0) - { - parts.Add($"Fallback: {knownConfigCount} KnownContainers aus appsettings."); - } - else - { - parts.Add("Tipp: KnownContainers in appsettings setzen oder SonicHome korrigieren."); - } - } - - foreach (string d in diagnostics.Take(3)) - { - parts.Add(d); - } - - return parts.Count == 0 ? null : string.Join(" ", parts); - } - - private static List BuildTargets( - SonicConnection connection, - IReadOnlyList containerNames, - IReadOnlyList knownTargets) - { - List result = []; - int syntheticId = -1; - - foreach (string containerName in containerNames) - { - DeploymentTarget? existing = knownTargets.FirstOrDefault(t => - string.Equals(t.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase) - && ContainerNamesMatch(t.ContainerName, containerName, connection.DomainName)); - - if (existing is not null) - { - if (!result.Any(r => r.Id == existing.Id)) - { - result.Add(existing); - } - } - else - { - result.Add(new DeploymentTarget - { - Id = syntheticId--, - Name = $"{connection.Name} / {containerName}", - Environment = connection.DomainName, - IsActive = true, - TargetDirectory = string.Empty, - CertificateFileName = string.Empty, - ContainerName = containerName, - RestartType = RestartType.SonicContainer, - SonicConnectionName = connection.Name, - SortOrder = result.Count * 10 - }); - } - } - - foreach (DeploymentTarget known in knownTargets) - { - if (!string.Equals(known.SonicConnectionName, connection.Name, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - bool alreadyAdded = result.Any(r => - ContainerNamesMatch(r.ContainerName, known.ContainerName, connection.DomainName) - || r.Id == known.Id); - - if (!alreadyAdded) - { - result.Add(known); - } - } - - return result; - } - - internal static bool ContainerNamesMatch(string? a, string? b, string? domain) - { - if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b)) - { - return false; - } - - if (string.Equals(a, b, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - string aShort = StripDomainPrefix(a, domain); - string bShort = StripDomainPrefix(b, domain); - return string.Equals(aShort, bShort, StringComparison.OrdinalIgnoreCase); - } - - private static string StripDomainPrefix(string name, string? domain) - { - if (!string.IsNullOrWhiteSpace(domain) - && name.StartsWith(domain + ".", StringComparison.OrdinalIgnoreCase)) - { - return name[(domain.Length + 1)..]; - } - - int dot = name.IndexOf('.'); - return dot > 0 ? name[(dot + 1)..] : name; - } } public sealed class SonicDiscoveryResult @@ -259,10 +111,14 @@ public sealed class SonicDiscoveryResult public string DomainName { get; init; } = string.Empty; public bool Success { get; init; } public string? ErrorMessage { get; init; } - public string? ResolvedPath { get; init; } public IReadOnlyList DiscoveredTargets { get; init; } = []; public IReadOnlyList RawContainerNames { get; init; } = []; public static SonicDiscoveryResult Failed(string connectionName, string error) - => new() { ConnectionName = connectionName, Success = false, ErrorMessage = error }; + => new() + { + ConnectionName = connectionName, + Success = false, + ErrorMessage = error + }; } diff --git a/ZA.CoreService.ESBCertificateManager/Services/SonicManagementClient.cs b/ZA.CoreService.ESBCertificateManager/Services/SonicManagementClient.cs index e349c0c..bcd937b 100644 --- a/ZA.CoreService.ESBCertificateManager/Services/SonicManagementClient.cs +++ b/ZA.CoreService.ESBCertificateManager/Services/SonicManagementClient.cs @@ -1,339 +1,67 @@ -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. +/// Dünne Fassade über die Sonic MfApi (wie SMC-Neustart). /// 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( + public async Task<(bool Reachable, string? Error, string? Detail)> 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) + // Soft-Fallback: KnownContainers + TCP reichen für Discovery + 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) { @@ -341,27 +69,13 @@ public sealed class SonicManagementClient : IDisposable TimeSpan.FromSeconds(Math.Clamp(_connection.PostRestartDelaySeconds, 0, 120)), cancellationToken); - return (true, $"Container '{containerName}' neugestartet (MfApi)", - $"Domain={_connection.DomainName}; ConnectionUrl={_connection.ConnectionUrl}\n{output}"); + return (true, $"Container '{containerName}' neugestartet", + $"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" + + return (false, "Neustart fehlgeschlagen", $"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) @@ -383,390 +97,12 @@ 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(); + // nichts zu dispose'n } } 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/Tools/verify-mfapi-classpath.bat b/ZA.CoreService.ESBCertificateManager/Tools/verify-mfapi-classpath.bat deleted file mode 100644 index 8655edc..0000000 --- a/ZA.CoreService.ESBCertificateManager/Tools/verify-mfapi-classpath.bat +++ /dev/null @@ -1,28 +0,0 @@ -@echo off -REM Prueft ob JMSConnectorAddress mit den Sonic-Client-JARs ladbar ist. -set LIB=C:\DEV\MQ10.0\lib -set JAVA=C:\Program Files (x86)\Java\jre1.8.0_501\bin\java.exe - -if not exist "%JAVA%" ( - echo Java nicht gefunden: %JAVA% - exit /b 1 -) -if not exist "%LIB%\mfcontext.jar" ( - echo mfcontext.jar fehlt unter %LIB% - exit /b 1 -) - -set CP=%LIB%\mgmt_client.jar;%LIB%\mgmt_config.jar;%LIB%\sonic_mgmt_client.jar;%LIB%\mfcontext.jar;%LIB%\sonic_Client.jar;%LIB%\sonic_Client_ext.jar - -echo Java: %JAVA% -echo CP: %CP% -echo. - -"%JAVA%" -cp "%CP%" -version -echo. - -"%JAVA%" -cp "%CP%" com.sonicsw.mf.jmx.client.JMSConnectorAddress 2>&1 -echo ExitCode=%ERRORLEVEL% -echo. -echo Erwartet: oft "main method" / NoSuchMethodError ODER Usage - Hauptsache KEIN ClassNotFoundException. -exit /b 0 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 diff --git a/ZA.CoreService.ESBCertificateManager/appsettings.json b/ZA.CoreService.ESBCertificateManager/appsettings.json index 6277230..49f4143 100644 --- a/ZA.CoreService.ESBCertificateManager/appsettings.json +++ b/ZA.CoreService.ESBCertificateManager/appsettings.json @@ -1,10 +1,7 @@ { - "ConnectionString": "", "UseOfflineSampleData": true, "SampleTargetsPath": "Data/targets.sample.json", "LogDirectory": "Logs", - "TlsTimeoutSeconds": 8, - "TlsRetryCount": 2, "SonicConnections": [ { "Name": "DE-Test", @@ -12,7 +9,6 @@ "ConnectionUrl": "tcp://dekun-painwbdet:13070", "Username": "Administrator", "Password": "Administrator", - "ManagementMode": "MfApi", "SonicHome": "C:\\DEV\\MQ10.0", "JavaHome": "C:\\Program Files (x86)\\Java\\jre1.8.0_501", "JavaPath": "C:\\Program Files (x86)\\Java\\jre1.8.0_501\\bin\\java.exe", @@ -20,20 +16,8 @@ "KnownContainers": [ "ct-ZADBService" ], - "WinRmUsername": "", - "WinRmPassword": "", - "WinRmPort": 5985, "TimeoutSeconds": 120, - "PostRestartDelaySeconds": 20, - "ManagementHttpPort": 8080, - "ApiBasePath": "/api/v1", - "ContainerListPath": "", - "ContainerRestartPath": "", - "ContainerStopPath": "", - "ContainerStartPath": "", - "WinRmRestartScript": "", - "WinRmContainerListScript": "", - "WinRmXapiImportScript": "" + "PostRestartDelaySeconds": 20 } ] }