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
}
]
}