Make installer runtime acceptance transactional
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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:
|
||||
|
||||
Binary file not shown.
@@ -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:
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 +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")]
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
Reference in New Issue
Block a user