Make installer runtime acceptance transactional

This commit is contained in:
2026-08-10 18:28:49 +02:00
parent 8fe1b57173
commit 448be8dc49
20 changed files with 874 additions and 65 deletions
+18
View File
@@ -408,6 +408,17 @@ Task gestoppt und die Programmversion umgeschaltet. Scheitert ein Folgeschritt,
versucht der Installer Programmverzeichnis, Checkmk-Wrapper und Scheduled Task
auf den vorherigen Stand zurückzusetzen.
Ab Version 2.2.4 ist die Umschaltung erst nach einer transaktionalen
Runtime-Abnahme abgeschlossen. Ein einmaliger, triggerloser Task läuft unter
dem echten Collector-Konto und erzwingt einen vollständigen Endpoint-
Katalogabgleich. Der Installer wartet begrenzt auf `LastTaskResult=0` und lässt
anschließend die installierte Produkt-EXE Snapshot und Katalog mit den normalen
Produktparsern validieren. Der Snapshot muss aus diesem Lauf stammen, die
erwartete Identität und alle neun eindeutigen Services ohne `UNKNOWN`
enthalten. Erst dann wird der normale Minutentask registriert und das Backup
gelöscht. Bei Fehler oder Timeout wird die alte Version wiederhergestellt und
ein frischer Lauf des alten Tasks abgewartet.
Der installierte Task selbst läuft mit `RunLevel Limited`. Der Installer
übergibt das Kennwort direkt an die Windows-Task-Scheduler-COM-Schnittstelle;
es wird weder in einer Prozesskommandozeile noch in Config oder Log
@@ -550,6 +561,8 @@ BizTalk-Probes:
| Key | Default |
| --- | --- |
| `Server` | `.` |
| `EnvironmentName` | leer |
| `IncludeEnvironmentInServiceName` | `false` |
| `QueryTimeoutSeconds` | `25` |
| `ProbeSqlConnectivity` | `true` |
| `SqlConnectionTimeoutSeconds` | `5` |
@@ -602,6 +615,11 @@ Automatisiert:
einem theoretischen Socket-Budget von etwa 15 Sekunden; das konfigurierte
Maximum von 100 Zielen in etwa 21 Sekunden,
- Update-Config-Merge erhält bestehende Werte und fügt neue Defaults hinzu,
- erzwungener Installations-Katalogabgleich wird unabhängig vom Katalogalter
ausgeführt,
- Runtime-Abnahme weist alten/manipulierten Snapshot, falsche Identität,
fehlenden Service, `UNKNOWN`, alten/ungültigen Katalog und fehlerhaften oder
nicht rechtzeitig beendeten Tasklauf zurück,
- Snapshot-Roundtrip und Ersatz,
- SHA-256-Manipulation wird verworfen,
- Stale-Snapshot wird verworfen,
+27 -12
View File
@@ -141,8 +141,11 @@ Pro Service werden standardmäßig maximal fünf betroffene Namen gezeigt.
Weitere Treffer erscheinen nur als `(+n more)`; Details sind zusätzlich auf
1600 Zeichen begrenzt. Metriken bleiben trotzdem vollständig.
Mit `EnvironmentName=ACC`, `DEV`, `TST` oder `PRD` wird die Umgebung in den
Servicenamen aufgenommen, zum Beispiel `BizTalk ACC Platform`.
`EnvironmentName=ACC`, `DEV`, `TST` oder `PRD` kennzeichnet Snapshot und
Endpoint-Katalog. Die neun Checkmk-Servicenamen bleiben standardmäßig
unverändert. Nur das explizite Opt-in
`IncludeEnvironmentInServiceName=true` ergänzt die Umgebung im Servicenamen
und verlangt anschließend eine Checkmk Service Discovery.
Statuslogik und Metriken: [docs/CheckmkServices.md](docs/CheckmkServices.md)
@@ -217,7 +220,9 @@ artifacts\BizTalkCheckmkPulse-Setup\application\BizTalkCheckmkPulse.exe --self-t
Erwartet werden exakt neun `OK`-Zeilen. Die Regressionstests prüfen
zusätzlich Snapshot-Roundtrip, atomaren Ersatz, SHA-256-Manipulation,
Stale-Erkennung, stabile Fallbacks und die bestehenden BizTalk-WMI-Diagnosen.
Stale-Erkennung, stabile Fallbacks, den exakten Servicevertrag, erzwungene
Katalogaktualisierung sowie erfolgreiche und abgewiesene
Post-Install-Runtime-Abnahmen.
Ein Mono-Build ist eine hilfreiche Quellcodeprüfung, ersetzt aber nicht die
Windows-/BizTalk-Laufzeitvalidierung.
@@ -265,17 +270,26 @@ Der Installer:
2. baut die neue Version in einem Staging-Verzeichnis auf und übernimmt
vorhandene AppSettings; neue Keys erhalten ihre neuen Defaults. Die alten
unveränderten 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. behält `%ProgramData%\BizTalkCheckmkPulse` mit Endpoint-Katalog, Snapshot
3. vergleicht installierten und neuen Checkmk-Servicevertrag; ein
unbestätigter Rename stoppt vor jeder Umschaltung,
4. schaltet die Programmdateien per Verzeichniswechsel um,
5. startet einen triggerlosen Provider-Abnahmelauf unter dem echten
Collector-Konto und erzwingt einen vollständigen Endpoint-Katalogabgleich,
6. wartet höchstens vier Minuten auf `LastTaskResult=0` und validiert danach
mit der Produkt-EXE einen erst nach Installationsbeginn erzeugten Snapshot,
die Collector-Identität, alle neun eindeutigen Services ohne `UNKNOWN`
sowie den frischen, maschinen- und umgebungsgebundenen Endpoint-Katalog,
7. registriert erst nach dieser Abnahme den normalen Minutentask; andernfalls
werden vorherige Version, Wrapper und Task samt frischem Altsnapshot
wiederhergestellt,
8. behält `%ProgramData%\BizTalkCheckmkPulse` mit Endpoint-Katalog, Snapshot
und Logs bei,
5. installiert EXE und Config nach `%ProgramFiles%\BizTalkCheckmkPulse`,
6. setzt explizite ACLs für Administratoren, Provider und `LocalSystem`,
7. installiert nur den kleinen `.cmd`-Consumer unter
9. installiert EXE und Config nach `%ProgramFiles%\BizTalkCheckmkPulse`, setzt
explizite ACLs für Administratoren, Provider und `LocalSystem` und
installiert nur den kleinen `.cmd`-Consumer unter
`%ProgramData%\checkmk\agent\local`,
8. registriert `BizTalk Checkmk Pulse Provider` minütlich mit
`IgnoreNew`, fünf Minuten Laufzeitlimit und zwei Wiederholungen,
9. führt den installierten Self-Test erneut aus und startet den Provider.
10. registriert `BizTalk Checkmk Pulse Provider` minütlich mit `IgnoreNew`,
fünf Minuten Laufzeitlimit und zwei Wiederholungen.
Installation, Update, Deinstallation und Laufzeit verwenden keine PowerShell.
@@ -356,6 +370,7 @@ Wichtige Werte:
| Key | Default | Bedeutung |
| --- | --- | --- |
| `EnvironmentName` | leer | Optional `ACC`, `DEV`, `TST`, `PRD`. |
| `IncludeEnvironmentInServiceName` | `false` | Nur mit geplanter Service Discovery aktivieren. |
| `SnapshotPath` | `%ProgramData%\BizTalkCheckmkPulse\data\...` | Gemeinsame Provider-/Consumer-Datei. |
| `SnapshotMaxAgeSeconds` | `180` | Ab diesem Alter liefert der Consumer `UNKNOWN`. |
| `SnapshotMaxBytes` | `1048576` | Harte Eingabegröße für den Consumer. |
+19
View File
@@ -36,6 +36,25 @@ Sicherheitsstopp. Die Installation darf nur über die Checkbox
fortgesetzt werden. Der Installer zeigt dabei die entfernten und neuen Namen
an. Eine bloß geänderte Ausgabereihenfolge gilt nicht als Rename.
Version 2.2.4 nimmt die umgeschaltete Installation zusätzlich vollständig ab:
- einmaliger, triggerloser Providerlauf unter dem angegebenen Collector-Konto,
- erzwungener vollständiger Abgleich von `endpoints.xml`, wobei gültige
manuelle Overrides erhalten bleiben,
- maximal vier Minuten Warten auf `LastTaskResult=0`,
- Snapshot muss nach Beginn des Abnahmelaufs erzeugt, SHA-256-valid, für die
aktuelle Maschine und vom erwarteten Collector-Konto geschrieben sein,
- alle neun stabilen Services müssen eindeutig vorhanden sein; `WARN` und
`CRIT` sind reale Betriebszustände, `UNKNOWN` blockiert die Installation,
- bei aktivierter Endpoint-Prüfung muss auch der Katalog nach Beginn des
Abnahmelaufs atomar synchronisiert worden sein.
Erst danach ersetzt der Installer den Abnahmetask durch den normalen
Minutentask und löscht das Backup. Bei Fehler oder Timeout wird die vorherige
Programmversion samt Task wiederhergestellt und ein frischer Lauf der alten
Version abgewartet. Fehler in Paket-, Config- oder Rename-Vorprüfung treten vor
jeder Taskänderung auf und lassen die laufende Installation unangetastet.
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
@@ -75,6 +75,10 @@ nicht netzwerkprüfbare Kandidaten. Version 2.2.2 weist sie explizit als
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.
- Version 2.2.4 erzwingt nach der Umschaltung einen frischen vollständigen
Endpoint-Katalogabgleich und akzeptiert die Installation erst nach einem
erfolgreichen Providerlauf mit neuem, identitäts- und integritätsgeprüftem
Snapshot sowie neun verlässlichen stabilen Services.
### Korrekte Abdeckungssemantik
@@ -135,7 +139,7 @@ Endpoint-/Namensfix bewusst nicht grüngefärbt.
## ACC-Abnahme nach Installation
1. Setup 2.2.3 als Update ausführen; Umgebung `ACC` kann bestehen bleiben.
1. Setup 2.2.4 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:
+6
View File
@@ -15,6 +15,12 @@ Abgleich alle `EndpointDiscoveryIntervalHours` (Default: 168 Stunden). Ein
Fehler beim Abgleich ersetzt keinen letzten gültigen Katalog. Checkmk meldet
den überfälligen/fehlgeschlagenen Abgleich trotzdem als `UNKNOWN`.
Bei Installation oder Update ab Version 2.2.4 erzwingt ein einmaliger
Provider-Abnahmelauf unabhängig vom Alter einen vollständigen Abgleich. Der
Installer akzeptiert den Release erst, wenn der neue Synchronisationszeitpunkt
nach dem Start dieses Laufs liegt. Gültige manuelle Overrides werden dabei
erhalten; der Katalog wird nicht blind gelöscht.
Unabhängig vom Wochenabgleich wird bei jedem Minutenlauf gegen den aktuellen
BizTalk-Runtimezustand gefiltert:
+11
View File
@@ -53,6 +53,17 @@ Verbindlicher Agent-Dump:
## Service Discovery
Das Setup 2.2.4 schließt erst erfolgreich ab, nachdem der erste Providerlauf
unter dem echten Collector-Konto einen frischen Snapshot und bei aktivierter
Endpoint-Prüfung einen frisch synchronisierten Katalog erzeugt hat. Die neun
stabilen Services müssen vollständig und ohne `UNKNOWN` vorliegen. Diese lokale
Runtime-Abnahme ersetzt nicht die zentrale Checkmk Service Discovery.
`EnvironmentName` kennzeichnet Snapshot und Katalog, ändert die neun
Servicenamen aber standardmäßig nicht. Nur das bewusste Opt-in
`IncludeEnvironmentInServiceName=true` erzeugt umgebungsabhängige Namen und
verlangt den anschließenden Discovery-Abgleich.
1. Provider dreimal erfolgreich laufen lassen.
2. `LastTaskResult=0`, frischen Snapshot und Log prüfen.
3. Agent-Dump im `LocalSystem`-Kontext prüfen.
+5 -4
View File
@@ -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.3 | Stand 10.08.2026 | Seite "), size=7.5, color=MUTED)
format_run(p.add_run("Version 2.2.4 | 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.3"
core.comments = "Externe Lösungsübersicht zum implementierten Stand 2.2.4"
# 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.3"),
("Lösungsstand", "BizTalk Checkmk Pulse 2.2.4"),
("Zielplattform", "Microsoft BizTalk Server 2020 · Windows Server 2019 · Checkmk 2.4"),
("Dokumentstand", "10. August 2026"),
("Dokumentzweck", "Architektur- und Leistungsüberblick für technische Stakeholder"),
@@ -544,6 +544,7 @@ def build_document():
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_bullet(document, "Nach der Umschaltung erzwingt das Setup einen frischen Katalogabgleich und akzeptiert die Installation erst nach einem erfolgreichen, identitäts- und integritätsgeprüften Providerlauf ohne UNKNOWN in den neun stabilen Services.")
add_heading(document, "Sicherheitsmodell", level=2)
add_table(
@@ -572,7 +573,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.3 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.4 sowie Checkmk User Guide „Local checks“, abgerufen am 10. August 2026.", {"size": 7.8, "color": MUTED}),),
before=8,
after=0,
)
@@ -7,6 +7,7 @@ using System.Linq;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text;
using System.Xml;
namespace BizTalkCheckmkPulse.Setup
@@ -14,6 +15,7 @@ namespace BizTalkCheckmkPulse.Setup
internal sealed class InstallerEngine
{
internal const string TaskName = "BizTalk Checkmk Pulse Provider";
private static readonly TimeSpan PostInstallValidationTimeout = TimeSpan.FromMinutes(4);
private readonly string packageDirectory;
private readonly string installDirectory;
private readonly string runtimeDirectory;
@@ -54,7 +56,8 @@ namespace BizTalkCheckmkPulse.Setup
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 hadExistingExecutable = File.Exists(targetExe);
var mutationStarted = false;
var backupCreated = false;
var filesActivated = false;
var scheduler = new TaskSchedulerService();
@@ -94,8 +97,8 @@ namespace BizTalkCheckmkPulse.Setup
}
// Erst nach vollstaendiger Staging-Pruefung wird der laufende Provider angehalten.
mutationStarted = true;
scheduler.DeleteIfExists(TaskName);
taskRemoved = true;
report("Vorhandener Scheduled Task angehalten und fuer das Update entfernt.");
if (hadExistingInstallation)
@@ -129,28 +132,53 @@ namespace BizTalkCheckmkPulse.Setup
RunSelfTest(targetExe);
report("Installierter Self-Test erfolgreich: neun Checkmk-Services.");
scheduler.RegisterAndStart(
var runtimeValidationStartedUtc = DateTime.UtcNow;
scheduler.RegisterValidationAndStart(
TaskName,
targetExe,
installDirectory,
account,
isGmsa ? null : password,
isGmsa);
taskRemoved = false;
report("Scheduled Task registriert und einmalig gestartet: " + TaskName);
report("Einmaliger Provider-Abnahmelauf mit erzwungenem Endpoint-Katalogabgleich gestartet.");
var completedRun = scheduler.WaitForSuccessfulRun(
TaskName,
runtimeValidationStartedUtc,
PostInstallValidationTimeout,
report);
report("Provider-Abnahmelauf erfolgreich: LastTaskResult=" + completedRun.ExitCode + ".");
var runtimeValidation = RunRuntimeValidation(
targetExe,
runtimeValidationStartedUtc,
account);
report("Post-Install-Runtime-Abnahme erfolgreich: " + runtimeValidation);
scheduler.RegisterRecurring(
TaskName,
targetExe,
installDirectory,
account,
isGmsa ? null : password,
isGmsa);
report("Validierter minuetlicher Scheduled Task registriert: " + TaskName);
TryDeleteDirectory(backupDirectory, report);
}
catch (Exception installException)
{
var rollbackFailures = new List<string>();
try
if (mutationStarted)
{
scheduler.DeleteIfExists(TaskName);
}
catch (Exception ex)
{
rollbackFailures.Add("Task stoppen: " + ex.Message);
try
{
scheduler.DeleteIfExists(TaskName);
}
catch (Exception ex)
{
rollbackFailures.Add("Task stoppen: " + ex.Message);
}
}
if (filesActivated || backupCreated)
@@ -167,27 +195,36 @@ namespace BizTalkCheckmkPulse.Setup
}
}
try
{
RestoreWrapper(targetWrapper, previousWrapper);
}
catch (Exception ex)
{
rollbackFailures.Add("Checkmk-Wrapper wiederherstellen: " + ex.Message);
}
if (taskRemoved && hadExistingInstallation && File.Exists(targetExe))
if (mutationStarted)
{
try
{
scheduler.RegisterAndStart(
RestoreWrapper(targetWrapper, previousWrapper);
}
catch (Exception ex)
{
rollbackFailures.Add("Checkmk-Wrapper wiederherstellen: " + ex.Message);
}
}
if (mutationStarted && hadExistingExecutable && File.Exists(targetExe))
{
try
{
var rollbackRunStartedUtc = DateTime.UtcNow;
scheduler.RegisterRecurringAndStart(
TaskName,
targetExe,
installDirectory,
account,
isGmsa ? null : password,
isGmsa);
report("Scheduled Task fuer die vorherige Version wiederhergestellt.");
scheduler.WaitForSuccessfulRun(
TaskName,
rollbackRunStartedUtc,
PostInstallValidationTimeout,
report);
report("Scheduled Task und frischer Snapshot der vorherigen Version wiederhergestellt.");
}
catch (Exception ex)
{
@@ -445,6 +482,45 @@ namespace BizTalkCheckmkPulse.Setup
}
}
private static string RunRuntimeValidation(
string executable,
DateTime notBeforeUtc,
string expectedIdentity)
{
var identityBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(expectedIdentity));
var arguments = "--validate-runtime --validation-not-before-utc "
+ notBeforeUtc.ToUniversalTime().ToString("o")
+ " --expected-identity-base64 " + identityBase64;
var start = new ProcessStartInfo(executable, arguments)
{
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true
};
using (var process = Process.Start(start))
{
if (process == null) throw new InvalidOperationException("Runtime-Abnahme konnte nicht gestartet werden.");
var output = process.StandardOutput.ReadToEnd().Trim();
var error = process.StandardError.ReadToEnd().Trim();
if (!process.WaitForExit(30000))
{
process.Kill();
throw new TimeoutException("Runtime-Abnahme hat das Zeitlimit ueberschritten.");
}
if (process.ExitCode != 0
|| !output.StartsWith("RUNTIME_VALIDATION_V1 ", StringComparison.Ordinal))
{
throw new InvalidOperationException(
"Runtime-Abnahme fehlgeschlagen. Exitcode=" + process.ExitCode
+ ". " + (error.Length == 0 ? output : error));
}
return output;
}
}
private static string ExtractServiceName(string line)
{
var firstQuote = line.IndexOf('"');
@@ -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.3.0")]
[assembly: AssemblyFileVersion("2.2.3.0")]
[assembly: AssemblyVersion("2.2.4.0")]
[assembly: AssemblyFileVersion("2.2.4.0")]
@@ -13,18 +13,64 @@ namespace BizTalkCheckmkPulse.Setup
private const int TaskInstancesIgnoreNew = 2;
private const int TaskStateRunning = 4;
public void RegisterAndStart(
public void RegisterValidationAndStart(
string taskName,
string executable,
string workingDirectory,
string account,
string password,
bool isGmsa)
{
Register(
taskName,
executable,
workingDirectory,
account,
password,
isGmsa,
"--collect --force-endpoint-refresh",
false,
true);
}
public void RegisterRecurring(
string taskName,
string executable,
string workingDirectory,
string account,
string password,
bool isGmsa)
{
Register(taskName, executable, workingDirectory, account, password, isGmsa, "--collect", true, false);
}
public void RegisterRecurringAndStart(
string taskName,
string executable,
string workingDirectory,
string account,
string password,
bool isGmsa)
{
Register(taskName, executable, workingDirectory, account, password, isGmsa, "--collect", true, true);
}
private static void Register(
string taskName,
string executable,
string workingDirectory,
string account,
string password,
bool isGmsa,
string arguments,
bool enableRecurringTrigger,
bool startImmediately)
{
dynamic service = null;
dynamic root = null;
dynamic definition = null;
dynamic registeredTask = null;
dynamic runningTask = null;
try
{
var schedulerType = Type.GetTypeFromProgID("Schedule.Service", true);
@@ -52,12 +98,12 @@ namespace BizTalkCheckmkPulse.Setup
dynamic trigger = definition.Triggers.Create(TaskTriggerTime);
trigger.StartBoundary = DateTime.Now.AddMinutes(1).ToString("s");
trigger.Enabled = true;
trigger.Enabled = enableRecurringTrigger;
trigger.Repetition.Interval = "PT1M";
dynamic action = definition.Actions.Create(TaskActionExecute);
action.Path = executable;
action.Arguments = "--collect";
action.Arguments = arguments;
action.WorkingDirectory = workingDirectory;
registeredTask = root.RegisterTaskDefinition(
@@ -68,10 +114,14 @@ namespace BizTalkCheckmkPulse.Setup
isGmsa ? null : password,
TaskLogonPassword,
null);
registeredTask.Run(null);
if (startImmediately)
{
runningTask = registeredTask.Run(null);
}
}
finally
{
Release(runningTask);
Release(registeredTask);
Release(definition);
Release(root);
@@ -79,6 +129,81 @@ namespace BizTalkCheckmkPulse.Setup
}
}
public TaskRunResult WaitForSuccessfulRun(
string taskName,
DateTime notBeforeUtc,
TimeSpan timeout,
Action<string> report)
{
dynamic service = null;
dynamic root = null;
dynamic task = null;
var startedUtc = DateTime.UtcNow;
var nextReportUtc = startedUtc.AddSeconds(15);
var lastState = -1;
var lastResult = int.MinValue;
DateTime? lastRunUtc = null;
try
{
var schedulerType = Type.GetTypeFromProgID("Schedule.Service", true);
service = Activator.CreateInstance(schedulerType);
service.Connect();
root = service.GetFolder("\\");
task = root.GetTask(taskName);
while (DateTime.UtcNow - startedUtc < timeout)
{
lastState = (int)task.State;
lastResult = (int)task.LastTaskResult;
var lastRun = (DateTime)task.LastRunTime;
lastRunUtc = lastRun.Year <= 1900
? (DateTime?)null
: DateTime.SpecifyKind(lastRun, DateTimeKind.Local).ToUniversalTime();
if (IsSuccessfulCompletion(lastState, lastResult, lastRunUtc, notBeforeUtc))
{
return new TaskRunResult(lastRunUtc.Value, lastResult);
}
if (report != null && DateTime.UtcNow >= nextReportUtc)
{
report(
"Warte auf erfolgreichen Providerlauf: state=" + lastState
+ ", last_result=" + lastResult
+ ", last_run=" + (lastRunUtc.HasValue ? lastRunUtc.Value.ToString("o") : "(noch keiner)") + ".");
nextReportUtc = DateTime.UtcNow.AddSeconds(15);
}
Thread.Sleep(500);
}
throw new TimeoutException(
"Providerlauf wurde innerhalb von " + Math.Ceiling(timeout.TotalSeconds)
+ " Sekunden nicht erfolgreich abgeschlossen. state=" + lastState
+ ", last_result=" + lastResult
+ ", last_run=" + (lastRunUtc.HasValue ? lastRunUtc.Value.ToString("o") : "(noch keiner)") + ".");
}
finally
{
Release(task);
Release(root);
Release(service);
}
}
internal static bool IsSuccessfulCompletion(
int taskState,
int lastTaskResult,
DateTime? lastRunUtc,
DateTime notBeforeUtc)
{
return taskState != TaskStateRunning
&& lastTaskResult == 0
&& lastRunUtc.HasValue
// Task Scheduler kann LastRunTime mit geringerer Aufloesung als DateTime.UtcNow liefern.
&& lastRunUtc.Value >= notBeforeUtc.ToUniversalTime().AddSeconds(-2);
}
public void DeleteIfExists(string taskName)
{
dynamic service = null;
@@ -131,4 +256,16 @@ namespace BizTalkCheckmkPulse.Setup
if (value != null && Marshal.IsComObject(value)) Marshal.FinalReleaseComObject(value);
}
}
internal sealed class TaskRunResult
{
public TaskRunResult(DateTime lastRunUtc, int exitCode)
{
LastRunUtc = lastRunUtc;
ExitCode = exitCode;
}
public DateTime LastRunUtc { get; private set; }
public int ExitCode { get; private set; }
}
}
+2
View File
@@ -5,6 +5,8 @@
<add key="Server" value="." />
<add key="ServicePrefix" value="BizTalk" />
<add key="EnvironmentName" value="" />
<!-- Stabiler Checkmk-Servicevertrag; nur mit geplanter Service Discovery aktivieren. -->
<add key="IncludeEnvironmentInServiceName" value="false" />
<!-- Provider/Consumer-Datei. Der Scheduled Task schreibt, LocalSystem liest. -->
<add key="SnapshotPath" value="%ProgramData%\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot" />
@@ -52,6 +52,7 @@
<Compile Include="MonitoringOptions.cs" />
<Compile Include="Models.cs" />
<Compile Include="Program.cs" />
<Compile Include="RuntimeValidator.cs" />
<Compile Include="SqlConnectivityProbe.cs" />
<Compile Include="SnapshotStore.cs" />
<Compile Include="WmiBizTalkProbe.cs" />
@@ -62,7 +62,8 @@ namespace BizTalkCheckmkPulse
}
var now = DateTime.UtcNow;
state.RefreshRequired = catalog == null
state.RefreshRequired = _options.ForceEndpointCatalogRefresh
|| catalog == null
|| catalog.SynchronizedUtc > now.AddMinutes(5)
|| now - catalog.SynchronizedUtc >= TimeSpan.FromHours(_options.EndpointDiscoveryIntervalHours);
if (state.RefreshRequired)
@@ -4,6 +4,7 @@ using System.Configuration;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
namespace BizTalkCheckmkPulse
{
@@ -49,6 +50,10 @@ namespace BizTalkCheckmkPulse
public int EndpointMaxCount { get; set; }
public bool Collect { get; set; }
public bool SelfTest { get; set; }
public bool ForceEndpointCatalogRefresh { get; set; }
public bool RuntimeValidation { get; set; }
public DateTime? RuntimeValidationNotBeforeUtc { get; set; }
public string ExpectedRuntimeIdentity { get; set; }
/// <summary>
/// Setzt konservative Standardwerte fuer BizTalk Server 2020.
@@ -91,6 +96,7 @@ namespace BizTalkCheckmkPulse
EndpointProbeTimeoutMilliseconds = 3000;
EndpointProbeMaxConcurrency = 16;
EndpointMaxCount = 100;
ExpectedRuntimeIdentity = string.Empty;
}
/// <summary>
@@ -182,6 +188,33 @@ namespace BizTalkCheckmkPulse
{
options.Collect = true;
}
else if (EqualsAny(arg, "--force-endpoint-refresh", "/force-endpoint-refresh"))
{
options.ForceEndpointCatalogRefresh = true;
}
else if (EqualsAny(arg, "--validate-runtime", "/validate-runtime"))
{
options.RuntimeValidation = true;
}
else if (EqualsAny(arg, "--validation-not-before-utc", "/validation-not-before-utc") && i + 1 < args.Length)
{
DateTime value;
if (!DateTime.TryParseExact(
args[++i],
"o",
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out value))
{
throw new ConfigurationErrorsException("Runtime validation timestamp must use the round-trip UTC format.");
}
options.RuntimeValidationNotBeforeUtc = value;
}
else if (EqualsAny(arg, "--expected-identity-base64", "/expected-identity-base64") && i + 1 < args.Length)
{
options.ExpectedRuntimeIdentity = new UTF8Encoding(false, true)
.GetString(Convert.FromBase64String(args[++i]));
}
else if (EqualsAny(arg, "--consume", "/consume"))
{
options.Collect = false;
@@ -214,6 +247,18 @@ namespace BizTalkCheckmkPulse
/// </summary>
private static void Validate(MonitoringOptions options)
{
if (options.ForceEndpointCatalogRefresh && !options.Collect)
{
throw new ConfigurationErrorsException("ForceEndpointCatalogRefresh is valid only in provider mode.");
}
if (options.RuntimeValidation
&& (!options.RuntimeValidationNotBeforeUtc.HasValue
|| string.IsNullOrWhiteSpace(options.ExpectedRuntimeIdentity)))
{
throw new ConfigurationErrorsException("Runtime validation requires a start timestamp and expected identity.");
}
if (string.IsNullOrWhiteSpace(options.SnapshotPath) || !Path.IsPathRooted(options.SnapshotPath))
{
throw new ConfigurationErrorsException("SnapshotPath must be an absolute path.");
+37 -6
View File
@@ -37,6 +37,11 @@ namespace BizTalkCheckmkPulse
return 0;
}
if (options.RuntimeValidation)
{
return RunRuntimeValidation(options, formatter);
}
return options.Collect
? RunProvider(options, formatter)
: RunConsumer(options, formatter);
@@ -51,7 +56,14 @@ namespace BizTalkCheckmkPulse
Console.WriteLine(line);
}
return options != null && options.Collect ? 1 : 0;
var strictFailureMode = options != null
? options.Collect || options.RuntimeValidation
: (args ?? new string[0]).Any(x =>
string.Equals(x, "--collect", StringComparison.OrdinalIgnoreCase)
|| string.Equals(x, "/collect", StringComparison.OrdinalIgnoreCase)
|| string.Equals(x, "--validate-runtime", StringComparison.OrdinalIgnoreCase)
|| string.Equals(x, "/validate-runtime", StringComparison.OrdinalIgnoreCase));
return strictFailureMode ? 1 : 0;
}
}
@@ -99,13 +111,9 @@ namespace BizTalkCheckmkPulse
+ " send_ports_not_started="
+ result.SendPorts.Count(x => x.Status != 3)
+ " endpoint_candidates_active="
+ result.EndpointConnectivity.ActiveCandidates
+ result.EndpointCandidates.Count(x => x.Active)
+ " endpoints_active="
+ result.EndpointConnectivity.Active
+ " endpoints_unsupported="
+ result.EndpointConnectivity.UnsupportedActive
+ " endpoints_excluded="
+ result.EndpointConnectivity.ExcludedActive
+ " endpoints_failed="
+ result.EndpointConnectivity.Results.Count(x => !x.Available)
+ " endpoints_unresolved="
@@ -137,6 +145,29 @@ namespace BizTalkCheckmkPulse
}
}
private static int RunRuntimeValidation(MonitoringOptions options, CheckmkLocalFormatter formatter)
{
var validation = RuntimeValidator.Validate(
options,
formatter,
options.RuntimeValidationNotBeforeUtc.Value,
options.ExpectedRuntimeIdentity,
DateTime.UtcNow);
if (!validation.IsSuccess)
{
Console.Error.WriteLine("Runtime validation failed: " + validation.Error);
return 1;
}
Console.WriteLine(
"RUNTIME_VALIDATION_V1 generatedUtc=" + validation.GeneratedUtc.ToString("o")
+ " catalogUtc=" + (validation.CatalogSynchronizedUtc.HasValue
? validation.CatalogSynchronizedUtc.Value.ToString("o")
: "disabled")
+ " stableServices=" + validation.StableServiceCount);
return 0;
}
private static void EnsureProviderIdentity()
{
using (var identity = WindowsIdentity.GetCurrent())
@@ -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.3.0")]
[assembly: AssemblyFileVersion("2.2.3.0")]
[assembly: AssemblyVersion("2.2.4.0")]
[assembly: AssemblyFileVersion("2.2.4.0")]
+179
View File
@@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace BizTalkCheckmkPulse
{
/// <summary>
/// Validiert die vom ersten Providerlauf erzeugten Runtime-Artefakte mit denselben
/// Parsern und Grenzen, die auch der laufende Consumer verwendet.
/// </summary>
internal static class RuntimeValidator
{
public static RuntimeValidationResult Validate(
MonitoringOptions options,
CheckmkLocalFormatter formatter,
DateTime notBeforeUtc,
string expectedIdentity,
DateTime utcNow)
{
if (options == null) throw new ArgumentNullException("options");
if (formatter == null) throw new ArgumentNullException("formatter");
var snapshot = new SnapshotStore(options.SnapshotPath, options.SnapshotMaxBytes)
.Read(utcNow, TimeSpan.FromSeconds(options.SnapshotMaxAgeSeconds));
if (!snapshot.IsSuccess)
{
return RuntimeValidationResult.Failed("Snapshot ist ungueltig: " + snapshot.Error);
}
var boundary = notBeforeUtc.ToUniversalTime();
if (snapshot.GeneratedUtc < boundary)
{
return RuntimeValidationResult.Failed(
"Snapshot stammt nicht aus dem gestarteten Providerlauf. generatedUtc="
+ snapshot.GeneratedUtc.ToString("o") + ", requiredUtc=" + boundary.ToString("o") + ".");
}
if (!string.Equals(snapshot.Identity, expectedIdentity, StringComparison.OrdinalIgnoreCase))
{
return RuntimeValidationResult.Failed(
"Snapshot wurde von einer unerwarteten Identitaet erzeugt. expected="
+ expectedIdentity + ", actual=" + (snapshot.Identity ?? "(leer)") + ".");
}
string serviceError;
var expectedServices = formatter.FormatSelfTest().Select(ExtractServiceName).ToArray();
if (!ContainsExactStableContract(snapshot.Lines, expectedServices, out serviceError))
{
return RuntimeValidationResult.Failed(serviceError);
}
DateTime? catalogUtc = null;
if (options.ProbeEndpointConnectivity)
{
EndpointCatalog catalog;
try
{
catalog = new EndpointCatalogStore(
options.EndpointCatalogPath,
options.EndpointCatalogMaxBytes,
options.EndpointCatalogMaxEntries)
.Read(options.EnvironmentName);
}
catch (Exception ex)
{
return RuntimeValidationResult.Failed(
"Endpoint-Katalog ist ungueltig: " + ex.GetType().Name + ": " + ex.Message);
}
if (catalog.SynchronizedUtc < boundary)
{
return RuntimeValidationResult.Failed(
"Endpoint-Katalog wurde beim Installationslauf nicht aktualisiert. synchronizedUtc="
+ catalog.SynchronizedUtc.ToString("o") + ", requiredUtc=" + boundary.ToString("o") + ".");
}
if (catalog.SynchronizedUtc > utcNow.ToUniversalTime().AddMinutes(5))
{
return RuntimeValidationResult.Failed("Endpoint-Katalog-Zeitstempel liegt unplausibel in der Zukunft.");
}
catalogUtc = catalog.SynchronizedUtc;
}
return RuntimeValidationResult.Success(
snapshot.GeneratedUtc,
catalogUtc,
expectedServices.Length);
}
internal static bool ContainsExactStableContract(
IEnumerable<string> lines,
IEnumerable<string> expectedServices,
out string error)
{
string[] actual;
try
{
actual = (lines ?? Enumerable.Empty<string>()).Select(ExtractServiceName).ToArray();
}
catch (Exception ex)
{
error = "Snapshot-Servicevertrag kann nicht gelesen werden: " + ex.Message;
return false;
}
if (actual.Distinct(StringComparer.Ordinal).Count() != actual.Length)
{
error = "Snapshot enthaelt doppelte Checkmk-Servicenamen.";
return false;
}
var actualSet = new HashSet<string>(actual, StringComparer.Ordinal);
var expected = (expectedServices ?? Enumerable.Empty<string>()).ToArray();
var missing = expected
.Where(x => !actualSet.Contains(x))
.OrderBy(x => x, StringComparer.Ordinal)
.ToArray();
if (missing.Length != 0)
{
error = "Snapshot enthaelt nicht alle stabilen Checkmk-Services. Fehlend=["
+ string.Join(", ", missing) + "].";
return false;
}
var unknown = (lines ?? Enumerable.Empty<string>())
.Where(x => x != null && x.StartsWith("3 ", StringComparison.Ordinal))
.Select(ExtractServiceName)
.Where(x => expected.Contains(x, StringComparer.Ordinal))
.OrderBy(x => x, StringComparer.Ordinal)
.ToArray();
if (unknown.Length != 0)
{
error = "Snapshot enthaelt unzuverlaessige UNKNOWN-Zustaende in stabilen Services. UNKNOWN=["
+ string.Join(", ", unknown) + "].";
return false;
}
error = string.Empty;
return true;
}
private static string ExtractServiceName(string line)
{
var firstQuote = (line ?? string.Empty).IndexOf('"');
var secondQuote = firstQuote < 0 ? -1 : line.IndexOf('"', firstQuote + 1);
if (firstQuote < 0 || secondQuote <= firstQuote + 1)
throw new InvalidOperationException("Ungueltige Local-Check-Zeile.");
return line.Substring(firstQuote + 1, secondQuote - firstQuote - 1);
}
}
internal sealed class RuntimeValidationResult
{
private RuntimeValidationResult() { }
public bool IsSuccess { get; private set; }
public string Error { get; private set; }
public DateTime GeneratedUtc { get; private set; }
public DateTime? CatalogSynchronizedUtc { get; private set; }
public int StableServiceCount { get; private set; }
public static RuntimeValidationResult Success(DateTime generatedUtc, DateTime? catalogUtc, int serviceCount)
{
return new RuntimeValidationResult
{
IsSuccess = true,
GeneratedUtc = generatedUtc,
CatalogSynchronizedUtc = catalogUtc,
StableServiceCount = serviceCount
};
}
public static RuntimeValidationResult Failed(string error)
{
return new RuntimeValidationResult { Error = error };
}
}
}
+6 -4
View File
@@ -114,7 +114,7 @@ namespace BizTalkCheckmkPulse
}
var expectedMachine = DecodeHeader(headerLines[2], "machineBase64=");
DecodeHeader(headerLines[3], "identityBase64=");
var identity = DecodeHeader(headerLines[3], "identityBase64=");
var expectedHash = ReadHeaderValue(headerLines[5], "payloadSha256=");
if (!string.Equals(expectedMachine, Environment.MachineName, StringComparison.OrdinalIgnoreCase))
{
@@ -159,7 +159,7 @@ namespace BizTalkCheckmkPulse
+ "s.");
}
return SnapshotReadResult.Success(lines, generatedUtc);
return SnapshotReadResult.Success(lines, generatedUtc, identity);
}
catch (FileNotFoundException)
{
@@ -330,14 +330,16 @@ namespace BizTalkCheckmkPulse
public string Error { get; private set; }
public IReadOnlyList<string> Lines { get; private set; }
public DateTime GeneratedUtc { get; private set; }
public string Identity { get; private set; }
public static SnapshotReadResult Success(IReadOnlyList<string> lines, DateTime generatedUtc)
public static SnapshotReadResult Success(IReadOnlyList<string> lines, DateTime generatedUtc, string identity)
{
return new SnapshotReadResult
{
IsSuccess = true,
Lines = lines,
GeneratedUtc = generatedUtc
GeneratedUtc = generatedUtc,
Identity = identity
};
}
+267 -6
View File
@@ -37,14 +37,22 @@ namespace BizTalkCheckmkPulse.Tests
Run("EndpointOutputListsOnlyUnavailableTargets", EndpointOutputListsOnlyUnavailableTargets);
Run("EndpointOutputExplainsOnlyRealResolutionGaps", EndpointOutputExplainsOnlyRealResolutionGaps);
Run("EndpointProbeBudgetFitsMinuteInterval", EndpointProbeBudgetFitsMinuteInterval);
<<<<<<< HEAD
=======
Run("ForcedEndpointRefreshRewritesFreshCatalog", ForcedEndpointRefreshRewritesFreshCatalog);
Run("EnvironmentLabelDoesNotRenameServicesByDefault", EnvironmentLabelDoesNotRenameServicesByDefault);
Run("DefaultServiceContractIsExact", DefaultServiceContractIsExact);
Run("InstallerRejectsUnconfirmedServiceRename", InstallerRejectsUnconfirmedServiceRename);
Run("InstallerAllowsConfirmedServiceRename", InstallerAllowsConfirmedServiceRename);
>>>>>>> e694e4c (Guard Checkmk service renames during updates)
Run("InstallerUpdatePreservesExistingSettings", InstallerUpdatePreservesExistingSettings);
Run("RuntimeValidationAcceptsFreshConsistentArtifacts", RuntimeValidationAcceptsFreshConsistentArtifacts);
Run("RuntimeValidationRejectsPreInstallSnapshot", RuntimeValidationRejectsPreInstallSnapshot);
Run("RuntimeValidationRejectsWrongIdentity", RuntimeValidationRejectsWrongIdentity);
Run("RuntimeValidationRejectsStaleCatalog", RuntimeValidationRejectsStaleCatalog);
Run("RuntimeValidationRejectsChangedServiceContract", RuntimeValidationRejectsChangedServiceContract);
Run("RuntimeValidationRejectsUnknownStableService", RuntimeValidationRejectsUnknownStableService);
Run("RuntimeValidationAllowsDisabledEndpointProbeWithoutCatalog", RuntimeValidationAllowsDisabledEndpointProbeWithoutCatalog);
Run("ForceEndpointRefreshRequiresProviderMode", ForceEndpointRefreshRequiresProviderMode);
Run("RuntimeValidationArgumentsAreParsedStrictly", RuntimeValidationArgumentsAreParsedStrictly);
Run("TaskCompletionRequiresFreshSuccessfulRun", TaskCompletionRequiresFreshSuccessfulRun);
Run("SnapshotRoundTripPreservesLines", SnapshotRoundTripPreservesLines);
Run("SnapshotRejectsTampering", SnapshotRejectsTampering);
Run("SnapshotRejectsStaleData", SnapshotRejectsStaleData);
@@ -337,6 +345,7 @@ namespace BizTalkCheckmkPulse.Tests
Assert(result.IsSuccess, "snapshot should be readable: " + result.Error);
AssertEqual(lines.Length, result.Lines.Count, "snapshot line count");
Assert(lines.SequenceEqual(result.Lines), "snapshot payload changed");
AssertEqual("DOMAIN\\collector$", result.Identity, "snapshot identity");
var replacement = lines.Select(x => x.Replace("Self test OK", "Replacement OK")).ToArray();
store.Write(replacement, generated.AddSeconds(30), "DOMAIN\\collector$");
@@ -690,8 +699,51 @@ namespace BizTalkCheckmkPulse.Tests
Assert(configuredMaximum <= 30000, "configured endpoint worst-case must leave headroom in the minute interval");
}
<<<<<<< HEAD
=======
private static void ForcedEndpointRefreshRewritesFreshCatalog()
{
var directory = Path.Combine(Path.GetTempPath(), "BizTalkCheckmkPulse.ForcedCatalogTests." + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(directory);
try
{
var options = new MonitoringOptions
{
EnvironmentName = "ACC",
EndpointCatalogPath = Path.Combine(directory, "endpoints.xml"),
LogDirectory = Path.Combine(directory, "logs"),
ForceEndpointCatalogRefresh = true
};
var oldTimestamp = DateTime.UtcNow.AddHours(-1);
new EndpointCatalogStore(
options.EndpointCatalogPath,
options.EndpointCatalogMaxBytes,
options.EndpointCatalogMaxEntries)
.Write(new EndpointCatalog
{
EnvironmentName = options.EnvironmentName,
SynchronizedUtc = oldTimestamp
});
var probeResult = new ProbeResult();
probeResult.Platform.ReceiveLocationsDataAvailable = true;
probeResult.Platform.SendPortsDataAvailable = true;
new EndpointConnectivityProbe(options, new FileLogger(options.LogDirectory, 1, "test"))
.Query(probeResult);
var refreshed = new EndpointCatalogStore(
options.EndpointCatalogPath,
options.EndpointCatalogMaxBytes,
options.EndpointCatalogMaxEntries)
.Read(options.EnvironmentName);
Assert(probeResult.EndpointConnectivity.RefreshRequired, "forced catalog refresh was not requested");
Assert(probeResult.EndpointConnectivity.RefreshSucceeded, "forced catalog refresh failed");
Assert(refreshed.SynchronizedUtc > oldTimestamp, "forced catalog refresh did not replace timestamp");
}
finally
{
if (Directory.Exists(directory)) Directory.Delete(directory, true);
}
}
private static void EnvironmentLabelDoesNotRenameServicesByDefault()
{
var options = new MonitoringOptions { EnvironmentName = "ACC" };
@@ -765,7 +817,6 @@ namespace BizTalkCheckmkPulse.Tests
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"));
@@ -779,6 +830,7 @@ namespace BizTalkCheckmkPulse.Tests
source,
"<configuration><appSettings>"
+ "<add key=\"EnvironmentName\" value=\"\" />"
+ "<add key=\"IncludeEnvironmentInServiceName\" value=\"false\" />"
+ "<add key=\"EndpointProbeMaxConcurrency\" value=\"16\" />"
+ "<add key=\"EndpointMaxCount\" value=\"100\" />"
+ "<add key=\"EndpointCatalogMaxEntries\" value=\"1000\" />"
@@ -802,6 +854,7 @@ namespace BizTalkCheckmkPulse.Tests
string.Empty);
var merged = File.ReadAllText(staged);
AssertEqual("ACC", effectiveEnvironment, "preserved installer environment");
Assert(merged.Contains("key=\"IncludeEnvironmentInServiceName\" value=\"false\""), "stable service-name default must be added on update");
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=\"EndpointCatalogMaxEntries\" value=\"1000\""), "new catalog entry limit must be added");
@@ -821,6 +874,214 @@ namespace BizTalkCheckmkPulse.Tests
}
}
private static void RuntimeValidationAcceptsFreshConsistentArtifacts()
{
var boundary = DateTime.UtcNow.AddMinutes(-1);
var result = ValidateRuntimeArtifacts(
boundary,
boundary.AddSeconds(10),
boundary.AddSeconds(5),
"BEW\\t231bizmon",
"BEW\\t231bizmon",
true,
null);
Assert(result.IsSuccess, "fresh consistent runtime artifacts must pass: " + result.Error);
AssertEqual(9, result.StableServiceCount, "validated stable service count");
}
private static void RuntimeValidationRejectsPreInstallSnapshot()
{
var boundary = DateTime.UtcNow.AddMinutes(-1);
var result = ValidateRuntimeArtifacts(
boundary,
boundary.AddSeconds(-1),
boundary.AddSeconds(5),
"BEW\\t231bizmon",
"BEW\\t231bizmon",
true,
null);
Assert(!result.IsSuccess, "snapshot from before installation must fail");
Assert(result.Error.Contains("nicht aus dem gestarteten Providerlauf"), "old snapshot reason missing");
}
private static void RuntimeValidationRejectsWrongIdentity()
{
var boundary = DateTime.UtcNow.AddMinutes(-1);
var result = ValidateRuntimeArtifacts(
boundary,
boundary.AddSeconds(10),
boundary.AddSeconds(5),
"BEW\\other-account",
"BEW\\t231bizmon",
true,
null);
Assert(!result.IsSuccess, "unexpected provider identity must fail");
Assert(result.Error.Contains("unerwarteten Identitaet"), "identity mismatch reason missing");
}
private static void RuntimeValidationRejectsStaleCatalog()
{
var boundary = DateTime.UtcNow.AddMinutes(-1);
var result = ValidateRuntimeArtifacts(
boundary,
boundary.AddSeconds(10),
boundary.AddSeconds(-1),
"BEW\\t231bizmon",
"BEW\\t231bizmon",
true,
null);
Assert(!result.IsSuccess, "catalog from before installation must fail");
Assert(result.Error.Contains("nicht aktualisiert"), "old catalog reason missing");
}
private static void RuntimeValidationRejectsChangedServiceContract()
{
var boundary = DateTime.UtcNow.AddMinutes(-1);
var lines = new CheckmkLocalFormatter(new MonitoringOptions()).FormatSelfTest().ToArray();
lines[0] = lines[0].Replace("BizTalk Platform", "BizTalk Renamed Platform");
var result = ValidateRuntimeArtifacts(
boundary,
boundary.AddSeconds(10),
boundary.AddSeconds(5),
"BEW\\t231bizmon",
"BEW\\t231bizmon",
true,
lines);
Assert(!result.IsSuccess, "changed runtime service contract must fail");
Assert(result.Error.Contains("Fehlend=[BizTalk Platform]"), "missing stable service reason missing");
}
private static void RuntimeValidationAllowsDisabledEndpointProbeWithoutCatalog()
{
var boundary = DateTime.UtcNow.AddMinutes(-1);
var result = ValidateRuntimeArtifacts(
boundary,
boundary.AddSeconds(10),
null,
"BEW\\t231bizmon",
"BEW\\t231bizmon",
false,
null);
Assert(result.IsSuccess, "disabled endpoint probe must not require catalog: " + result.Error);
Assert(!result.CatalogSynchronizedUtc.HasValue, "disabled endpoint probe must report no catalog timestamp");
}
private static void RuntimeValidationRejectsUnknownStableService()
{
var boundary = DateTime.UtcNow.AddMinutes(-1);
var lines = new CheckmkLocalFormatter(new MonitoringOptions()).FormatSelfTest().ToArray();
lines[0] = "3" + lines[0].Substring(1);
var result = ValidateRuntimeArtifacts(
boundary,
boundary.AddSeconds(10),
boundary.AddSeconds(5),
"BEW\\t231bizmon",
"BEW\\t231bizmon",
true,
lines);
Assert(!result.IsSuccess, "UNKNOWN stable service must fail runtime acceptance");
Assert(result.Error.Contains("UNKNOWN=[BizTalk Platform]"), "UNKNOWN stable service reason missing");
}
private static void ForceEndpointRefreshRequiresProviderMode()
{
var options = MonitoringOptions.Load(new[] { "--collect", "--force-endpoint-refresh" });
Assert(options.Collect, "forced refresh must retain provider mode");
Assert(options.ForceEndpointCatalogRefresh, "forced refresh option missing");
try
{
MonitoringOptions.Load(new[] { "--force-endpoint-refresh" });
throw new InvalidOperationException("forced refresh without provider mode was accepted");
}
catch (Exception ex)
{
Assert(ex.Message.Contains("valid only in provider mode"), "forced refresh rejection reason missing");
}
}
private static void RuntimeValidationArgumentsAreParsedStrictly()
{
var timestamp = DateTime.UtcNow;
var identity = "BEW\\t231bizmon";
var options = MonitoringOptions.Load(new[]
{
"--validate-runtime",
"--validation-not-before-utc",
timestamp.ToString("o"),
"--expected-identity-base64",
Convert.ToBase64String(Encoding.UTF8.GetBytes(identity))
});
Assert(options.RuntimeValidation, "runtime validation mode missing");
AssertEqual(timestamp.ToString("o"), options.RuntimeValidationNotBeforeUtc.Value.ToString("o"), "runtime validation timestamp");
AssertEqual(identity, options.ExpectedRuntimeIdentity, "runtime validation identity");
}
private static void TaskCompletionRequiresFreshSuccessfulRun()
{
var boundary = DateTime.UtcNow;
Assert(BizTalkCheckmkPulse.Setup.TaskSchedulerService.IsSuccessfulCompletion(
3, 0, boundary.AddSeconds(1), boundary), "fresh successful ready task must pass");
Assert(!BizTalkCheckmkPulse.Setup.TaskSchedulerService.IsSuccessfulCompletion(
4, 0, boundary.AddSeconds(1), boundary), "running task must not pass");
Assert(!BizTalkCheckmkPulse.Setup.TaskSchedulerService.IsSuccessfulCompletion(
3, 1, boundary.AddSeconds(1), boundary), "failed task result must not pass");
Assert(!BizTalkCheckmkPulse.Setup.TaskSchedulerService.IsSuccessfulCompletion(
3, 0, boundary.AddSeconds(-3), boundary), "old successful task must not pass");
}
private static RuntimeValidationResult ValidateRuntimeArtifacts(
DateTime boundaryUtc,
DateTime snapshotUtc,
DateTime? catalogUtc,
string snapshotIdentity,
string expectedIdentity,
bool probeEndpoints,
string[] snapshotLines)
{
var directory = Path.Combine(Path.GetTempPath(), "BizTalkCheckmkPulse.RuntimeValidationTests." + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(directory);
try
{
var options = new MonitoringOptions
{
EnvironmentName = "ACC",
SnapshotPath = Path.Combine(directory, "snapshot.txt"),
EndpointCatalogPath = Path.Combine(directory, "endpoints.xml"),
ProbeEndpointConnectivity = probeEndpoints
};
var formatter = new CheckmkLocalFormatter(options);
new SnapshotStore(options.SnapshotPath, options.SnapshotMaxBytes).Write(
snapshotLines ?? formatter.FormatSelfTest().ToArray(),
snapshotUtc,
snapshotIdentity);
if (catalogUtc.HasValue)
{
new EndpointCatalogStore(
options.EndpointCatalogPath,
options.EndpointCatalogMaxBytes,
options.EndpointCatalogMaxEntries)
.Write(new EndpointCatalog
{
EnvironmentName = options.EnvironmentName,
SynchronizedUtc = catalogUtc.Value
});
}
return RuntimeValidator.Validate(
options,
formatter,
boundaryUtc,
expectedIdentity,
boundaryUtc.AddMinutes(1));
}
finally
{
if (Directory.Exists(directory)) Directory.Delete(directory, true);
}
}
private static void SnapshotRejectsTampering()
{
WithTemporarySnapshot((path, store) =>