diff --git a/deployment/INSTALLATION.md b/deployment/INSTALLATION.md index 38caa69..4aedd7c 100644 --- a/deployment/INSTALLATION.md +++ b/deployment/INSTALLATION.md @@ -28,7 +28,22 @@ nach dem nächsten Agentenlauf das 2.2.1-Fehlerbild `Item not found in monitoring data`. Nur bei bewusst auf `true` gesetztem Opt-in ist anschließend eine Checkmk Service Discovery erforderlich. -Vor dem Stoppen des vorhandenen Tasks wird die neue Version separat getestet. +Ab Version 2.2.3 vergleicht der Installer bei jedem Update die exakten +Servicenamen der installierten und der vorbereiteten Version. Neue, entfernte +oder umbenannte Services führen **vor dem Stoppen des Tasks** zu einem +Sicherheitsstopp. Die Installation darf nur über die Checkbox +**Service-Rename ist beabsichtigt; Checkmk Service Discovery ist eingeplant** +fortgesetzt werden. Der Installer zeigt dabei die entfernten und neuen Namen +an. Eine bloß geänderte Ausgabereihenfolge gilt nicht als Rename. + +Beim korrigierenden Wechsel von 2.2.1 mit `BizTalk ACC ...` auf stabile +`BizTalk ...`-Namen ist die Änderung beabsichtigt: Checkbox aktivieren und den +Servicebestand danach per Discovery abgleichen. Sind in Checkmk bereits die +unpräfixierten Services vorhanden, werden damit insbesondere eventuell +vorhandene falsche `BizTalk ACC ...`-Services bereinigt. + +Vor dem Stoppen des vorhandenen Tasks wird die neue Version separat getestet +und ihr Checkmk-Servicevertrag mit der installierten Version verglichen. Bei normalen Servicekonten prüft der Installer außerdem Kennwort und Batch-Anmelderecht vor der Umschaltung. Der Task wird vor dem Dateitausch deaktiviert und sein Prozessende maximal zehn diff --git a/docs/ACC-Endpoint-Reachability-Analyse-2026-08-10.md b/docs/ACC-Endpoint-Reachability-Analyse-2026-08-10.md index 0f602f2..40875b3 100644 --- a/docs/ACC-Endpoint-Reachability-Analyse-2026-08-10.md +++ b/docs/ACC-Endpoint-Reachability-Analyse-2026-08-10.md @@ -71,6 +71,10 @@ nicht netzwerkprüfbare Kandidaten. Version 2.2.2 weist sie explizit als `BizTalk ...` und passen wieder zu den bereits entdeckten ACC-Services. - Nur ein bewusstes Opt-in mit `true` erzeugt Namen wie `BizTalk ACC ...` und verlangt anschließend eine Checkmk Service Discovery. +- Version 2.2.3 ergänzt einen versionsübergreifenden Installer-Schutz: Der + tatsächliche Servicevertrag der vorhandenen und der neuen EXE wird vor dem + Task-Stopp verglichen. Unbestätigte Renames, neue oder entfernte Services + blockieren das Update; die Differenz wird vollständig angezeigt. ### Korrekte Abdeckungssemantik @@ -131,7 +135,9 @@ Endpoint-/Namensfix bewusst nicht grüngefärbt. ## ACC-Abnahme nach Installation -1. Setup 2.2.2 als Update ausführen; Umgebung `ACC` kann bestehen bleiben. +1. Setup 2.2.3 als Update ausführen; Umgebung `ACC` kann bestehen bleiben. + Beim Wechsel von der fehlerhaften 2.2.1-Ausgabe `BizTalk ACC ...` auf die + stabilen Namen die Rename-/Discovery-Checkbox bewusst aktivieren. 2. In der installierten Config bestätigen: ```text diff --git a/docs/BizTalk_Checkmk_Pulse_Architekturueberblick.docx b/docs/BizTalk_Checkmk_Pulse_Architekturueberblick.docx index 37e7de3..d61488e 100644 Binary files a/docs/BizTalk_Checkmk_Pulse_Architekturueberblick.docx and b/docs/BizTalk_Checkmk_Pulse_Architekturueberblick.docx differ diff --git a/scripts/generate-architecture-overview.py b/scripts/generate-architecture-overview.py index bce810e..5aa73e4 100644 --- a/scripts/generate-architecture-overview.py +++ b/scripts/generate-architecture-overview.py @@ -306,7 +306,7 @@ def configure_document(document): p = footer.paragraphs[0] p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.paragraph_format.space_after = Pt(0) - format_run(p.add_run("Version 2.2.2 | Stand 10.08.2026 | Seite "), size=7.5, color=MUTED) + format_run(p.add_run("Version 2.2.3 | Stand 10.08.2026 | Seite "), size=7.5, color=MUTED) add_page_field(p) @@ -320,7 +320,7 @@ def build_document(): core.title = "BizTalk Checkmk Pulse – Architektur- und Lösungsüberblick" core.subject = "Gesamtarchitektur, Funktionsumfang und Datenaustausch" core.author = "BEW" - core.comments = "Externe Lösungsübersicht zum implementierten Stand 2.2.2" + core.comments = "Externe Lösungsübersicht zum implementierten Stand 2.2.3" # Cover banner = document.add_table(rows=1, cols=1) @@ -377,7 +377,7 @@ def build_document(): document, ("Dokumentmerkmal", "Angabe"), ( - ("Lösungsstand", "BizTalk Checkmk Pulse 2.2.2"), + ("Lösungsstand", "BizTalk Checkmk Pulse 2.2.3"), ("Zielplattform", "Microsoft BizTalk Server 2020 · Windows Server 2019 · Checkmk 2.4"), ("Dokumentstand", "10. August 2026"), ("Dokumentzweck", "Architektur- und Leistungsüberblick für technische Stakeholder"), @@ -406,7 +406,7 @@ def build_document(): add_heading(document, "2. Was ist Checkmk – und was ist ein Local Check?") add_body(document, "Checkmk ist eine zentrale Monitoring-Plattform für IT-Infrastrukturen und Anwendungen. Auf überwachten Servern liefert ein Agent technische Zustände und Messwerte an die Checkmk-Instanz. Dort werden die Daten als Hosts und Services dargestellt, historisiert und für Dashboards, Schwellwerte und Benachrichtigungen verwendet.") add_body(document, "Ein Local Check ist eine bewusst einfache Erweiterungsmöglichkeit des Checkmk-Agenten. Die Prüfung läuft auf dem Zielsystem und gibt pro Service eine Textzeile mit vier Bestandteilen aus:") - add_code_block(document, ('0 "BizTalk PRD Send Ports" biztalk_send_ports_total=31;;;0|biztalk_send_ports_started=31;;;0', 'Alle Send Ports sind aktiv.')) + add_code_block(document, ('0 "BizTalk Send Ports" biztalk_send_ports_total=31;;;0|biztalk_send_ports_started=31;;;0', 'Alle Send Ports sind aktiv.')) add_table( document, ("Bestandteil", "Bedeutung"), @@ -512,8 +512,8 @@ def build_document(): "payloadLines=9", "payloadSha256=<64 hexadezimale Zeichen>", "", - '0 "BizTalk PRD Suspended Instances" biztalk_suspended_total=0;;;0|... Suspended total=0.', - '0 "BizTalk PRD Endpoint Reachability" biztalk_endpoints_active=61;;;0|... Alle 61 aktiven Endpunkte sind erreichbar.', + '0 "BizTalk Suspended Instances" biztalk_suspended_total=0;;;0|... Suspended total=0.', + '0 "BizTalk Endpoint Reachability" biztalk_endpoints_active=61;;;0|... Alle 61 aktiven Endpunkte sind erreichbar.', ), ) add_body(document, "Die Base64-Felder verhindern problematische Trennzeichen im Header; sie sind keine Verschlüsselung. SHA-256 erkennt unvollständige oder veränderte Snapshots. Der Schutz vor unberechtigtem lokalem Zugriff erfolgt über Windows-Dateirechte.") @@ -543,6 +543,7 @@ def build_document(): add_bullet(document, "Getrennte Tageslogs für Provider und Consumer; Standardaufbewahrung 30 Tage.") add_bullet(document, "Erwartet inaktive Receive Locations und Send Ports können exakt allowlisted werden und bleiben als Messwert sichtbar.") add_bullet(document, "Endpoint-Prüfung standardmäßig mit maximal 100 Zielen, 16 parallelen Probes und 3 Sekunden Timeout je Ziel.") + add_bullet(document, "Der Installer vergleicht bei Updates die exakten Checkmk-Servicenamen und stoppt unbestätigte Änderungen, bevor Task oder Programmdateien angefasst werden.") add_heading(document, "Sicherheitsmodell", level=2) add_table( @@ -571,7 +572,7 @@ def build_document(): add_callout(document, "Kernaussage: ", "Ein privilegierter Read-only-Collector sammelt, ein unprivilegierter Consumer validiert und übergibt – Checkmk erhält neun stabile Services statt direkten Zugriff auf BizTalk oder SQL.", fill=LIGHT_GREEN, border=GREEN) add_rich_paragraph( document, - (("Dokumentbasis: Implementierung und Projektdokumentation von BizTalk Checkmk Pulse 2.2.2 sowie Checkmk User Guide „Local checks“, abgerufen am 10. August 2026.", {"size": 7.8, "color": MUTED}),), + (("Dokumentbasis: Implementierung und Projektdokumentation von BizTalk Checkmk Pulse 2.2.3 sowie Checkmk User Guide „Local checks“, abgerufen am 10. August 2026.", {"size": 7.8, "color": MUTED}),), before=8, after=0, ) diff --git a/src/BizTalkCheckmkPulse.Setup/InstallerEngine.cs b/src/BizTalkCheckmkPulse.Setup/InstallerEngine.cs index e9659b7..43431f9 100644 --- a/src/BizTalkCheckmkPulse.Setup/InstallerEngine.cs +++ b/src/BizTalkCheckmkPulse.Setup/InstallerEngine.cs @@ -27,7 +27,13 @@ namespace BizTalkCheckmkPulse.Setup checkmkLocalDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "checkmk", "agent", "local"); } - public void Install(string account, string password, bool isGmsa, string environmentName, Action report) + public void Install( + string account, + string password, + bool isGmsa, + string environmentName, + bool allowServiceNameChange, + Action report) { Validate(account, password, isGmsa, environmentName); report = report ?? delegate { }; @@ -72,9 +78,21 @@ namespace BizTalkCheckmkPulse.Setup stagedConfig, File.Exists(targetConfig) ? targetConfig : null, environmentName); - RunSelfTest(stagedExe); + var stagedServiceNames = RunSelfTest(stagedExe); report("Update-Staging validiert. Umgebung=" + (effectiveEnvironment.Length == 0 ? "(keine)" : effectiveEnvironment) + "."); + if (File.Exists(targetExe)) + { + var installedServiceNames = RunSelfTest(targetExe); + var serviceNameChange = EnsureServiceNameCompatibility( + installedServiceNames, + stagedServiceNames, + allowServiceNameChange); + report(serviceNameChange.Length == 0 + ? "Checkmk-Servicevertrag unveraendert: keine Service Discovery erforderlich." + : "Checkmk-Service-Rename ausdruecklich bestaetigt; Service Discovery erforderlich. " + serviceNameChange); + } + // Erst nach vollstaendiger Staging-Pruefung wird der laufende Provider angehalten. scheduler.DeleteIfExists(TaskName); taskRemoved = true; @@ -397,7 +415,7 @@ namespace BizTalkCheckmkPulse.Setup new DirectoryInfo(path).SetAccessControl(security); } - private static void RunSelfTest(string executable) + private static IReadOnlyList RunSelfTest(string executable) { var start = new ProcessStartInfo(executable, "--self-test") { @@ -419,7 +437,45 @@ namespace BizTalkCheckmkPulse.Setup var lines = output.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); if (process.ExitCode != 0 || lines.Length != 9 || lines.Any(x => !x.StartsWith("0 ", StringComparison.Ordinal))) throw new InvalidOperationException("Self-Test fehlgeschlagen. Exitcode=" + process.ExitCode + ", Zeilen=" + lines.Length + ". " + error); + + var serviceNames = lines.Select(ExtractServiceName).ToArray(); + if (serviceNames.Distinct(StringComparer.Ordinal).Count() != serviceNames.Length) + throw new InvalidOperationException("Self-Test fehlgeschlagen: Checkmk-Servicenamen sind nicht eindeutig."); + return serviceNames; } } + + private static string ExtractServiceName(string line) + { + var firstQuote = line.IndexOf('"'); + var secondQuote = firstQuote < 0 ? -1 : line.IndexOf('"', firstQuote + 1); + if (firstQuote < 0 || secondQuote <= firstQuote + 1) + throw new InvalidOperationException("Self-Test fehlgeschlagen: Checkmk-Servicename kann nicht gelesen werden: " + line); + return line.Substring(firstQuote + 1, secondQuote - firstQuote - 1); + } + + internal static string EnsureServiceNameCompatibility( + IEnumerable installedServiceNames, + IEnumerable stagedServiceNames, + bool allowServiceNameChange) + { + var installed = new HashSet(installedServiceNames ?? Enumerable.Empty(), StringComparer.Ordinal); + var staged = new HashSet(stagedServiceNames ?? Enumerable.Empty(), StringComparer.Ordinal); + if (installed.SetEquals(staged)) return string.Empty; + + var removed = installed.Except(staged, StringComparer.Ordinal).OrderBy(x => x, StringComparer.Ordinal).ToArray(); + var added = staged.Except(installed, StringComparer.Ordinal).OrderBy(x => x, StringComparer.Ordinal).ToArray(); + var description = "Entfernt=[" + string.Join(", ", removed) + "]; Neu=[" + string.Join(", ", added) + "]."; + if (!allowServiceNameChange) + { + throw new InvalidOperationException( + "Sicherheitsstopp: Das Update wuerde Checkmk-Servicenamen aendern. " + + description + + " Ohne Service Discovery entstehen verwaiste bzw. fehlende Services. " + + "Nur wenn die Aenderung beabsichtigt ist, im Setup 'Service Discovery ist eingeplant' bestaetigen und anschliessend die Discovery ausfuehren."); + } + + return description; + } } } diff --git a/src/BizTalkCheckmkPulse.Setup/MainForm.cs b/src/BizTalkCheckmkPulse.Setup/MainForm.cs index 4931c0b..261f941 100644 --- a/src/BizTalkCheckmkPulse.Setup/MainForm.cs +++ b/src/BizTalkCheckmkPulse.Setup/MainForm.cs @@ -12,6 +12,7 @@ namespace BizTalkCheckmkPulse.Setup private readonly TextBox password = new TextBox(); private readonly ComboBox environment = new ComboBox(); private readonly CheckBox gmsa = new CheckBox(); + private readonly CheckBox allowServiceNameChange = new CheckBox(); private readonly Button install = new Button(); private readonly Button uninstall = new Button(); private readonly TextBox status = new TextBox(); @@ -20,8 +21,8 @@ namespace BizTalkCheckmkPulse.Setup { this.engine = engine; Text = "BizTalk Checkmk Pulse Setup"; - ClientSize = new Size(650, 485); - MinimumSize = new Size(666, 524); + ClientSize = new Size(650, 535); + MinimumSize = new Size(666, 574); StartPosition = FormStartPosition.CenterScreen; Font = new Font("Segoe UI", 9F); @@ -68,18 +69,23 @@ namespace BizTalkCheckmkPulse.Setup if (gmsa.Checked) password.Clear(); }; - install.Location = new Point(190, 267); + allowServiceNameChange.Location = new Point(190, 262); + allowServiceNameChange.Size = new Size(420, 34); + allowServiceNameChange.Text = "Service-Rename ist beabsichtigt; Checkmk Service Discovery ist eingeplant"; + allowServiceNameChange.Enabled = engine.IsInstalled; + + install.Location = new Point(190, 306); install.Size = new Size(200, 34); install.Text = "Installieren / aktualisieren"; install.Click += async delegate { await InstallAsync(); }; - uninstall.Location = new Point(410, 267); + uninstall.Location = new Point(410, 306); uninstall.Size = new Size(200, 34); uninstall.Text = "Deinstallieren"; uninstall.Click += async delegate { await UninstallAsync(); }; - status.Location = new Point(22, 320); - status.Size = new Size(588, 135); + status.Location = new Point(22, 359); + status.Size = new Size(588, 146); status.Multiline = true; status.ReadOnly = true; status.ScrollBars = ScrollBars.Vertical; @@ -89,7 +95,7 @@ namespace BizTalkCheckmkPulse.Setup Controls.AddRange(new Control[] { - title, description, account, password, environment, gmsa, install, uninstall, status + title, description, account, password, environment, gmsa, allowServiceNameChange, install, uninstall, status }); AcceptButton = install; } @@ -114,6 +120,7 @@ namespace BizTalkCheckmkPulse.Setup collectorPassword, collectorIsGmsa, environmentName, + allowServiceNameChange.Checked, Report)); password.Clear(); Report("Installation abgeschlossen. Der Provider-Task wurde einmalig gestartet."); @@ -166,6 +173,7 @@ namespace BizTalkCheckmkPulse.Setup account.Enabled = !busy; environment.Enabled = !busy; gmsa.Enabled = !busy; + allowServiceNameChange.Enabled = !busy && engine.IsInstalled; password.Enabled = !busy && !gmsa.Checked; UseWaitCursor = busy; if (message != null) Report(message); diff --git a/src/BizTalkCheckmkPulse.Setup/Properties/AssemblyInfo.cs b/src/BizTalkCheckmkPulse.Setup/Properties/AssemblyInfo.cs index 5aa7b40..fe5b884 100644 --- a/src/BizTalkCheckmkPulse.Setup/Properties/AssemblyInfo.cs +++ b/src/BizTalkCheckmkPulse.Setup/Properties/AssemblyInfo.cs @@ -6,5 +6,5 @@ using System.Runtime.CompilerServices; [assembly: AssemblyDescription("PowerShell-free installer and updater for BizTalk Checkmk Pulse")] [assembly: AssemblyCompany("BEW")] [assembly: AssemblyProduct("BizTalk Checkmk Pulse")] -[assembly: AssemblyVersion("2.2.2.0")] -[assembly: AssemblyFileVersion("2.2.2.0")] +[assembly: AssemblyVersion("2.2.3.0")] +[assembly: AssemblyFileVersion("2.2.3.0")] diff --git a/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs b/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs index ff889fc..21e8c35 100644 --- a/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs +++ b/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs @@ -6,5 +6,5 @@ using System.Reflection; [assembly: AssemblyDescription("Privileged BizTalk data provider and validated Checkmk snapshot consumer")] [assembly: AssemblyCompany("BEW")] [assembly: AssemblyProduct("BizTalk Checkmk Pulse")] -[assembly: AssemblyVersion("2.2.2.0")] -[assembly: AssemblyFileVersion("2.2.2.0")] +[assembly: AssemblyVersion("2.2.3.0")] +[assembly: AssemblyFileVersion("2.2.3.0")] diff --git a/tests/BizTalkCheckmkPulse.Tests/Program.cs b/tests/BizTalkCheckmkPulse.Tests/Program.cs index cbd3538..08c7765 100644 --- a/tests/BizTalkCheckmkPulse.Tests/Program.cs +++ b/tests/BizTalkCheckmkPulse.Tests/Program.cs @@ -37,6 +37,13 @@ namespace BizTalkCheckmkPulse.Tests Run("EndpointOutputListsOnlyUnavailableTargets", EndpointOutputListsOnlyUnavailableTargets); Run("EndpointOutputExplainsOnlyRealResolutionGaps", EndpointOutputExplainsOnlyRealResolutionGaps); Run("EndpointProbeBudgetFitsMinuteInterval", EndpointProbeBudgetFitsMinuteInterval); +<<<<<<< HEAD +======= + Run("EnvironmentLabelDoesNotRenameServicesByDefault", EnvironmentLabelDoesNotRenameServicesByDefault); + Run("DefaultServiceContractIsExact", DefaultServiceContractIsExact); + Run("InstallerRejectsUnconfirmedServiceRename", InstallerRejectsUnconfirmedServiceRename); + Run("InstallerAllowsConfirmedServiceRename", InstallerAllowsConfirmedServiceRename); +>>>>>>> e694e4c (Guard Checkmk service renames during updates) Run("InstallerUpdatePreservesExistingSettings", InstallerUpdatePreservesExistingSettings); Run("SnapshotRoundTripPreservesLines", SnapshotRoundTripPreservesLines); Run("SnapshotRejectsTampering", SnapshotRejectsTampering); @@ -683,6 +690,82 @@ namespace BizTalkCheckmkPulse.Tests Assert(configuredMaximum <= 30000, "configured endpoint worst-case must leave headroom in the minute interval"); } +<<<<<<< HEAD +======= + private static void EnvironmentLabelDoesNotRenameServicesByDefault() + { + var options = new MonitoringOptions { EnvironmentName = "ACC" }; + var lines = new CheckmkLocalFormatter(options).FormatSelfTest().ToArray(); + Assert(lines.Any(x => x.Contains("\"BizTalk Platform\"")), "stable service name without environment missing"); + Assert(lines.All(x => x.IndexOf("\"BizTalk ACC ", StringComparison.Ordinal) < 0), "environment label must not rename services by default"); + + options.IncludeEnvironmentInServiceName = true; + lines = new CheckmkLocalFormatter(options).FormatSelfTest().ToArray(); + Assert(lines.Any(x => x.Contains("\"BizTalk ACC Platform\"")), "explicit environment service-name opt-in missing"); + } + + private static void DefaultServiceContractIsExact() + { + var actual = new CheckmkLocalFormatter(new MonitoringOptions()) + .FormatSelfTest() + .Select(ExtractQuotedServiceName) + .ToArray(); + var expected = new[] + { + "BizTalk Platform", + "BizTalk SQL Access", + "BizTalk Suspended Instances", + "BizTalk Host Instances", + "BizTalk Receive Locations", + "BizTalk Send Ports", + "BizTalk Endpoint Reachability", + "BizTalk Orchestrations", + "BizTalk Event Log" + }; + AssertEqual(string.Join("|", expected), string.Join("|", actual), "exact default service contract"); + } + + private static void InstallerRejectsUnconfirmedServiceRename() + { + try + { + BizTalkCheckmkPulse.Setup.InstallerEngine.EnsureServiceNameCompatibility( + new[] { "BizTalk Platform", "BizTalk Event Log" }, + new[] { "BizTalk ACC Platform", "BizTalk ACC Event Log" }, + false); + throw new InvalidOperationException("unconfirmed rename was accepted"); + } + catch (InvalidOperationException ex) + { + Assert(ex.Message.Contains("Sicherheitsstopp"), "rename rejection must explain the safety stop"); + Assert(ex.Message.Contains("Service Discovery"), "rename rejection must require discovery"); + } + } + + private static void InstallerAllowsConfirmedServiceRename() + { + var unchanged = BizTalkCheckmkPulse.Setup.InstallerEngine.EnsureServiceNameCompatibility( + new[] { "BizTalk Platform", "BizTalk Event Log" }, + new[] { "BizTalk Event Log", "BizTalk Platform" }, + false); + AssertEqual(string.Empty, unchanged, "service ordering must not be treated as rename"); + + var change = BizTalkCheckmkPulse.Setup.InstallerEngine.EnsureServiceNameCompatibility( + new[] { "BizTalk ACC Platform" }, + new[] { "BizTalk Platform" }, + true); + Assert(change.Contains("BizTalk ACC Platform"), "confirmed rename must report removed service"); + Assert(change.Contains("BizTalk Platform"), "confirmed rename must report new service"); + } + + private static string ExtractQuotedServiceName(string line) + { + var start = line.IndexOf('"') + 1; + var end = line.IndexOf('"', start); + return line.Substring(start, end - start); + } + +>>>>>>> e694e4c (Guard Checkmk service renames during updates) private static void InstallerUpdatePreservesExistingSettings() { var directory = Path.Combine(Path.GetTempPath(), "BizTalkCheckmkPulse.InstallerTests." + Guid.NewGuid().ToString("N"));