From 8888ab97c0bfd415b1e527161716c1fbedb4add9 Mon Sep 17 00:00:00 2001 From: Johannes Rest Date: Tue, 4 Aug 2026 10:27:10 +0200 Subject: [PATCH] Harden ACC installer update and endpoint performance --- Dokumentation.md | 20 +- README.md | 37 +- deployment/INSTALLATION.md | 26 ++ docs/CheckmkServices.md | 2 + docs/EndpointCatalog.md | 16 + docs/ExampleOutput.md | 2 +- scripts/package-release.cmd | 2 + src/BizTalkCheckmkPulse.Packager/Program.cs | 35 ++ .../BizTalkCheckmkPulse.Setup.csproj | 1 + .../InstallerEngine.cs | 331 +++++++++++++++--- src/BizTalkCheckmkPulse.Setup/MainForm.cs | 8 +- .../Properties/AssemblyInfo.cs | 10 + .../TaskSchedulerService.cs | 11 + src/BizTalkCheckmkPulse/App.config | 5 +- .../CheckmkLocalFormatter.cs | 14 +- .../EndpointCatalogStore.cs | 4 +- .../EndpointConnectivityProbe.cs | 41 ++- src/BizTalkCheckmkPulse/Models.cs | 2 + src/BizTalkCheckmkPulse/MonitoringOptions.cs | 4 +- src/BizTalkCheckmkPulse/Program.cs | 4 + .../Properties/AssemblyInfo.cs | 4 +- .../BizTalkCheckmkPulse.Tests.csproj | 4 + tests/BizTalkCheckmkPulse.Tests/Program.cs | 75 +++- 23 files changed, 581 insertions(+), 77 deletions(-) create mode 100644 src/BizTalkCheckmkPulse.Setup/Properties/AssemblyInfo.cs diff --git a/Dokumentation.md b/Dokumentation.md index a78a0d9..ce16327 100644 --- a/Dokumentation.md +++ b/Dokumentation.md @@ -380,7 +380,9 @@ scripts\package-release.cmd Der Release-Build der Visual-Studio-2019-Solution baut Anwendung, grafischen Installer und Packaging-Werkzeug. Danach liegen der Paketordner `artifacts\BizTalkCheckmkPulse-Setup` und das direkt transportierbare -`artifacts\BizTalkCheckmkPulse-Setup.zip` bereit. +`artifacts\BizTalkCheckmkPulse-Setup.zip` bereit. Zusaetzlich entstehen +`BizTalkCheckmkPulse-Setup.zip.b64.txt` fuer `certutil -decode` und eine +SHA-256-Textdatei. ### 8.2 Normales Servicekonto @@ -392,6 +394,14 @@ starten. Der Installer muss als lokaler Administrator laufen. Er vergibt keine AD-/BizTalk-/SQL-Rechte; diese bleiben getrennte administrative Freigaben. +Beim Update wird zuerst die neue Paketversion im Staging-Verzeichnis mit neun +Self-Test-Zeilen validiert. Bestehende AppSettings werden in die neue Config +uebernommen; neue Keys stammen aus der neuen Standardkonfiguration. Die +Runtime-Daten unter `%ProgramData%` bleiben bestehen. Erst danach wird der +Task gestoppt und die Programmversion umgeschaltet. Scheitert ein Folgeschritt, +versucht der Installer Programmverzeichnis, Checkmk-Wrapper und Scheduled Task +auf den vorherigen Stand zurueckzusetzen. + Der installierte Task selbst laeuft mit `RunLevel Limited`. Der Installer uebergibt das Kennwort direkt an die Windows-Task-Scheduler-COM-Schnittstelle; es wird weder in einer Prozesskommandozeile noch in Config oder Log @@ -542,8 +552,8 @@ BizTalk-Probes: | `ProbeEndpointConnectivity` | `true` | | `EndpointDiscoveryIntervalHours` | `168` | | `EndpointProbeTimeoutMilliseconds` | `3000` | -| `EndpointProbeMaxConcurrency` | `12` | -| `EndpointMaxCount` | `500` | +| `EndpointProbeMaxConcurrency` | `16` | +| `EndpointMaxCount` | `100` | Alarmierung: @@ -580,6 +590,10 @@ Automatisiert: - Endpoint-Adressen werden ohne URI-Secrets auf Host/Port reduziert, - manuelle Endpoint-Overrides ueberleben den automatischen Wochenabgleich, - nur nicht erreichbare Endpoints erscheinen im Checkmk-Detail, +- 70 vollstaendig timeoutende eindeutige Ziele bleiben mit den Defaults in + einem theoretischen Socket-Budget von etwa 15 Sekunden; das konfigurierte + Maximum von 100 Zielen in etwa 21 Sekunden, +- Update-Config-Merge erhaelt bestehende Werte und fuegt neue Defaults hinzu, - Snapshot-Roundtrip und Ersatz, - SHA-256-Manipulation wird verworfen, - Stale-Snapshot wird verworfen, diff --git a/README.md b/README.md index b431e1c..1d12931 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,8 @@ und das transportierbare ZIP: ```text artifacts\ BizTalkCheckmkPulse-Setup.zip + BizTalkCheckmkPulse-Setup.zip.b64.txt + BizTalkCheckmkPulse-Setup.zip.sha256.txt BizTalkCheckmkPulse-Setup\ Setup.exe INSTALLATION.md @@ -183,6 +185,14 @@ Das Packaging-Projekt ist Teil von `BizTalkCheckmkPulse.sln`. Ein normaler `Release`-Build in Visual Studio 2019 genuegt daher; ein separates PowerShell-/WiX-/MSI-Tooling wird nicht benoetigt. +Die Base64-TXT kann auf dem Windows-Zielsystem ohne Zusatzwerkzeug dekodiert +werden: + +```cmd +certutil -decode BizTalkCheckmkPulse-Setup.zip.b64.txt BizTalkCheckmkPulse-Setup.zip +certutil -hashfile BizTalkCheckmkPulse-Setup.zip SHA256 +``` + Format-Self-Test ohne WMI, SQL oder Event Log: ```cmd @@ -226,7 +236,7 @@ normaler Bestandteil des Kontonamens verarbeitet. Wenn das Kennwort rotiert oder ablaeuft, muss es im Scheduled Task aktualisiert werden. Dazu `Setup.exe` mit dem neuen Kennwort erneut ausfuehren. Bis dahin -wird der Snapshot nach 180 Sekunden stale und Checkmk zeigt alle acht Services +wird der Snapshot nach 180 Sekunden stale und Checkmk zeigt alle neun Services als `UNKNOWN`. Ein gMSA kann ueber die Checkbox ohne Kennworteingabe installiert werden; die @@ -234,15 +244,22 @@ produktive Standardbeschreibung geht vom normalen Servicekonto aus. Der Installer: -1. kopiert EXE und Config nach - `%ProgramFiles%\BizTalkCheckmkPulse`, -2. erstellt `%ProgramData%\BizTalkCheckmkPulse\data` und `logs`, -3. setzt explizite ACLs fuer Administratoren, Provider und `LocalSystem`, -4. installiert nur den kleinen `.cmd`-Consumer unter +1. prueft Paket und neun Self-Test-Zeilen, bevor der bestehende Task angehalten + wird, +2. baut die neue Version in einem Staging-Verzeichnis auf und uebernimmt + vorhandene AppSettings; neue Keys erhalten ihre neuen Defaults. Die alten + unveraenderten Endpoint-Defaults `12`/`500` werden auf `16`/`100` migriert, +3. schaltet die Programmdateien per Verzeichniswechsel um; bei einem Fehler + werden vorherige Version, Wrapper und Task best effort wiederhergestellt, +4. behaelt `%ProgramData%\BizTalkCheckmkPulse` mit Endpoint-Katalog, Snapshot + und Logs bei, +5. installiert EXE und Config nach `%ProgramFiles%\BizTalkCheckmkPulse`, +6. setzt explizite ACLs fuer Administratoren, Provider und `LocalSystem`, +7. installiert nur den kleinen `.cmd`-Consumer unter `%ProgramData%\checkmk\agent\local`, -5. registriert `BizTalk Checkmk Pulse Provider` minuetlich mit +8. registriert `BizTalk Checkmk Pulse Provider` minuetlich mit `IgnoreNew`, fuenf Minuten Laufzeitlimit und zwei Wiederholungen, -6. fuehrt den Self-Test aus und startet den Provider einmalig. +9. fuehrt den installierten Self-Test erneut aus und startet den Provider. Installation, Update, Deinstallation und Laufzeit verwenden keine PowerShell. @@ -332,8 +349,8 @@ Wichtige Werte: | `EndpointCatalogPath` | `%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml` | Lokal gepflegte Endpoint-Konfiguration ohne vollstaendige URIs/Secrets. | | `EndpointDiscoveryIntervalHours` | `168` | Intervall fuer den vollstaendigen Umgebungsabgleich. | | `EndpointProbeTimeoutMilliseconds` | `3000` | Timeout je dedupliziertem Host/Port-Ziel. | -| `EndpointProbeMaxConcurrency` | `12` | Begrenzte parallele Socket-Probes. | -| `EndpointMaxCount` | `500` | Harte Obergrenze gegen fehlerhafte/uebergrosse Konfiguration. | +| `EndpointProbeMaxConcurrency` | `16` | Begrenzte parallele Socket-Probes. | +| `EndpointMaxCount` | `100` | Harte Obergrenze und Laufzeitbudget; bei 3 s Timeout theoretisch maximal etwa 21 s. | | `QueryTimeoutSeconds` | `25` | WMI-Timeout je Query. | | `SqlConnectionTimeoutSeconds` | `5` | SQL-Timeout je Ziel. | | `WarnResumableThreshold` | `1` | WARN ab n resumable Suspensions. | diff --git a/deployment/INSTALLATION.md b/deployment/INSTALLATION.md index 52541bf..fedb536 100644 --- a/deployment/INSTALLATION.md +++ b/deployment/INSTALLATION.md @@ -1,5 +1,12 @@ # Installation auf dem BizTalk-Server +Falls nur die TXT-Datei uebertragen wurde: + +```cmd +certutil -decode BizTalkCheckmkPulse-Setup.zip.b64.txt BizTalkCheckmkPulse-Setup.zip +certutil -hashfile BizTalkCheckmkPulse-Setup.zip SHA256 +``` + 1. `BizTalkCheckmkPulse-Setup.zip` vollstaendig in ein lokales Verzeichnis entpacken. 2. `Setup.exe` als lokaler Administrator starten und die UAC-Abfrage bestaetigen. 3. Das Collector-Konto im Format `DOMAIN\Benutzer` eingeben, zum Beispiel @@ -8,6 +15,25 @@ 5. Im Aufgabenplaner den Task `BizTalk Checkmk Pulse Provider` und danach den Checkmk-Agent-Dump kontrollieren. +Bei einer bereits vorhandenen ACC-Installation ist dies ein Update. Der +Installer erkennt die bestehende Umgebung, behaelt Endpoint-Katalog, Snapshot +und Logs sowie vorhandene AppSettings bei und ergaenzt neue Config-Keys aus dem +Paket. Das BEW-Konto und sein aktuelles Kennwort muessen erneut eingegeben +werden, weil der Scheduled Task mit den bestaetigten Zugangsdaten neu +registriert wird. + +Die frueheren unveraenderten Endpoint-Defaults `12` parallele Probes und +`EndpointMaxCount=500` werden beim Update auf `16` beziehungsweise `100` +migriert. Abweichende, bewusst konfigurierte Werte bleiben erhalten. + +Vor dem Stoppen des vorhandenen Tasks wird die neue Version separat getestet. +Bei normalen Servicekonten prueft der Installer ausserdem Kennwort und +Batch-Anmelderecht vor der Umschaltung. +Der Task wird vor dem Dateitausch deaktiviert und sein Prozessende maximal zehn +Sekunden abgewartet, damit keine laufende Provider-EXE ueberschrieben wird. +Bei einem Fehler nach der Umschaltung versucht der Installer, vorherige +Programmdateien, Wrapper und Task wiederherzustellen. + PowerShell wird fuer Installation, Update, Deinstallation und Laufzeit nicht benoetigt. Das Kennwort wird direkt an die Windows-Aufgabenplanung uebergeben und weder in einer Datei noch in einer Prozesskommandozeile abgelegt. diff --git a/docs/CheckmkServices.md b/docs/CheckmkServices.md index 2faad1f..e387ea1 100644 --- a/docs/CheckmkServices.md +++ b/docs/CheckmkServices.md @@ -202,11 +202,13 @@ Metriken: | --- | --- | | `biztalk_endpoints_configured` | Eintraege in `endpoints.xml`, einschliesslich derzeit inaktiver. | | `biztalk_endpoints_active` | Im aktuellen Lauf Started/Enabled und fuer die Probe ausgewaehlt. | +| `biztalk_endpoints_unique_targets` | Nach Host/Port/Protokoll-Deduplizierung tatsaechlich verbundene Ziele. | | `biztalk_endpoints_tested` | Vollstaendig abgeschlossene Artefaktprobes. | | `biztalk_endpoints_available` | Erreichbare aktive Artefaktziele. | | `biztalk_endpoints_failed` | Nicht erreichbare aktive Artefaktziele; CRIT ab 1. | | `biztalk_endpoints_unsupported` | Aktive externe Adressen ohne sicher bestimmbaren Host/Port. | | `biztalk_endpoints_inactive_skipped` | Konfigurierte Eintraege, deren BizTalk-Artefakt aktuell nicht aktiv ist. | +| `biztalk_endpoint_probe_duration_ms` | Gesamtlaufzeit der parallelisierten Socket-Probes in Millisekunden. | Bei expliziten `udp://host:port`-Eintraegen kann ein generischer Check ohne applikationsspezifisches Protokoll nur DNS, Route und erfolgreichen lokalen diff --git a/docs/EndpointCatalog.md b/docs/EndpointCatalog.md index 976aca2..8630fa1 100644 --- a/docs/EndpointCatalog.md +++ b/docs/EndpointCatalog.md @@ -26,6 +26,22 @@ Damit wird ein inzwischen gestopptes/deaktiviertes Artefakt sofort nicht mehr geprueft. Neue Artefakte werden spaetestens beim naechsten Wochenabgleich aufgenommen. +## Laufzeitbudget + +Die Socket-Probes werden nach Protokoll, Host und Port dedupliziert und mit +maximal `EndpointProbeMaxConcurrency=16` parallelen Verbindungen ausgefuehrt. +Bei `EndpointProbeTimeoutMilliseconds=3000` benoetigen 70 vollstaendig +timeoutende eindeutige Ziele theoretisch etwa 15 Sekunden. Die Standardgrenze +`EndpointMaxCount=100` begrenzt diesen Anteil eines Providerlaufs auf etwa +21 Sekunden. Nicht pruefbare aktive BizTalk-Artefakte zaehlen nicht gegen diese +Grenze; sie werden separat als `unsupported` gemeldet. + +Checkmk selbst wartet nicht auf diese Verbindungen, sondern liest nur den +zuletzt atomar geschriebenen Snapshot. Die reale ACC-Laufzeit ist nach dem +Update an `biztalk_endpoint_probe_duration_ms` sowie an `endpoint_probe_ms` und +`elapsed_ms` im Provider-Log zu kontrollieren. Der Scheduled Task verhindert +mit `IgnoreNew` ueberlappende Providerlaeufe. + ## Sicherheitsmodell Der Katalog speichert nicht die vollstaendige BizTalk-Adresse. Persistiert diff --git a/docs/ExampleOutput.md b/docs/ExampleOutput.md index 10d84d2..78699c3 100644 --- a/docs/ExampleOutput.md +++ b/docs/ExampleOutput.md @@ -61,7 +61,7 @@ Artefakte. Nur die Textliste ist begrenzt. Gesund, ohne Auflistung aller Ziele: ```text -0 "BizTalk ACC Endpoint Reachability" biztalk_endpoints_configured=63;;;0|biztalk_endpoints_active=61;;;0|biztalk_endpoints_tested=61;;;0|biztalk_endpoints_available=61;;;0|biztalk_endpoints_failed=0;;1;0|biztalk_endpoints_unsupported=0;;1;0|biztalk_endpoints_inactive_skipped=2;;;0 Alle 61 aktiven, pruefbaren Send-/Receive-Endpunkte sind erreichbar; catalog_utc=2026-08-04T10:00:00.0000000Z +0 "BizTalk ACC Endpoint Reachability" biztalk_endpoints_configured=63;;;0|biztalk_endpoints_active=61;;;0|biztalk_endpoints_unique_targets=44;;;0|biztalk_endpoints_tested=61;;;0|biztalk_endpoints_available=61;;;0|biztalk_endpoints_failed=0;;1;0|biztalk_endpoints_unsupported=0;;1;0|biztalk_endpoints_inactive_skipped=2;;;0|biztalk_endpoint_probe_duration_ms=842;;;0 Alle 61 aktiven, pruefbaren Send-/Receive-Endpunkte sind erreichbar (unique_targets=44, probe_ms=842); catalog_utc=2026-08-04T10:00:00.0000000Z ``` Fehlerhaft, nur mit nicht erreichbaren Zielen: diff --git a/scripts/package-release.cmd b/scripts/package-release.cmd index 7f4c15a..d138b3c 100644 --- a/scripts/package-release.cmd +++ b/scripts/package-release.cmd @@ -6,4 +6,6 @@ if errorlevel 1 exit /b 1 echo Install package written to artifacts\BizTalkCheckmkPulse-Setup echo Install ZIP written to artifacts\BizTalkCheckmkPulse-Setup.zip +echo Certutil Base64 written to artifacts\BizTalkCheckmkPulse-Setup.zip.b64.txt +echo SHA-256 written to artifacts\BizTalkCheckmkPulse-Setup.zip.sha256.txt exit /b 0 diff --git a/src/BizTalkCheckmkPulse.Packager/Program.cs b/src/BizTalkCheckmkPulse.Packager/Program.cs index b471e4c..b0114c6 100644 --- a/src/BizTalkCheckmkPulse.Packager/Program.cs +++ b/src/BizTalkCheckmkPulse.Packager/Program.cs @@ -1,6 +1,8 @@ using System; using System.IO; using System.IO.Compression; +using System.Security.Cryptography; +using System.Text; namespace BizTalkCheckmkPulse.Packager { @@ -17,6 +19,8 @@ namespace BizTalkCheckmkPulse.Packager var package = Path.Combine(artifacts, "BizTalkCheckmkPulse-Setup"); var application = Path.Combine(package, "application"); var zip = Path.Combine(artifacts, "BizTalkCheckmkPulse-Setup.zip"); + var base64 = zip + ".b64.txt"; + var checksum = zip + ".sha256.txt"; if (Directory.Exists(package)) Directory.Delete(package, true); Directory.CreateDirectory(application); @@ -39,8 +43,15 @@ namespace BizTalkCheckmkPulse.Packager if (File.Exists(zip)) File.Delete(zip); ZipFile.CreateFromDirectory(package, zip, CompressionLevel.Optimal, false); + WriteBase64(zip, base64); + File.WriteAllText( + checksum, + Sha256(zip) + " " + Path.GetFileName(zip) + Environment.NewLine, + new UTF8Encoding(false)); Console.WriteLine("Install package: " + package); Console.WriteLine("Install ZIP: " + zip); + Console.WriteLine("Base64 TXT: " + base64); + Console.WriteLine("SHA-256 TXT: " + checksum); return 0; } catch (Exception ex) @@ -55,5 +66,29 @@ namespace BizTalkCheckmkPulse.Packager if (!File.Exists(source)) throw new FileNotFoundException("Required package file missing: " + source, source); File.Copy(source, target, true); } + + private static void WriteBase64(string source, string target) + { + var encoded = Convert.ToBase64String(File.ReadAllBytes(source)); + var builder = new StringBuilder(encoded.Length + encoded.Length / 64 * 2 + 2); + for (var offset = 0; offset < encoded.Length; offset += 64) + { + builder.Append(encoded, offset, Math.Min(64, encoded.Length - offset)); + builder.Append('\n'); + } + File.WriteAllText(target, builder.ToString(), new UTF8Encoding(false)); + } + + private static string Sha256(string path) + { + using (var stream = File.OpenRead(path)) + using (var algorithm = SHA256.Create()) + { + var hash = algorithm.ComputeHash(stream); + var builder = new StringBuilder(hash.Length * 2); + foreach (var value in hash) builder.Append(value.ToString("x2")); + return builder.ToString(); + } + } } } diff --git a/src/BizTalkCheckmkPulse.Setup/BizTalkCheckmkPulse.Setup.csproj b/src/BizTalkCheckmkPulse.Setup/BizTalkCheckmkPulse.Setup.csproj index 017ab23..ada08f1 100644 --- a/src/BizTalkCheckmkPulse.Setup/BizTalkCheckmkPulse.Setup.csproj +++ b/src/BizTalkCheckmkPulse.Setup/BizTalkCheckmkPulse.Setup.csproj @@ -39,6 +39,7 @@ + diff --git a/src/BizTalkCheckmkPulse.Setup/InstallerEngine.cs b/src/BizTalkCheckmkPulse.Setup/InstallerEngine.cs index 9c54d96..e9659b7 100644 --- a/src/BizTalkCheckmkPulse.Setup/InstallerEngine.cs +++ b/src/BizTalkCheckmkPulse.Setup/InstallerEngine.cs @@ -1,7 +1,10 @@ using System; +using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Security.AccessControl; using System.Security.Principal; using System.Xml; @@ -27,6 +30,7 @@ namespace BizTalkCheckmkPulse.Setup public void Install(string account, string password, bool isGmsa, string environmentName, Action report) { Validate(account, password, isGmsa, environmentName); + report = report ?? delegate { }; var accountSid = (SecurityIdentifier)new NTAccount(account).Translate(typeof(SecurityIdentifier)); report("Konto aufgeloest: " + account + " (" + accountSid.Value + ")"); @@ -37,46 +41,153 @@ namespace BizTalkCheckmkPulse.Setup RequireFile(sourceExe); RequireFile(sourceConfig); RequireFile(sourceWrapper); - - // Ein laufender Provider kann die installierte EXE waehrend eines Updates sperren. - new TaskSchedulerService().DeleteIfExists(TaskName); - report("Vorhandener Scheduled Task angehalten beziehungsweise fuer das Update entfernt."); - - Directory.CreateDirectory(installDirectory); var targetExe = Path.Combine(installDirectory, "BizTalkCheckmkPulse.exe"); var targetConfig = targetExe + ".config"; - File.Copy(sourceExe, targetExe, true); - File.Copy(sourceConfig, targetConfig, true); - SetEnvironment(targetConfig, environmentName); - report("Programmdateien installiert: " + installDirectory); + var stagingDirectory = installDirectory + ".staging." + Guid.NewGuid().ToString("N"); + var backupDirectory = installDirectory + ".backup." + Guid.NewGuid().ToString("N"); + var targetWrapper = Path.Combine(checkmkLocalDirectory, "biztalk_checkmk_pulse.cmd"); + var previousWrapper = File.Exists(targetWrapper) ? File.ReadAllBytes(targetWrapper) : null; + var hadExistingInstallation = Directory.Exists(installDirectory); + var taskRemoved = false; + var backupCreated = false; + var filesActivated = false; + var scheduler = new TaskSchedulerService(); - var dataDirectory = Path.Combine(runtimeDirectory, "data"); - var logDirectory = Path.Combine(runtimeDirectory, "logs"); - Directory.CreateDirectory(runtimeDirectory); - Directory.CreateDirectory(dataDirectory); - Directory.CreateDirectory(logDirectory); + try + { + RunSelfTest(sourceExe); + report("Paket-Vorpruefung erfolgreich: neun Checkmk-Services."); + if (!isGmsa) + { + ValidateBatchLogon(account, password); + report("Collector-Anmeldung und 'Log on as a batch job' vor dem Update bestaetigt."); + } - ApplyDirectoryAcl(installDirectory, accountSid, FileSystemRights.ReadAndExecute, FileSystemRights.ReadAndExecute); - ApplyDirectoryAcl(runtimeDirectory, accountSid, FileSystemRights.ReadAndExecute, FileSystemRights.ReadAndExecute); - ApplyDirectoryAcl(dataDirectory, accountSid, FileSystemRights.Modify, FileSystemRights.ReadAndExecute); - ApplyDirectoryAcl(logDirectory, accountSid, FileSystemRights.Modify, FileSystemRights.Modify); - report("Least-Privilege-Verzeichnisrechte gesetzt."); + Directory.CreateDirectory(stagingDirectory); + var stagedExe = Path.Combine(stagingDirectory, "BizTalkCheckmkPulse.exe"); + var stagedConfig = stagedExe + ".config"; + File.Copy(sourceExe, stagedExe, false); + var effectiveEnvironment = PrepareConfig( + sourceConfig, + stagedConfig, + File.Exists(targetConfig) ? targetConfig : null, + environmentName); + RunSelfTest(stagedExe); + report("Update-Staging validiert. Umgebung=" + (effectiveEnvironment.Length == 0 ? "(keine)" : effectiveEnvironment) + "."); - Directory.CreateDirectory(checkmkLocalDirectory); - File.Copy(sourceWrapper, Path.Combine(checkmkLocalDirectory, "biztalk_checkmk_pulse.cmd"), true); - report("Checkmk Local Check installiert: " + checkmkLocalDirectory); + // Erst nach vollstaendiger Staging-Pruefung wird der laufende Provider angehalten. + scheduler.DeleteIfExists(TaskName); + taskRemoved = true; + report("Vorhandener Scheduled Task angehalten und fuer das Update entfernt."); - RunSelfTest(targetExe); - report("Self-Test erfolgreich: acht Checkmk-Services."); + if (hadExistingInstallation) + { + Directory.Move(installDirectory, backupDirectory); + backupCreated = true; + } - new TaskSchedulerService().RegisterAndStart( - TaskName, - targetExe, - installDirectory, - account, - isGmsa ? null : password, - isGmsa); - report("Scheduled Task registriert: " + TaskName); + Directory.Move(stagingDirectory, installDirectory); + filesActivated = true; + report(hadExistingInstallation + ? "Programmdateien atomar auf die neue Version umgestellt." + : "Programmdateien installiert: " + installDirectory); + + var dataDirectory = Path.Combine(runtimeDirectory, "data"); + var logDirectory = Path.Combine(runtimeDirectory, "logs"); + Directory.CreateDirectory(runtimeDirectory); + Directory.CreateDirectory(dataDirectory); + Directory.CreateDirectory(logDirectory); + + ApplyDirectoryAcl(installDirectory, accountSid, FileSystemRights.ReadAndExecute, FileSystemRights.ReadAndExecute); + ApplyDirectoryAcl(runtimeDirectory, accountSid, FileSystemRights.ReadAndExecute, FileSystemRights.ReadAndExecute); + ApplyDirectoryAcl(dataDirectory, accountSid, FileSystemRights.Modify, FileSystemRights.ReadAndExecute); + ApplyDirectoryAcl(logDirectory, accountSid, FileSystemRights.Modify, FileSystemRights.Modify); + report("Least-Privilege-Verzeichnisrechte gesetzt; vorhandene Runtime-Daten bleiben erhalten."); + + Directory.CreateDirectory(checkmkLocalDirectory); + File.Copy(sourceWrapper, targetWrapper, true); + report("Checkmk Local Check installiert: " + checkmkLocalDirectory); + + RunSelfTest(targetExe); + report("Installierter Self-Test erfolgreich: neun Checkmk-Services."); + + scheduler.RegisterAndStart( + TaskName, + targetExe, + installDirectory, + account, + isGmsa ? null : password, + isGmsa); + taskRemoved = false; + report("Scheduled Task registriert und einmalig gestartet: " + TaskName); + + TryDeleteDirectory(backupDirectory, report); + } + catch (Exception installException) + { + var rollbackFailures = new List(); + try + { + scheduler.DeleteIfExists(TaskName); + } + catch (Exception ex) + { + rollbackFailures.Add("Task stoppen: " + ex.Message); + } + + if (filesActivated || backupCreated) + { + try + { + if (Directory.Exists(installDirectory)) Directory.Delete(installDirectory, true); + if (backupCreated && Directory.Exists(backupDirectory)) Directory.Move(backupDirectory, installDirectory); + report("Vorherige Programmversion wiederhergestellt."); + } + catch (Exception ex) + { + rollbackFailures.Add("Programmdateien wiederherstellen: " + ex.Message); + } + } + + try + { + RestoreWrapper(targetWrapper, previousWrapper); + } + catch (Exception ex) + { + rollbackFailures.Add("Checkmk-Wrapper wiederherstellen: " + ex.Message); + } + + if (taskRemoved && hadExistingInstallation && File.Exists(targetExe)) + { + try + { + scheduler.RegisterAndStart( + TaskName, + targetExe, + installDirectory, + account, + isGmsa ? null : password, + isGmsa); + report("Scheduled Task fuer die vorherige Version wiederhergestellt."); + } + catch (Exception ex) + { + rollbackFailures.Add("Scheduled Task wiederherstellen: " + ex.Message); + } + } + + var rollback = rollbackFailures.Count == 0 + ? "Rollback erfolgreich." + : "Rollback unvollstaendig: " + string.Join(" | ", rollbackFailures); + throw new InvalidOperationException( + "Installation/Update fehlgeschlagen. " + rollback + " Ursache: " + installException.Message, + installException); + } + finally + { + TryDeleteDirectory(stagingDirectory, null); + } } public void Uninstall(bool keepRuntimeData, Action report) @@ -108,14 +219,152 @@ namespace BizTalkCheckmkPulse.Setup throw new FileNotFoundException("Installationspaket ist unvollstaendig. Datei fehlt: " + path, path); } - private static void SetEnvironment(string configPath, string environmentName) + internal string GetInstalledEnvironment() { - var document = new XmlDocument { PreserveWhitespace = true }; - document.Load(configPath); - var setting = document.SelectSingleNode("/configuration/appSettings/add[@key='EnvironmentName']") as XmlElement; - if (setting == null) throw new InvalidDataException("EnvironmentName fehlt in " + configPath + "."); - setting.SetAttribute("value", environmentName); - document.Save(configPath); + var config = Path.Combine(installDirectory, "BizTalkCheckmkPulse.exe.config"); + if (!File.Exists(config)) return string.Empty; + try + { + var document = LoadXml(config); + var setting = FindAppSetting(document, "EnvironmentName"); + return setting == null ? string.Empty : setting.GetAttribute("value").Trim(); + } + catch + { + return string.Empty; + } + } + + internal bool IsInstalled + { + get { return File.Exists(Path.Combine(installDirectory, "BizTalkCheckmkPulse.exe")); } + } + + internal static string PrepareConfig( + string sourceConfig, + string stagedConfig, + string existingConfig, + string requestedEnvironment) + { + var document = LoadXml(sourceConfig); + if (!string.IsNullOrWhiteSpace(existingConfig) && File.Exists(existingConfig)) + { + var existing = LoadXml(existingConfig); + var existingSettings = existing.SelectNodes("/configuration/appSettings/add[@key]"); + if (existingSettings != null) + { + foreach (XmlNode node in existingSettings) + { + var element = node as XmlElement; + if (element == null) continue; + var key = element.GetAttribute("key"); + var value = element.GetAttribute("value"); + if (IsSupersededDefault(key, value)) continue; + var target = FindAppSetting(document, key); + if (target != null) target.SetAttribute("value", value); + } + } + } + + var environmentSetting = FindAppSetting(document, "EnvironmentName"); + if (environmentSetting == null) + throw new InvalidDataException("EnvironmentName fehlt in " + sourceConfig + "."); + if (!string.IsNullOrWhiteSpace(requestedEnvironment)) + environmentSetting.SetAttribute("value", requestedEnvironment.Trim()); + + document.Save(stagedConfig); + return environmentSetting.GetAttribute("value").Trim(); + } + + private static XmlDocument LoadXml(string path) + { + var document = new XmlDocument { PreserveWhitespace = true, XmlResolver = null }; + using (var reader = XmlReader.Create(path, new XmlReaderSettings + { + DtdProcessing = DtdProcessing.Prohibit, + XmlResolver = null + })) + { + document.Load(reader); + } + return document; + } + + private static XmlElement FindAppSetting(XmlDocument document, string key) + { + var nodes = document.SelectNodes("/configuration/appSettings/add[@key]"); + if (nodes == null) return null; + foreach (XmlNode node in nodes) + { + var element = node as XmlElement; + if (element != null && string.Equals(element.GetAttribute("key"), key, StringComparison.Ordinal)) + return element; + } + return null; + } + + private static bool IsSupersededDefault(string key, string value) + { + return string.Equals(key, "EndpointProbeMaxConcurrency", StringComparison.Ordinal) + && string.Equals(value, "12", StringComparison.Ordinal) + || string.Equals(key, "EndpointMaxCount", StringComparison.Ordinal) + && string.Equals(value, "500", StringComparison.Ordinal); + } + + private static void ValidateBatchLogon(string account, string password) + { + var separator = account.IndexOf('\\'); + var domain = account.Substring(0, separator); + var user = account.Substring(separator + 1); + IntPtr token; + if (!LogonUser(user, domain, password, 4, 0, out token)) + { + throw new InvalidOperationException( + "Collector-Anmeldung als Batch fehlgeschlagen: " + + new Win32Exception(Marshal.GetLastWin32Error()).Message + + ". Kennwort und lokales Recht 'Log on as a batch job' pruefen."); + } + + CloseHandle(token); + } + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool LogonUser( + string userName, + string domain, + string password, + int logonType, + int logonProvider, + out IntPtr token); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CloseHandle(IntPtr handle); + + private static void RestoreWrapper(string path, byte[] previousContent) + { + if (previousContent == null) + { + if (File.Exists(path)) File.Delete(path); + return; + } + + Directory.CreateDirectory(Path.GetDirectoryName(path)); + File.WriteAllBytes(path, previousContent); + } + + private static void TryDeleteDirectory(string path, Action report) + { + if (!Directory.Exists(path)) return; + try + { + Directory.Delete(path, true); + } + catch (Exception ex) + { + if (report != null) report("Hinweis: temporaeres Verzeichnis konnte nicht entfernt werden: " + path + " (" + ex.Message + ")"); + } } private static void ApplyDirectoryAcl( @@ -168,7 +417,7 @@ namespace BizTalkCheckmkPulse.Setup throw new InvalidOperationException("Self-Test hat das Zeitlimit ueberschritten."); } var lines = output.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); - if (process.ExitCode != 0 || lines.Length != 8 || lines.Any(x => !x.StartsWith("0 ", StringComparison.Ordinal))) + 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); } } diff --git a/src/BizTalkCheckmkPulse.Setup/MainForm.cs b/src/BizTalkCheckmkPulse.Setup/MainForm.cs index d3b5806..4931c0b 100644 --- a/src/BizTalkCheckmkPulse.Setup/MainForm.cs +++ b/src/BizTalkCheckmkPulse.Setup/MainForm.cs @@ -55,7 +55,9 @@ namespace BizTalkCheckmkPulse.Setup environment.Size = new Size(160, 23); environment.DropDownStyle = ComboBoxStyle.DropDownList; environment.Items.AddRange(new object[] { "(keine)", "ACC", "DEV", "TST", "PRD" }); - environment.SelectedIndex = 0; + var installedEnvironment = engine.GetInstalledEnvironment(); + var installedIndex = environment.Items.IndexOf(installedEnvironment); + environment.SelectedIndex = installedIndex >= 0 ? installedIndex : 0; gmsa.Location = new Point(190, 229); gmsa.Size = new Size(420, 24); @@ -81,7 +83,9 @@ namespace BizTalkCheckmkPulse.Setup status.Multiline = true; status.ReadOnly = true; status.ScrollBars = ScrollBars.Vertical; - status.Text = "Bereit. Setup.exe muss aus dem vollstaendig entpackten Installationspaket gestartet werden."; + status.Text = engine.IsInstalled + ? "Bestehende Installation erkannt. Update behaelt Runtime-Daten und vorhandene Konfigurationswerte bei; Konto und Kennwort werden fuer den Scheduled Task neu bestaetigt." + : "Bereit. Setup.exe muss aus dem vollstaendig entpackten Installationspaket gestartet werden."; Controls.AddRange(new Control[] { diff --git a/src/BizTalkCheckmkPulse.Setup/Properties/AssemblyInfo.cs b/src/BizTalkCheckmkPulse.Setup/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..d147f79 --- /dev/null +++ b/src/BizTalkCheckmkPulse.Setup/Properties/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("BizTalkCheckmkPulse.Tests")] +[assembly: AssemblyTitle("BizTalk Checkmk Pulse Setup")] +[assembly: AssemblyDescription("PowerShell-free installer and updater for BizTalk Checkmk Pulse")] +[assembly: AssemblyCompany("BEW")] +[assembly: AssemblyProduct("BizTalk Checkmk Pulse")] +[assembly: AssemblyVersion("2.2.1.0")] +[assembly: AssemblyFileVersion("2.2.1.0")] diff --git a/src/BizTalkCheckmkPulse.Setup/TaskSchedulerService.cs b/src/BizTalkCheckmkPulse.Setup/TaskSchedulerService.cs index a8f2ad5..6747094 100644 --- a/src/BizTalkCheckmkPulse.Setup/TaskSchedulerService.cs +++ b/src/BizTalkCheckmkPulse.Setup/TaskSchedulerService.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using System.Threading; namespace BizTalkCheckmkPulse.Setup { @@ -10,6 +11,7 @@ namespace BizTalkCheckmkPulse.Setup private const int TaskTriggerTime = 1; private const int TaskActionExecute = 0; private const int TaskInstancesIgnoreNew = 2; + private const int TaskStateRunning = 4; public void RegisterAndStart( string taskName, @@ -91,6 +93,7 @@ namespace BizTalkCheckmkPulse.Setup try { task = root.GetTask(taskName); + task.Enabled = false; try { task.Stop(0); @@ -99,6 +102,14 @@ namespace BizTalkCheckmkPulse.Setup { // Ein nicht laufender Task muss vor dem Loeschen nicht gestoppt werden. } + for (var attempt = 0; attempt < 100 && (int)task.State == TaskStateRunning; attempt++) + { + Thread.Sleep(100); + } + if ((int)task.State == TaskStateRunning) + { + throw new TimeoutException("Scheduled Task konnte innerhalb von 10 Sekunden nicht beendet werden."); + } root.DeleteTask(taskName, 0); } catch (COMException ex) diff --git a/src/BizTalkCheckmkPulse/App.config b/src/BizTalkCheckmkPulse/App.config index a5165e6..987ae1b 100644 --- a/src/BizTalkCheckmkPulse/App.config +++ b/src/BizTalkCheckmkPulse/App.config @@ -21,8 +21,9 @@ - - + + + diff --git a/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs b/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs index 7299cb6..1c6aa9e 100644 --- a/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs +++ b/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs @@ -391,18 +391,22 @@ namespace BizTalkCheckmkPulse : CheckState.Ok; var metrics = string.Format( CultureInfo.InvariantCulture, - "biztalk_endpoints_configured={0};;;0|biztalk_endpoints_active={1};;;0|biztalk_endpoints_tested={2};;;0|biztalk_endpoints_available={3};;;0|biztalk_endpoints_failed={4};;1;0|biztalk_endpoints_unsupported={5};;1;0|biztalk_endpoints_inactive_skipped={6};;;0", + "biztalk_endpoints_configured={0};;;0|biztalk_endpoints_active={1};;;0|biztalk_endpoints_unique_targets={2};;;0|biztalk_endpoints_tested={3};;;0|biztalk_endpoints_available={4};;;0|biztalk_endpoints_failed={5};;1;0|biztalk_endpoints_unsupported={6};;1;0|biztalk_endpoints_inactive_skipped={7};;;0|biztalk_endpoint_probe_duration_ms={8};;;0", endpointState.Configured, endpointState.Active, + endpointState.UniqueTargets, endpointState.Results.Count, available, failed.Length, endpointState.UnsupportedActive, - endpointState.SkippedInactive); + endpointState.SkippedInactive, + endpointState.ProbeDurationMilliseconds); var detail = new StringBuilder(); if (failed.Length == 0 && !incomplete) { - detail.Append("Alle ").Append(endpointState.Active).Append(" aktiven, pruefbaren Send-/Receive-Endpunkte sind erreichbar"); + detail.Append("Alle ").Append(endpointState.Active).Append(" aktiven, pruefbaren Send-/Receive-Endpunkte sind erreichbar") + .Append(" (unique_targets=").Append(endpointState.UniqueTargets) + .Append(", probe_ms=").Append(endpointState.ProbeDurationMilliseconds).Append(")"); } else { @@ -410,7 +414,9 @@ namespace BizTalkCheckmkPulse .Append(", tested=").Append(endpointState.Results.Count) .Append(", available=").Append(available) .Append(", failed=").Append(failed.Length) - .Append(", unsupported_external=").Append(endpointState.UnsupportedActive); + .Append(", unsupported_external=").Append(endpointState.UnsupportedActive) + .Append(", unique_targets=").Append(endpointState.UniqueTargets) + .Append(", probe_ms=").Append(endpointState.ProbeDurationMilliseconds); AppendLimitedList(detail, "unavailable", failed.Select(x => EndpointConnectivityProbe.Display(x.Endpoint) + "(" + EmptyAsUnknown(x.Failure) + ")")); if (!string.IsNullOrWhiteSpace(endpointState.Failure)) diff --git a/src/BizTalkCheckmkPulse/EndpointCatalogStore.cs b/src/BizTalkCheckmkPulse/EndpointCatalogStore.cs index 237d9fb..0cbe4da 100644 --- a/src/BizTalkCheckmkPulse/EndpointCatalogStore.cs +++ b/src/BizTalkCheckmkPulse/EndpointCatalogStore.cs @@ -89,8 +89,8 @@ namespace BizTalkCheckmkPulse MachineName = machine, EnvironmentName = catalogEnvironment, SynchronizedUtc = synchronizedUtc, - ActiveCandidates = ReadInt(root, "activeCandidates", 0, _maxEntries), - UnsupportedCandidates = ReadInt(root, "unsupportedCandidates", 0, _maxEntries) + ActiveCandidates = ReadInt(root, "activeCandidates", 0, 100000), + UnsupportedCandidates = ReadInt(root, "unsupportedCandidates", 0, 100000) }; foreach (var node in root.Elements("Endpoint")) diff --git a/src/BizTalkCheckmkPulse/EndpointConnectivityProbe.cs b/src/BizTalkCheckmkPulse/EndpointConnectivityProbe.cs index 7896b01..66a056e 100644 --- a/src/BizTalkCheckmkPulse/EndpointConnectivityProbe.cs +++ b/src/BizTalkCheckmkPulse/EndpointConnectivityProbe.cs @@ -120,7 +120,15 @@ namespace BizTalkCheckmkPulse try { - foreach (var probeResult in ProbeAllAsync(activeEntries).GetAwaiter().GetResult()) + var probeStopwatch = Stopwatch.StartNew(); + var probeResults = ProbeAllAsync(activeEntries).GetAwaiter().GetResult(); + probeStopwatch.Stop(); + state.ProbeDurationMilliseconds = probeStopwatch.ElapsedMilliseconds; + state.UniqueTargets = activeEntries + .Select(TargetKey) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Count(); + foreach (var probeResult in probeResults) { state.Results.Add(probeResult); if (!probeResult.Available) @@ -192,10 +200,9 @@ namespace BizTalkCheckmkPulse { var catalog = SynchronizeCatalog(existing, candidates, synchronizedUtc); catalog.EnvironmentName = _options.EnvironmentName ?? string.Empty; - if (catalog.Entries.Count > _options.EndpointMaxCount - || catalog.ActiveCandidates > _options.EndpointMaxCount) + if (catalog.Entries.Count > _options.EndpointMaxCount) { - throw new InvalidDataException("Discovered endpoint candidates exceed EndpointMaxCount."); + throw new InvalidDataException("Probeable endpoints exceed EndpointMaxCount."); } return catalog; @@ -221,7 +228,7 @@ namespace BizTalkCheckmkPulse private async Task> ProbeAllAsync(EndpointCatalogEntry[] endpoints) { var unique = endpoints - .GroupBy(x => x.Protocol.ToUpperInvariant() + "|" + x.Host.ToUpperInvariant() + "|" + x.Port) + .GroupBy(TargetKey, StringComparer.OrdinalIgnoreCase) .ToArray(); var outcomes = new Dictionary(StringComparer.OrdinalIgnoreCase); using (var gate = new SemaphoreSlim(_options.EndpointProbeMaxConcurrency)) @@ -248,7 +255,7 @@ namespace BizTalkCheckmkPulse return endpoints.Select(endpoint => { - var key = endpoint.Protocol.ToUpperInvariant() + "|" + endpoint.Host.ToUpperInvariant() + "|" + endpoint.Port; + var key = TargetKey(endpoint); var outcome = outcomes[key]; return new EndpointProbeResult { @@ -260,6 +267,28 @@ namespace BizTalkCheckmkPulse }).ToArray(); } + internal static long CalculateWorstCaseProbeMilliseconds(int targetCount, int concurrency, int timeoutMilliseconds) + { + if (targetCount <= 0) + { + return 0; + } + + if (concurrency <= 0 || timeoutMilliseconds <= 0) + { + throw new ArgumentOutOfRangeException("concurrency"); + } + + return ((targetCount + concurrency - 1L) / concurrency) * timeoutMilliseconds; + } + + private static string TargetKey(EndpointCatalogEntry endpoint) + { + return endpoint.Protocol.ToUpperInvariant() + + "|" + endpoint.Host.ToUpperInvariant() + + "|" + endpoint.Port; + } + private async Task ProbeOneAsync(EndpointCatalogEntry endpoint) { return string.Equals(endpoint.Protocol, "UDP", StringComparison.OrdinalIgnoreCase) diff --git a/src/BizTalkCheckmkPulse/Models.cs b/src/BizTalkCheckmkPulse/Models.cs index 79fc8f7..a45753d 100644 --- a/src/BizTalkCheckmkPulse/Models.cs +++ b/src/BizTalkCheckmkPulse/Models.cs @@ -349,6 +349,8 @@ namespace BizTalkCheckmkPulse public int Active { get; set; } public int SkippedInactive { get; set; } public int UnsupportedActive { get; set; } + public int UniqueTargets { get; set; } + public long ProbeDurationMilliseconds { get; set; } public string Failure { get; set; } public List Results { get; private set; } } diff --git a/src/BizTalkCheckmkPulse/MonitoringOptions.cs b/src/BizTalkCheckmkPulse/MonitoringOptions.cs index dafa858..bcedc01 100644 --- a/src/BizTalkCheckmkPulse/MonitoringOptions.cs +++ b/src/BizTalkCheckmkPulse/MonitoringOptions.cs @@ -85,8 +85,8 @@ namespace BizTalkCheckmkPulse EndpointCatalogMaxBytes = 1048576; EndpointDiscoveryIntervalHours = 168; EndpointProbeTimeoutMilliseconds = 3000; - EndpointProbeMaxConcurrency = 12; - EndpointMaxCount = 500; + EndpointProbeMaxConcurrency = 16; + EndpointMaxCount = 100; } /// diff --git a/src/BizTalkCheckmkPulse/Program.cs b/src/BizTalkCheckmkPulse/Program.cs index 84f3718..621cfd2 100644 --- a/src/BizTalkCheckmkPulse/Program.cs +++ b/src/BizTalkCheckmkPulse/Program.cs @@ -102,6 +102,10 @@ namespace BizTalkCheckmkPulse + result.EndpointConnectivity.Active + " endpoints_failed=" + result.EndpointConnectivity.Results.Count(x => !x.Available) + + " endpoints_unique_targets=" + + result.EndpointConnectivity.UniqueTargets + + " endpoint_probe_ms=" + + result.EndpointConnectivity.ProbeDurationMilliseconds + " elapsed_ms=" + stopwatch.ElapsedMilliseconds); return 0; diff --git a/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs b/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs index 8152431..1b32362 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.0.0")] -[assembly: AssemblyFileVersion("2.2.0.0")] +[assembly: AssemblyVersion("2.2.1.0")] +[assembly: AssemblyFileVersion("2.2.1.0")] diff --git a/tests/BizTalkCheckmkPulse.Tests/BizTalkCheckmkPulse.Tests.csproj b/tests/BizTalkCheckmkPulse.Tests/BizTalkCheckmkPulse.Tests.csproj index a9a5d74..aea08d4 100644 --- a/tests/BizTalkCheckmkPulse.Tests/BizTalkCheckmkPulse.Tests.csproj +++ b/tests/BizTalkCheckmkPulse.Tests/BizTalkCheckmkPulse.Tests.csproj @@ -39,6 +39,10 @@ {A4D4D050-9EA7-4A71-B510-7D9D699B9F38} BizTalkCheckmkPulse + + {764AC43A-26D8-43C1-9121-13FC8A9CC8B1} + BizTalkCheckmkPulse.Setup + diff --git a/tests/BizTalkCheckmkPulse.Tests/Program.cs b/tests/BizTalkCheckmkPulse.Tests/Program.cs index a8bf0b1..f32eb91 100644 --- a/tests/BizTalkCheckmkPulse.Tests/Program.cs +++ b/tests/BizTalkCheckmkPulse.Tests/Program.cs @@ -32,6 +32,8 @@ namespace BizTalkCheckmkPulse.Tests Run("EndpointCatalogPreservesManualOverrides", EndpointCatalogPreservesManualOverrides); Run("EndpointCatalogRoundTrip", EndpointCatalogRoundTrip); Run("EndpointOutputListsOnlyUnavailableTargets", EndpointOutputListsOnlyUnavailableTargets); + Run("EndpointProbeBudgetFitsMinuteInterval", EndpointProbeBudgetFitsMinuteInterval); + Run("InstallerUpdatePreservesExistingSettings", InstallerUpdatePreservesExistingSettings); Run("SnapshotRoundTripPreservesLines", SnapshotRoundTripPreservesLines); Run("SnapshotRejectsTampering", SnapshotRejectsTampering); Run("SnapshotRejectsStaleData", SnapshotRejectsStaleData); @@ -454,11 +456,11 @@ namespace BizTalkCheckmkPulse.Tests { EnvironmentName = "ACC", SynchronizedUtc = DateTime.UtcNow, - ActiveCandidates = 1, + ActiveCandidates = 150, UnsupportedCandidates = 0 }; catalog.Entries.Add(TestEndpoint("Orders", "api.example.test")); - var store = new EndpointCatalogStore(path, 1048576, 500); + var store = new EndpointCatalogStore(path, 1048576, 100); store.Write(catalog); var loaded = store.Read("ACC"); @@ -493,6 +495,75 @@ namespace BizTalkCheckmkPulse.Tests }; } + private static void EndpointProbeBudgetFitsMinuteInterval() + { + var options = new MonitoringOptions(); + var seventyTargets = EndpointConnectivityProbe.CalculateWorstCaseProbeMilliseconds( + 70, + options.EndpointProbeMaxConcurrency, + options.EndpointProbeTimeoutMilliseconds); + var configuredMaximum = EndpointConnectivityProbe.CalculateWorstCaseProbeMilliseconds( + options.EndpointMaxCount, + options.EndpointProbeMaxConcurrency, + options.EndpointProbeTimeoutMilliseconds); + + AssertEqual(15000, (int)seventyTargets, "70-target theoretical timeout budget"); + Assert(configuredMaximum <= 30000, "configured endpoint worst-case must leave headroom in the minute interval"); + } + + private static void InstallerUpdatePreservesExistingSettings() + { + var directory = Path.Combine(Path.GetTempPath(), "BizTalkCheckmkPulse.InstallerTests." + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(directory); + try + { + var source = Path.Combine(directory, "source.config"); + var existing = Path.Combine(directory, "existing.config"); + var staged = Path.Combine(directory, "staged.config"); + File.WriteAllText( + source, + "" + + "" + + "" + + "" + + "" + + "", + new UTF8Encoding(false)); + File.WriteAllText( + existing, + "" + + "" + + "" + + "" + + "" + + "", + new UTF8Encoding(false)); + + var effectiveEnvironment = BizTalkCheckmkPulse.Setup.InstallerEngine.PrepareConfig( + source, + staged, + existing, + string.Empty); + var merged = File.ReadAllText(staged); + AssertEqual("ACC", effectiveEnvironment, "preserved installer environment"); + Assert(merged.Contains("key=\"EndpointProbeMaxConcurrency\" value=\"7\""), "existing operational value must be preserved"); + Assert(merged.Contains("key=\"EndpointMaxCount\" value=\"100\""), "superseded old default must migrate to new bounded default"); + Assert(merged.Contains("key=\"NewSetting\" value=\"new-default\""), "new source setting must be added"); + Assert(merged.IndexOf("RemovedLegacySetting", StringComparison.Ordinal) < 0, "removed legacy key must not be resurrected"); + + effectiveEnvironment = BizTalkCheckmkPulse.Setup.InstallerEngine.PrepareConfig( + source, + staged, + existing, + "PRD"); + AssertEqual("PRD", effectiveEnvironment, "requested environment override"); + } + finally + { + if (Directory.Exists(directory)) Directory.Delete(directory, true); + } + } + private static void SnapshotRejectsTampering() { WithTemporarySnapshot((path, store) =>