Compare commits
6
Commits
26e6ea5aac
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f8e8034e6 | ||
|
|
951a5e72d1 | ||
|
|
cc1e59916a | ||
|
|
448be8dc49 | ||
|
|
8fe1b57173 | ||
|
|
d408419c5f |
@@ -408,6 +408,54 @@ Task gestoppt und die Programmversion umgeschaltet. Scheitert ein Folgeschritt,
|
|||||||
versucht der Installer Programmverzeichnis, Checkmk-Wrapper und Scheduled Task
|
versucht der Installer Programmverzeichnis, Checkmk-Wrapper und Scheduled Task
|
||||||
auf den vorherigen Stand zurückzusetzen.
|
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 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.
|
||||||
|
|
||||||
|
Version 2.2.5 korrigiert die versionsübergreifende Vorprüfung. Die neue
|
||||||
|
Paket-, Staging- und Zielversion muss weiterhin exakt neun gültige
|
||||||
|
Self-Test-Zeilen liefern. Für die installierte Ausgangsversion wird dagegen
|
||||||
|
deren vollständiger, eindeutiger und ausschließlich grüner Servicevertrag
|
||||||
|
akzeptiert, auch wenn er aus einer älteren Version mit acht Services stammt.
|
||||||
|
Rein additive Services blockieren das Update nicht; entfernte Namen und damit
|
||||||
|
auch Renames bleiben vor der Umschaltung bestätigungspflichtig.
|
||||||
|
|
||||||
|
Parallel erzeugt jeder Installations-/Updatelauf ein separates
|
||||||
|
`%ProgramData%\BizTalkCheckmkPulse\logs\setup-*.log`. Protokolliert werden
|
||||||
|
Phase, Setup-/Binary-Version, Pfad und Größe, Self-Test-stdout/-stderr,
|
||||||
|
Exitcodes, Taskstatus, Exception-Kette, letzte Provider-Logzeilen bei einem
|
||||||
|
Abnahmefehler und das Rollback-Ergebnis. Das Collector-Kennwort wird der
|
||||||
|
Logging-Komponente nie übergeben. Ein frisch abgeschlossener Task mit
|
||||||
|
Fehlercode wird sofort erkannt; der Fehler erscheint dezimal und hexadezimal.
|
||||||
|
|
||||||
|
Version 2.2.6 behandelt einen fachlichen UNKNOWN-Zustand der Endpoint-Prüfung
|
||||||
|
nicht mehr als Installationsdefekt. Sind Providerlauf, Snapshot, Identität,
|
||||||
|
neun Services und der erzwungen aktualisierte Katalog technisch valide, darf
|
||||||
|
`BizTalk Endpoint Reachability` als einziger Service `UNKNOWN` bleiben. Die
|
||||||
|
vollständige Local-Check-Zeile wird als Abnahmewarnung ausgegeben und bleibt
|
||||||
|
unverändert für Checkmk erhalten. Damit wird eine unvollständige
|
||||||
|
Endpoint-Auflösung nicht grüngefärbt und zugleich kein funktionsfähiges Update
|
||||||
|
zurückgerollt. UNKNOWN in Platform, SQL Access, Suspensions, Host Instances,
|
||||||
|
Receive Locations, Send Ports, Orchestrations oder Event Log bleibt
|
||||||
|
blockierend, weil dort die Kern-Datenerhebung nicht zuverlässig belegt ist.
|
||||||
|
|
||||||
|
Version 2.2.7 klassifiziert die im RTM-Abnahmelauf identifizierten Adapter
|
||||||
|
präziser. Scheduling-Komponenten sind keine Netzwerkziele und werden komplett
|
||||||
|
ausgenommen. `Database://host[:port]/.../Oracle` wird per TCP auf den
|
||||||
|
expliziten Port oder den Oracle-Standard 1521 geprüft. WCF-SAP verwendet den
|
||||||
|
ersten SAProuter-Hop oder das Paar `ListenerGwHost`/`ListenerGwServ`; dabei
|
||||||
|
entspricht `sapgwNN` dem TCP-Port `33NN`. Diese beiden Adapterklassen laufen
|
||||||
|
als Best-Effort-Diagnose: Ein Fehlschlag bleibt über Metriken sichtbar, erzeugt
|
||||||
|
aber keinen Alarm. Alle regulären und manuellen Ziele behalten ihre bisherige
|
||||||
|
strenge Statuslogik.
|
||||||
|
|
||||||
Der installierte Task selbst läuft mit `RunLevel Limited`. Der Installer
|
Der installierte Task selbst läuft mit `RunLevel Limited`. Der Installer
|
||||||
übergibt das Kennwort direkt an die Windows-Task-Scheduler-COM-Schnittstelle;
|
übergibt das Kennwort direkt an die Windows-Task-Scheduler-COM-Schnittstelle;
|
||||||
es wird weder in einer Prozesskommandozeile noch in Config oder Log
|
es wird weder in einer Prozesskommandozeile noch in Config oder Log
|
||||||
@@ -550,6 +598,8 @@ BizTalk-Probes:
|
|||||||
| Key | Default |
|
| Key | Default |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `Server` | `.` |
|
| `Server` | `.` |
|
||||||
|
| `EnvironmentName` | leer |
|
||||||
|
| `IncludeEnvironmentInServiceName` | `false` |
|
||||||
| `QueryTimeoutSeconds` | `25` |
|
| `QueryTimeoutSeconds` | `25` |
|
||||||
| `ProbeSqlConnectivity` | `true` |
|
| `ProbeSqlConnectivity` | `true` |
|
||||||
| `SqlConnectionTimeoutSeconds` | `5` |
|
| `SqlConnectionTimeoutSeconds` | `5` |
|
||||||
@@ -602,6 +652,12 @@ Automatisiert:
|
|||||||
einem theoretischen Socket-Budget von etwa 15 Sekunden; das konfigurierte
|
einem theoretischen Socket-Budget von etwa 15 Sekunden; das konfigurierte
|
||||||
Maximum von 100 Zielen in etwa 21 Sekunden,
|
Maximum von 100 Zielen in etwa 21 Sekunden,
|
||||||
- Update-Config-Merge erhält bestehende Werte und fügt neue Defaults hinzu,
|
- 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, Kernservice-`UNKNOWN`, alten/ungültigen Katalog und
|
||||||
|
fehlerhaften oder nicht rechtzeitig beendeten Tasklauf zurück; ein isoliertes
|
||||||
|
Endpoint-Reachability-`UNKNOWN` wird mit vollständigem Detail akzeptiert,
|
||||||
- Snapshot-Roundtrip und Ersatz,
|
- Snapshot-Roundtrip und Ersatz,
|
||||||
- SHA-256-Manipulation wird verworfen,
|
- SHA-256-Manipulation wird verworfen,
|
||||||
- Stale-Snapshot wird verworfen,
|
- Stale-Snapshot wird verworfen,
|
||||||
|
|||||||
@@ -26,6 +26,15 @@ Checkmk-Service `BizTalk Endpoint Reachability` zusammengefasst:
|
|||||||
- HTTP(S), SFTP, FTP, WCF/`net.tcp` und UNC/SMB werden durch einen reinen
|
- HTTP(S), SFTP, FTP, WCF/`net.tcp` und UNC/SMB werden durch einen reinen
|
||||||
TCP-Verbindungsaufbau zu Host und Port geprüft. Explizite
|
TCP-Verbindungsaufbau zu Host und Port geprüft. Explizite
|
||||||
`udp://host:port`-Ziele verwenden ein UDP-Datagramm.
|
`udp://host:port`-Ziele verwenden ein UDP-Datagramm.
|
||||||
|
- Scheduling-Komponenten werden vollständig aus der Endpoint-Prüfung
|
||||||
|
ausgeschlossen. Oracle-`DATABASE`-Ziele werden aus
|
||||||
|
`Database://host[:port]/.../Oracle` auf TCP 1521 beziehungsweise den
|
||||||
|
expliziten Port reduziert. WCF-SAP nutzt den ersten SAProuter-Hop oder
|
||||||
|
`ListenerGwHost`/`ListenerGwServ`; `sapgwNN` wird als TCP `33NN` aufgelöst.
|
||||||
|
- Automatisch abgeleitete DATABASE-/WCF-SAP-Probes sind Best-Effort:
|
||||||
|
erfolgreiche Tests bleiben messbar, nicht ableitbare oder nicht erreichbare
|
||||||
|
Ziele erzeugen jedoch weder `UNKNOWN` noch `WARN`/`CRIT`. Für verbindliche
|
||||||
|
Alarmierung kann weiterhin ein manueller Katalogeintrag verwendet werden.
|
||||||
- Es werden weder HTTP-Aufrufe noch Anmeldungen, fachliche Requests oder
|
- Es werden weder HTTP-Aufrufe noch Anmeldungen, fachliche Requests oder
|
||||||
BizTalk-Nachrichten gesendet. Der Check validiert DNS, Netzwerkpfad,
|
BizTalk-Nachrichten gesendet. Der Check validiert DNS, Netzwerkpfad,
|
||||||
Firewall und bei TCP einen annehmenden Zielport, nicht die fachliche
|
Firewall und bei TCP einen annehmenden Zielport, nicht die fachliche
|
||||||
@@ -84,6 +93,10 @@ Der Datenaustausch ist bewusst defensiv:
|
|||||||
`UNKNOWN`-Snapshot und einen ungleich null lautenden Task-Exitcode.
|
`UNKNOWN`-Snapshot und einen ungleich null lautenden Task-Exitcode.
|
||||||
- Provider und Consumer protokollieren in tägliche Dateien; die Aufbewahrung
|
- Provider und Consumer protokollieren in tägliche Dateien; die Aufbewahrung
|
||||||
ist standardmäßig 30 Tage.
|
ist standardmäßig 30 Tage.
|
||||||
|
- Jeder Installations-/Updatelauf schreibt zusätzlich ein eigenes
|
||||||
|
`setup-*.log` mit Phase, Binary-Version, Self-Test-Ausgabe,
|
||||||
|
Task-Scheduler-Status, Exception-Kette und Rollback-Ergebnis. Kennwörter
|
||||||
|
werden nicht an das Logging übergeben.
|
||||||
- Snapshotgröße, WMI-/SQL-Timeouts, Log-Retention und Stale-Grenze sind
|
- Snapshotgröße, WMI-/SQL-Timeouts, Log-Retention und Stale-Grenze sind
|
||||||
begrenzt und konfigurierbar.
|
begrenzt und konfigurierbar.
|
||||||
|
|
||||||
@@ -124,11 +137,13 @@ Auch schemafreie `host:port`-Angaben sowie FTP-/SFTP-Adressen der Form
|
|||||||
nicht in den Endpoint-Katalog übernommen.
|
nicht in den Endpoint-Katalog übernommen.
|
||||||
|
|
||||||
Nicht jede aktive BizTalk-Adresse ist selbst ein Socket-Ziel. Dynamische Send
|
Nicht jede aktive BizTalk-Adresse ist selbst ein Socket-Ziel. Dynamische Send
|
||||||
Ports, lokale Pfade, SMTP-Empfänger, Named Pipes und relative HTTP-/WCF-
|
Ports, lokale Pfade, SMTP-Empfänger, Named Pipes, relative HTTP-/WCF-
|
||||||
Receive-Listener werden deshalb als erwartbar nicht socket-prüfbar behandelt
|
Receive-Listener, Scheduling-Komponenten sowie nicht sicher ableitbare
|
||||||
und erzeugen kein `UNKNOWN`. Nur eine aktive statische Adresse, die tatsächlich
|
DATABASE-/WCF-SAP-Adressen werden deshalb als erwartbar nicht socket-prüfbar
|
||||||
ein Netzwerkziel enthalten müsste und nicht sicher aufgelöst werden kann,
|
behandelt und erzeugen kein `UNKNOWN`. Nur eine andere aktive statische
|
||||||
bleibt `UNKNOWN` und wird geheimnisfrei mit Artefakt, Adapter und Grund genannt.
|
Adresse, die tatsächlich ein Netzwerkziel enthalten müsste und nicht sicher
|
||||||
|
aufgelöst werden kann, bleibt `UNKNOWN` und wird geheimnisfrei mit Artefakt,
|
||||||
|
Adapter und Grund genannt.
|
||||||
|
|
||||||
Der geheimnisfreie Katalog liegt unter
|
Der geheimnisfreie Katalog liegt unter
|
||||||
`%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml`. Fehlt er, wird er beim
|
`%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml`. Fehlt er, wird er beim
|
||||||
@@ -141,8 +156,11 @@ Pro Service werden standardmäßig maximal fünf betroffene Namen gezeigt.
|
|||||||
Weitere Treffer erscheinen nur als `(+n more)`; Details sind zusätzlich auf
|
Weitere Treffer erscheinen nur als `(+n more)`; Details sind zusätzlich auf
|
||||||
1600 Zeichen begrenzt. Metriken bleiben trotzdem vollständig.
|
1600 Zeichen begrenzt. Metriken bleiben trotzdem vollständig.
|
||||||
|
|
||||||
Mit `EnvironmentName=ACC`, `DEV`, `TST` oder `PRD` wird die Umgebung in den
|
`EnvironmentName=ACC`, `DEV`, `TST` oder `PRD` kennzeichnet Snapshot und
|
||||||
Servicenamen aufgenommen, zum Beispiel `BizTalk ACC Platform`.
|
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)
|
Statuslogik und Metriken: [docs/CheckmkServices.md](docs/CheckmkServices.md)
|
||||||
|
|
||||||
@@ -151,6 +169,10 @@ Beispielausgaben: [docs/ExampleOutput.md](docs/ExampleOutput.md)
|
|||||||
Endpoint-Katalog, manuelle Overrides und Protokollgrenzen:
|
Endpoint-Katalog, manuelle Overrides und Protokollgrenzen:
|
||||||
[docs/EndpointCatalog.md](docs/EndpointCatalog.md)
|
[docs/EndpointCatalog.md](docs/EndpointCatalog.md)
|
||||||
|
|
||||||
|
Die RTM-Adapterregeln und ihre Herstellerquellen sind in
|
||||||
|
[docs/RTM-Endpoint-Adapterregeln-2026-08-11.md](docs/RTM-Endpoint-Adapterregeln-2026-08-11.md)
|
||||||
|
festgehalten.
|
||||||
|
|
||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
|
|
||||||
Build-Host:
|
Build-Host:
|
||||||
@@ -217,7 +239,9 @@ artifacts\BizTalkCheckmkPulse-Setup\application\BizTalkCheckmkPulse.exe --self-t
|
|||||||
|
|
||||||
Erwartet werden exakt neun `OK`-Zeilen. Die Regressionstests prüfen
|
Erwartet werden exakt neun `OK`-Zeilen. Die Regressionstests prüfen
|
||||||
zusätzlich Snapshot-Roundtrip, atomaren Ersatz, SHA-256-Manipulation,
|
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
|
Ein Mono-Build ist eine hilfreiche Quellcodeprüfung, ersetzt aber nicht die
|
||||||
Windows-/BizTalk-Laufzeitvalidierung.
|
Windows-/BizTalk-Laufzeitvalidierung.
|
||||||
|
|
||||||
@@ -258,24 +282,42 @@ als `UNKNOWN`.
|
|||||||
Ein gMSA kann über die Checkbox ohne Kennworteingabe installiert werden; die
|
Ein gMSA kann über die Checkbox ohne Kennworteingabe installiert werden; die
|
||||||
produktive Standardbeschreibung geht vom normalen Servicekonto aus.
|
produktive Standardbeschreibung geht vom normalen Servicekonto aus.
|
||||||
|
|
||||||
Der Installer:
|
Der Installer (ab Version 2.2.6):
|
||||||
|
|
||||||
1. prüft Paket und neun Self-Test-Zeilen, bevor der bestehende Task angehalten
|
1. prüft Paket und Staging zwingend auf neun Self-Test-Zeilen, bevor der
|
||||||
wird,
|
bestehende Task angehalten wird. Der Servicevertrag der installierten
|
||||||
|
Legacy-Version darf weniger gültige Zeilen enthalten,
|
||||||
2. baut die neue Version in einem Staging-Verzeichnis auf und übernimmt
|
2. baut die neue Version in einem Staging-Verzeichnis auf und übernimmt
|
||||||
vorhandene AppSettings; neue Keys erhalten ihre neuen Defaults. Die alten
|
vorhandene AppSettings; neue Keys erhalten ihre neuen Defaults. Die alten
|
||||||
unveränderten Endpoint-Defaults `12`/`500` werden auf `16`/`100` migriert,
|
unveränderten Endpoint-Defaults `12`/`500` werden auf `16`/`100` migriert,
|
||||||
3. schaltet die Programmdateien per Verzeichniswechsel um; bei einem Fehler
|
3. vergleicht installierten und neuen Checkmk-Servicevertrag. Rein additive
|
||||||
werden vorherige Version, Wrapper und Task best effort wiederhergestellt,
|
Services werden zugelassen und als Discovery-Hinweis gemeldet; entfernte
|
||||||
4. behält `%ProgramData%\BizTalkCheckmkPulse` mit Endpoint-Katalog, Snapshot
|
oder umbenannte Services stoppen ohne ausdrückliche Bestätigung vor jeder
|
||||||
und Logs bei,
|
Umschaltung,
|
||||||
5. installiert EXE und Config nach `%ProgramFiles%\BizTalkCheckmkPulse`,
|
4. schaltet die Programmdateien per Verzeichniswechsel um,
|
||||||
6. setzt explizite ACLs für Administratoren, Provider und `LocalSystem`,
|
5. startet einen triggerlosen Provider-Abnahmelauf unter dem echten
|
||||||
7. installiert nur den kleinen `.cmd`-Consumer unter
|
Collector-Konto und erzwingt einen vollständigen Endpoint-Katalogabgleich,
|
||||||
|
6. wartet höchstens vier Minuten auf `LastTaskResult=0`, bricht bei einem
|
||||||
|
bereits abgeschlossenen Fehlerlauf sofort mit Dezimal-/Hexcode ab und validiert danach
|
||||||
|
mit der Produkt-EXE einen erst nach Installationsbeginn erzeugten Snapshot,
|
||||||
|
die Collector-Identität, alle neun eindeutigen Services sowie den frischen,
|
||||||
|
maschinen- und umgebungsgebundenen Endpoint-Katalog. `UNKNOWN` in einem
|
||||||
|
Kernservice bleibt ein Abnahmefehler. Ein ausschließlich bei
|
||||||
|
`BizTalk Endpoint Reachability` auftretendes `UNKNOWN` wird als sichtbare
|
||||||
|
Betriebswarnung übernommen, weil es eine unvollständige Prüfabdeckung und
|
||||||
|
keinen defekten Installationslauf beschreibt,
|
||||||
|
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. Das laufbezogene Setup-Log enthält auch die vollständige
|
||||||
|
Self-Test-Ausgabe sowie bei Abnahmefehlern die letzten Provider-Logzeilen,
|
||||||
|
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`,
|
`%ProgramData%\checkmk\agent\local`,
|
||||||
8. registriert `BizTalk Checkmk Pulse Provider` minütlich mit
|
10. registriert `BizTalk Checkmk Pulse Provider` minütlich mit `IgnoreNew`,
|
||||||
`IgnoreNew`, fünf Minuten Laufzeitlimit und zwei Wiederholungen,
|
fünf Minuten Laufzeitlimit und zwei Wiederholungen.
|
||||||
9. führt den installierten Self-Test erneut aus und startet den Provider.
|
|
||||||
|
|
||||||
Installation, Update, Deinstallation und Laufzeit verwenden keine PowerShell.
|
Installation, Update, Deinstallation und Laufzeit verwenden keine PowerShell.
|
||||||
|
|
||||||
@@ -292,8 +334,20 @@ Provider-Log:
|
|||||||
```cmd
|
```cmd
|
||||||
dir /o-d "%ProgramData%\BizTalkCheckmkPulse\logs"
|
dir /o-d "%ProgramData%\BizTalkCheckmkPulse\logs"
|
||||||
type "%ProgramData%\BizTalkCheckmkPulse\logs\biztalk-checkmk-pulse-*.log"
|
type "%ProgramData%\BizTalkCheckmkPulse\logs\biztalk-checkmk-pulse-*.log"
|
||||||
|
type "%ProgramData%\BizTalkCheckmkPulse\logs\setup-*.log"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Die Analyse des in Version 2.2.4 beobachteten Abbruchs mit
|
||||||
|
`Exitcode=0, Zeilen=8` ist in
|
||||||
|
[docs/Installer-Self-Test-Analyse-2026-08-11.md](docs/Installer-Self-Test-Analyse-2026-08-11.md)
|
||||||
|
dokumentiert.
|
||||||
|
|
||||||
|
Der nachfolgende ACC-Abbruch mit einem technisch validen Snapshot und
|
||||||
|
`UNKNOWN=[BizTalk Endpoint Reachability]` ist in
|
||||||
|
[docs/Installer-Runtime-Unknown-Analyse-2026-08-11.md](docs/Installer-Runtime-Unknown-Analyse-2026-08-11.md)
|
||||||
|
analysiert. Der Zustand wird nicht grüngefärbt: Setup 2.2.6 schließt ab und
|
||||||
|
gibt die vollständige UNKNOWN-Zeile als Warnung aus; Checkmk behält Status 3.
|
||||||
|
|
||||||
Snapshot und Consumer:
|
Snapshot und Consumer:
|
||||||
|
|
||||||
```cmd
|
```cmd
|
||||||
@@ -356,6 +410,7 @@ Wichtige Werte:
|
|||||||
| Key | Default | Bedeutung |
|
| Key | Default | Bedeutung |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `EnvironmentName` | leer | Optional `ACC`, `DEV`, `TST`, `PRD`. |
|
| `EnvironmentName` | leer | Optional `ACC`, `DEV`, `TST`, `PRD`. |
|
||||||
|
| `IncludeEnvironmentInServiceName` | `false` | Nur mit geplanter Service Discovery aktivieren. |
|
||||||
| `SnapshotPath` | `%ProgramData%\BizTalkCheckmkPulse\data\...` | Gemeinsame Provider-/Consumer-Datei. |
|
| `SnapshotPath` | `%ProgramData%\BizTalkCheckmkPulse\data\...` | Gemeinsame Provider-/Consumer-Datei. |
|
||||||
| `SnapshotMaxAgeSeconds` | `180` | Ab diesem Alter liefert der Consumer `UNKNOWN`. |
|
| `SnapshotMaxAgeSeconds` | `180` | Ab diesem Alter liefert der Consumer `UNKNOWN`. |
|
||||||
| `SnapshotMaxBytes` | `1048576` | Harte Eingabegröße für den Consumer. |
|
| `SnapshotMaxBytes` | `1048576` | Harte Eingabegröße für den Consumer. |
|
||||||
@@ -395,7 +450,7 @@ liest den nächsten atomar publizierten Snapshot.
|
|||||||
| Provider meldet `Login failed` | Provider-Konto und exakt konfigurierte Read-Only-Gruppe sowie `BTS_READONLY_USERS` prüfen. |
|
| Provider meldet `Login failed` | Provider-Konto und exakt konfigurierte Read-Only-Gruppe sowie `BTS_READONLY_USERS` prüfen. |
|
||||||
| Receive Locations / Send Ports sind CRIT | `affected=` prüfen; nur fachlich bewusst inaktive Namen exakt in die jeweilige Allowlist aufnehmen. |
|
| Receive Locations / Send Ports sind CRIT | `affected=` prüfen; nur fachlich bewusst inaktive Namen exakt in die jeweilige Allowlist aufnehmen. |
|
||||||
| Endpoint Reachability ist CRIT | Nur `unavailable=` prüfen; DNS, Zielport, Firewall und externen Dienst kontrollieren. |
|
| Endpoint Reachability ist CRIT | Nur `unavailable=` prüfen; DNS, Zielport, Firewall und externen Dienst kontrollieren. |
|
||||||
| Endpoint Reachability ist UNKNOWN | `unresolved_endpoints=`, WMI-Vollständigkeit, `endpoints.xml` und wöchentlichen Refresh prüfen; indirekte Adapterziele bei Bedarf geheimnisfrei manuell überschreiben. |
|
| Endpoint Reachability ist UNKNOWN | `unresolved_endpoints=`, WMI-Vollständigkeit, `endpoints.xml` und wöchentlichen Refresh prüfen. Schedule sowie nicht ableitbare DATABASE-/WCF-SAP-Ziele sind ab 2.2.7 bereits ausgenommen; andere indirekte Ziele bei Bedarf geheimnisfrei manuell überschreiben. |
|
||||||
| `Wmi/Schema` | Klasse/Properties gegen BizTalk-2020-Schema prüfen; keine Rechte ausweiten. |
|
| `Wmi/Schema` | Klasse/Properties gegen BizTalk-2020-Schema prüfen; keine Rechte ausweiten. |
|
||||||
| Nur Event Log `UNKNOWN` | lokalen Application-Log-Zugriff des Provider-Kontos prüfen. |
|
| Nur Event Log `UNKNOWN` | lokalen Application-Log-Zugriff des Provider-Kontos prüfen. |
|
||||||
| Task-Result `2` | Parallelstart oder Snapshot-I/O; Log und Lock/ACL prüfen. |
|
| Task-Result `2` | Parallelstart oder Snapshot-I/O; Log und Lock/ACL prüfen. |
|
||||||
|
|||||||
@@ -21,7 +21,87 @@ migriert. Abweichende, bewusst konfigurierte Werte bleiben erhalten.
|
|||||||
`EndpointMaxCount` begrenzt nun eindeutige Socket-Ziele statt Artefakte; der
|
`EndpointMaxCount` begrenzt nun eindeutige Socket-Ziele statt Artefakte; der
|
||||||
neue Wert `EndpointCatalogMaxEntries=1000` begrenzt separat die Kataloggröße.
|
neue Wert `EndpointCatalogMaxEntries=1000` begrenzt separat die Kataloggröße.
|
||||||
|
|
||||||
Vor dem Stoppen des vorhandenen Tasks wird die neue Version separat getestet.
|
Version 2.2.2 ergänzt `IncludeEnvironmentInServiceName=false`. Damit bleibt
|
||||||
|
`EnvironmentName=ACC` als Umgebungsmetadatum erhalten, ohne die bestehenden
|
||||||
|
Services von `BizTalk ...` in `BizTalk ACC ...` umzubenennen. Das beseitigt
|
||||||
|
nach dem nächsten Agentenlauf das 2.2.1-Fehlerbild `Item not found in
|
||||||
|
monitoring data`. Nur bei bewusst auf `true` gesetztem Opt-in ist anschließend
|
||||||
|
eine Checkmk Service Discovery erforderlich.
|
||||||
|
|
||||||
|
Ab Version 2.2.5 vergleicht der Installer bei jedem Update die exakten
|
||||||
|
Servicenamen der installierten und der vorbereiteten Version. Rein neue
|
||||||
|
Services sind abwärtskompatibel, blockieren das Update nicht und werden als
|
||||||
|
Hinweis für die anschließende Service Discovery angezeigt. Entfernte oder
|
||||||
|
umbenannte Services führen **vor dem Stoppen des Tasks** zu einem
|
||||||
|
Sicherheitsstopp. Die Installation darf dann nur über die Checkbox
|
||||||
|
**Service-Rename ist beabsichtigt; Checkmk Service Discovery ist eingeplant**
|
||||||
|
fortgesetzt werden. Der Installer zeigt dabei die entfernten und neuen Namen
|
||||||
|
an. Eine bloß geänderte Ausgabereihenfolge gilt nicht als Rename.
|
||||||
|
|
||||||
|
Version 2.2.5 akzeptiert außerdem den gültigen Self-Test einer installierten
|
||||||
|
Legacy-Version mit weniger Services. Paket, Staging und aktivierte Zielversion
|
||||||
|
müssen weiterhin exakt neun Services liefern. Damit kann insbesondere eine
|
||||||
|
Version mit acht Services auf die Version mit dem zusätzlichen Service
|
||||||
|
`BizTalk Endpoint Reachability` aktualisiert werden. Der frühere Abbruch
|
||||||
|
`Exitcode=0, Zeilen=8` betraf die Altversion und war kein Fehler im neuen Paket.
|
||||||
|
|
||||||
|
Version 2.2.6 trennt technische Installationsabnahme und fachlichen
|
||||||
|
Endpoint-Zustand. Ein frischer, integritäts- und identitätsgeprüfter Snapshot
|
||||||
|
mit allen neun Services und ein frisch synchronisierter Endpoint-Katalog
|
||||||
|
belegen eine funktionsfähige Installation. Meldet ausschließlich
|
||||||
|
`BizTalk Endpoint Reachability` weiterhin `UNKNOWN`, wird die Installation
|
||||||
|
abgeschlossen und die vollständige Checkmk-Zeile als Warnung im Setup sowie
|
||||||
|
im Setup-Log ausgegeben. Der Status bleibt im Snapshot und in Checkmk
|
||||||
|
unverändert `UNKNOWN`, bis Auflösungslücken, Katalog-/Probeprobleme oder
|
||||||
|
fehlende manuelle Overrides betrieblich geklärt sind. `UNKNOWN` in Platform,
|
||||||
|
SQL oder einem anderen Kernservice bleibt ein blockierender Abnahmefehler.
|
||||||
|
|
||||||
|
Version 2.2.7 beseitigt die bekannten fachlichen Fehlalarme der
|
||||||
|
Endpoint-Prüfung: Scheduling-Komponenten werden ausgeschlossen; Oracle-
|
||||||
|
DATABASE- und WCF-SAP-Ziele werden als TCP-Best-Effort-Probes abgeleitet.
|
||||||
|
Nicht ableitbare oder nicht erreichbare Best-Effort-Ziele lösen weder UNKNOWN
|
||||||
|
noch CRIT aus. Reguläre und manuell konfigurierte Ziele bleiben unverändert
|
||||||
|
streng alarmierend.
|
||||||
|
|
||||||
|
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` in Kernservices blockiert die
|
||||||
|
Installation; ausschließlich `BizTalk Endpoint Reachability = UNKNOWN`
|
||||||
|
wird ab 2.2.6 als nicht blockierende Betriebswarnung übernommen,
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
Jeder Installations-/Updatelauf schreibt ein separates Diagnoselog nach
|
||||||
|
`%ProgramData%\BizTalkCheckmkPulse\logs\setup-*.log`. Es enthält die aktuelle
|
||||||
|
Phase, Pfad und Version jeder geprüften EXE, Exitcode, vollständiges stdout und
|
||||||
|
stderr des Self-Tests, Taskstatus samt Dezimal-/Hexcode, Exception-Kette und
|
||||||
|
Rollback-Ergebnis. Bei einem Runtime-Abnahmefehler werden zusätzlich die
|
||||||
|
letzten Provider-Logzeilen übernommen. Das Collector-Kennwort wird weder an
|
||||||
|
das Logging übergeben noch protokolliert. Ein abgeschlossener Providerlauf mit
|
||||||
|
Fehlercode beendet die Abnahme sofort; nur ein noch laufender Task wird bis zur
|
||||||
|
Timeoutgrenze abgewartet.
|
||||||
|
|
||||||
|
Beim korrigierenden Wechsel von 2.2.1 mit `BizTalk ACC ...` auf stabile
|
||||||
|
`BizTalk ...`-Namen ist die Änderung beabsichtigt: Checkbox aktivieren und den
|
||||||
|
Servicebestand danach per Discovery abgleichen. Sind in Checkmk bereits die
|
||||||
|
unpräfixierten Services vorhanden, werden damit insbesondere eventuell
|
||||||
|
vorhandene falsche `BizTalk ACC ...`-Services bereinigt.
|
||||||
|
|
||||||
|
Vor dem Stoppen des vorhandenen Tasks wird die neue Version separat getestet
|
||||||
|
und ihr Checkmk-Servicevertrag mit der installierten Version verglichen.
|
||||||
Bei normalen Servicekonten prüft der Installer außerdem Kennwort und
|
Bei normalen Servicekonten prüft der Installer außerdem Kennwort und
|
||||||
Batch-Anmelderecht vor der Umschaltung.
|
Batch-Anmelderecht vor der Umschaltung.
|
||||||
Der Task wird vor dem Dateitausch deaktiviert und sein Prozessende maximal zehn
|
Der Task wird vor dem Dateitausch deaktiviert und sein Prozessende maximal zehn
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
# ACC-Analyse: Checkmk-Datenalter und Endpoint Reachability
|
||||||
|
|
||||||
|
> Nachtrag 11.08.2026: Version 2.2.7 behebt die aus der RTM-Ausgabe
|
||||||
|
> identifizierten Schedule-, Oracle-DATABASE- und WCF-SAP-Sonderfälle. Die
|
||||||
|
> verbindliche Nachfolgeregel steht in
|
||||||
|
> [RTM-Endpoint-Adapterregeln-2026-08-11.md](RTM-Endpoint-Adapterregeln-2026-08-11.md).
|
||||||
|
|
||||||
|
## Befund
|
||||||
|
|
||||||
|
Die am 10.08.2026 bereitgestellten ACC-Artefakte zeigen zwei getrennte
|
||||||
|
Fehlerbilder.
|
||||||
|
|
||||||
|
### 1. Checkmk findet die neun Services nicht mehr
|
||||||
|
|
||||||
|
Die Checkmk-Ansicht erwartet die bisherigen Namen ohne Umgebung, zum Beispiel:
|
||||||
|
|
||||||
|
```text
|
||||||
|
BizTalk Endpoint Reachability
|
||||||
|
BizTalk Platform
|
||||||
|
BizTalk SQL Access
|
||||||
|
```
|
||||||
|
|
||||||
|
Der aktuelle Snapshot enthält dagegen ausschließlich umbenannte Services:
|
||||||
|
|
||||||
|
```text
|
||||||
|
BizTalk ACC Endpoint Reachability
|
||||||
|
BizTalk ACC Platform
|
||||||
|
BizTalk ACC SQL Access
|
||||||
|
```
|
||||||
|
|
||||||
|
Damit ist `Item not found in monitoring data` kein Beleg für einen veralteten
|
||||||
|
oder fehlenden Snapshot. Checkmk sucht nach einem anderen Servicenamen als der
|
||||||
|
Agent aktuell liefert. Die unterschiedlichen Alterswerte sind die Zeitpunkte,
|
||||||
|
zu denen Checkmk die jeweiligen alten Namen zuletzt gesehen hat.
|
||||||
|
|
||||||
|
Ursache in Version 2.2.1: `EnvironmentName=ACC` wurde zugleich als
|
||||||
|
Servicenamensbestandteil verwendet. Das Installerfeld „Umgebung“ konnte damit
|
||||||
|
bei einem Update den stabilen Checkmk-Vertrag unbeabsichtigt ändern.
|
||||||
|
|
||||||
|
### 2. Endpoint-Probe ist schnell und erfolgreich, wird aber als UNKNOWN formatiert
|
||||||
|
|
||||||
|
Der Snapshot belegt:
|
||||||
|
|
||||||
|
| Wert | ACC-Befund |
|
||||||
|
| --- | ---: |
|
||||||
|
| aktive Transportkandidaten laut Katalog | 135 |
|
||||||
|
| automatisch konfigurierte/prüfbare Artefakte | 64 |
|
||||||
|
| vollständig getestete Artefakte | 64 |
|
||||||
|
| erreichbare Artefakte | 64 |
|
||||||
|
| fehlgeschlagene Artefakte | 0 |
|
||||||
|
| eindeutige Host-/Port-/Protokoll-Ziele | 17 |
|
||||||
|
| nicht automatisch auflösbare externe Kandidaten | 61 |
|
||||||
|
| Probe-Laufzeit | 266 ms |
|
||||||
|
|
||||||
|
Die Netzwerkprobe selbst ist damit weder veraltet noch langsam. Sie
|
||||||
|
dedupliziert 64 Artefakte auf 17 Netzwerkverbindungen und schließt alle Probes
|
||||||
|
in deutlich unter einer Sekunde ab.
|
||||||
|
|
||||||
|
Version 2.2.1 behandelt jedoch jeden Wert
|
||||||
|
`biztalk_endpoints_unsupported > 0` als unvollständiges Ergebnis. Deshalb wird
|
||||||
|
der Service trotz `available=64` und `failed=0` als `UNKNOWN` ausgegeben. Die
|
||||||
|
61 Kandidaten fehlen erwartungsgemäß in `endpoints.xml`, weil für sie kein
|
||||||
|
sicherer und geheimnisfreier Host/Port ermittelt werden konnte. Das ist eine
|
||||||
|
Abdeckungsinformation, kein Fehler der 64 ausgeführten Probes.
|
||||||
|
|
||||||
|
Aus `135 - 64 - 61` ergeben sich zusätzlich 10 lokale, dynamische oder bewusst
|
||||||
|
nicht netzwerkprüfbare Kandidaten. Version 2.2.2 weist sie explizit als
|
||||||
|
`excluded` aus.
|
||||||
|
|
||||||
|
## Korrektur in Version 2.2.2
|
||||||
|
|
||||||
|
### Stabile Servicenamen
|
||||||
|
|
||||||
|
- `EnvironmentName` bleibt Umgebungsmetadatum für Snapshot und Katalog.
|
||||||
|
- Standard `IncludeEnvironmentInServiceName=false`: Die neun Namen bleiben
|
||||||
|
`BizTalk ...` und passen wieder zu den bereits entdeckten ACC-Services.
|
||||||
|
- Nur ein bewusstes Opt-in mit `true` erzeugt Namen wie `BizTalk ACC ...` und
|
||||||
|
verlangt anschließend eine Checkmk Service Discovery.
|
||||||
|
- Version 2.2.3 ergänzt einen versionsübergreifenden Installer-Schutz: Der
|
||||||
|
tatsächliche Servicevertrag der vorhandenen und der neuen EXE wird vor dem
|
||||||
|
Task-Stopp verglichen. Unbestätigte Renames, neue oder entfernte Services
|
||||||
|
blockieren das Update; die Differenz wird vollständig angezeigt.
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- Erfolgreich geprüfte Ziele bestimmen den Zustand der Endpoint-Probe.
|
||||||
|
- Ein fehlgeschlagenes getestetes Ziel bleibt `CRIT`.
|
||||||
|
- `unsupported` und `excluded` werden als vollständige Metriken ausgegeben,
|
||||||
|
führen neben erfolgreicher Prüfabdeckung aber nicht mehr zu `UNKNOWN`.
|
||||||
|
- Wenn externe `unsupported`-Kandidaten existieren und überhaupt kein Ziel
|
||||||
|
automatisch geprüft werden kann, bleibt der Service bewusst `UNKNOWN`.
|
||||||
|
|
||||||
|
### Sofortige Katalogkonsistenz
|
||||||
|
|
||||||
|
Der Provider vergleicht in jedem Minutenlauf die bereits aus WMI gelesenen
|
||||||
|
aktiven Kandidaten mit dem Katalog. Folgende Änderungen lösen sofort einen
|
||||||
|
atomaren Neuabgleich aus:
|
||||||
|
|
||||||
|
- neuer, deaktivierter oder entfernter prüfbarer Artefaktschlüssel,
|
||||||
|
- geänderter Host, Port oder Protokoll eines vorhandenen Artefakts,
|
||||||
|
- geänderte Zahl aktiver oder nicht automatisch auflösbarer Kandidaten.
|
||||||
|
|
||||||
|
Manuelle Overrides gelten weiterhin als beabsichtigte Abdeckung und lösen
|
||||||
|
keine Refresh-Schleife aus. Der 168-Stunden-Abgleich bleibt als zusätzliche
|
||||||
|
Frischegrenze bestehen.
|
||||||
|
|
||||||
|
### Laufzeitgrenzen
|
||||||
|
|
||||||
|
Die performante Implementierung bleibt unverändert:
|
||||||
|
|
||||||
|
- Deduplizierung nach Protokoll, Host und Port,
|
||||||
|
- maximal 16 parallele Probes,
|
||||||
|
- 3 Sekunden Timeout je eindeutiges Ziel,
|
||||||
|
- maximal 100 automatisch konfigurierte Endpoint-Einträge.
|
||||||
|
|
||||||
|
Der beobachtete ACC-Wert von 266 ms für 17 eindeutige Ziele liegt deutlich im
|
||||||
|
Minutenbudget.
|
||||||
|
|
||||||
|
## Erwartung nach dem Update
|
||||||
|
|
||||||
|
Der Endpoint-Service sollte bei unverändertem ACC-Zustand sinngemäß liefern:
|
||||||
|
|
||||||
|
```text
|
||||||
|
0 "BizTalk Endpoint Reachability" ... candidates=135 ... active=64 ...
|
||||||
|
available=64 ... failed=0 ... unsupported=61 ... excluded=10 ...
|
||||||
|
unique_targets=17 ... probe_ms=<Laufzeit>
|
||||||
|
```
|
||||||
|
|
||||||
|
Die übrigen fachlichen Zustände aus dem gelieferten Snapshot werden danach in
|
||||||
|
Checkmk wieder sichtbar und sind getrennt zu bewerten:
|
||||||
|
|
||||||
|
- 3 resumable Suspensions: `WARN`,
|
||||||
|
- 2 Host Instances mit unbekanntem Status: `CRIT`,
|
||||||
|
- 22 nicht allowlistete deaktivierte Receive Locations: `CRIT`,
|
||||||
|
- 17 nicht allowlistete inaktive Send Ports: `CRIT`,
|
||||||
|
- 2 Errors und 6 Warnings im Event-Log-Zeitfenster: `WARN`.
|
||||||
|
|
||||||
|
Diese Zustände sind nicht die Ursache des Datenalters und werden durch den
|
||||||
|
Endpoint-/Namensfix bewusst nicht grüngefärbt.
|
||||||
|
|
||||||
|
## ACC-Abnahme nach Installation
|
||||||
|
|
||||||
|
1. Setup 2.2.6 als Update ausführen; Umgebung `ACC` kann bestehen bleiben.
|
||||||
|
Beim Wechsel von der fehlerhaften 2.2.1-Ausgabe `BizTalk ACC ...` auf die
|
||||||
|
stabilen Namen die Rename-/Discovery-Checkbox bewusst aktivieren.
|
||||||
|
2. In der installierten Config bestätigen:
|
||||||
|
|
||||||
|
```text
|
||||||
|
EnvironmentName=ACC
|
||||||
|
IncludeEnvironmentInServiceName=false
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Scheduled Task manuell starten und `LastTaskResult=0` prüfen.
|
||||||
|
4. Snapshot zweimal im Abstand von ungefähr einer Minute prüfen; `generatedUtc`
|
||||||
|
muss fortschreiten und die Namen müssen `BizTalk ...` ohne `ACC` lauten.
|
||||||
|
5. `BizTalk Endpoint Reachability` muss bei `failed=0` und vorhandener
|
||||||
|
automatischer Prüfabdeckung `OK` liefern. `unsupported`/`excluded` bleiben
|
||||||
|
als Metriken sichtbar.
|
||||||
|
6. Agent-Dump prüfen:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
"C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump | findstr /C:"BizTalk "
|
||||||
|
```
|
||||||
|
|
||||||
|
7. Checkmk-Prüfung neu planen. Die bestehenden neun Namen sollten ohne
|
||||||
|
Umbenennung wieder Daten erhalten. Nur falls Checkmk noch alte
|
||||||
|
`BizTalk ACC ...`-Services enthält, einmal Service Discovery durchführen
|
||||||
|
und die bewusst falschen Namen entfernen.
|
||||||
|
8. `biztalk_endpoint_probe_duration_ms` und `elapsed_ms` über mehrere Läufe
|
||||||
|
beobachten; der Task darf nicht in das nächste Minutenintervall laufen.
|
||||||
Binary file not shown.
@@ -185,6 +185,10 @@ Zweck:
|
|||||||
- Erkennt unter anderem Standard-URIs, UNC, MSMQ, `net.tcp`, WCF-SQL-
|
- Erkennt unter anderem Standard-URIs, UNC, MSMQ, `net.tcp`, WCF-SQL-
|
||||||
`mssql`-URIs, schemafreie `host:port`-Angaben, FTP-/SFTP-Ziele mit optionalen
|
`mssql`-URIs, schemafreie `host:port`-Angaben, FTP-/SFTP-Ziele mit optionalen
|
||||||
Benutzerinformationen sowie lokale HTTP-/WCF-Wildcard-Listener.
|
Benutzerinformationen sowie lokale HTTP-/WCF-Wildcard-Listener.
|
||||||
|
- Schließt Scheduling-Komponenten vollständig aus. Oracle-`DATABASE`- und
|
||||||
|
WCF-SAP-Adressen werden, soweit eindeutig möglich, als TCP-Best-Effort-Ziel
|
||||||
|
geprüft; fehlende Ableitung und fehlgeschlagene Best-Effort-Probes alarmieren
|
||||||
|
nicht.
|
||||||
- Behandelt Adressen ohne eigenständiges Socket-Ziel (zum Beispiel lokale
|
- Behandelt Adressen ohne eigenständiges Socket-Ziel (zum Beispiel lokale
|
||||||
Pfade, SMTP-Empfänger, Named Pipes und relative Receive-Listener) als
|
Pfade, SMTP-Empfänger, Named Pipes und relative Receive-Listener) als
|
||||||
erwartbar ausgenommen und nicht als Abdeckungsfehler.
|
erwartbar ausgenommen und nicht als Abdeckungsfehler.
|
||||||
@@ -197,8 +201,8 @@ Status:
|
|||||||
|
|
||||||
| Status | Bedingung |
|
| Status | Bedingung |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `OK` | Katalog und Runtimezustand vollständig; alle aktiven prüfbaren Ziele erreichbar. |
|
| `OK` | Katalog und Runtimezustand vollständig; alle alarmierenden Ziele erreichbar. Best-Effort-Fehler bei DATABASE/WCF-SAP sind höchstens als Metrik sichtbar. |
|
||||||
| `CRIT` | Mindestens ein getestetes TCP-/UDP-Ziel nicht erreichbar. |
|
| `CRIT` | Mindestens ein reguläres oder manuell konfiguriertes TCP-/UDP-Ziel nicht erreichbar. |
|
||||||
| `UNKNOWN` | Katalog fehlt/ist ungültig, Wochenabgleich scheitert, Runtimezustand ist unvollständig oder ein aktiver externer Endpoint kann nicht sicher auf Host/Port reduziert werden. |
|
| `UNKNOWN` | Katalog fehlt/ist ungültig, Wochenabgleich scheitert, Runtimezustand ist unvollständig oder ein aktiver externer Endpoint kann nicht sicher auf Host/Port reduziert werden. |
|
||||||
| `OK` mit Hinweis | Nur wenn `ProbeEndpointConnectivity=false`; der Test ist bewusst deaktiviert. |
|
| `OK` mit Hinweis | Nur wenn `ProbeEndpointConnectivity=false`; der Test ist bewusst deaktiviert. |
|
||||||
|
|
||||||
@@ -214,6 +218,8 @@ Metriken:
|
|||||||
| `biztalk_endpoints_failed` | Nicht erreichbare aktive Artefaktziele; CRIT ab 1. |
|
| `biztalk_endpoints_failed` | Nicht erreichbare aktive Artefaktziele; CRIT ab 1. |
|
||||||
| `biztalk_endpoints_unresolved` | Aktive statische Netzwerkadressen ohne sicher bestimmbaren Host/Port; `UNKNOWN` ab 1, sofern kein manueller Override existiert. |
|
| `biztalk_endpoints_unresolved` | Aktive statische Netzwerkadressen ohne sicher bestimmbaren Host/Port; `UNKNOWN` ab 1, sofern kein manueller Override existiert. |
|
||||||
| `biztalk_endpoints_expected_non_socket` | Aktive Artefakte, deren Adresse erwartbar kein eigenständiges TCP-/UDP-Ziel enthält. |
|
| `biztalk_endpoints_expected_non_socket` | Aktive Artefakte, deren Adresse erwartbar kein eigenständiges TCP-/UDP-Ziel enthält. |
|
||||||
|
| `biztalk_endpoints_best_effort` | Automatisch abgeleitete aktive Oracle-DATABASE-/WCF-SAP-Ziele. |
|
||||||
|
| `biztalk_endpoints_best_effort_ignored` | Fehlgeschlagene Best-Effort-Probes, die vereinbarungsgemäß keinen Alarm auslösen. |
|
||||||
| `biztalk_endpoints_manual_overrides` | Aktive Ziele, die bewusst über einen geheimnisfreien manuellen Katalogeintrag aufgelöst werden. |
|
| `biztalk_endpoints_manual_overrides` | Aktive Ziele, die bewusst über einen geheimnisfreien manuellen Katalogeintrag aufgelöst werden. |
|
||||||
| `biztalk_endpoints_inactive_skipped` | Konfigurierte Einträge, deren BizTalk-Artefakt aktuell nicht aktiv ist. |
|
| `biztalk_endpoints_inactive_skipped` | Konfigurierte Einträge, deren BizTalk-Artefakt aktuell nicht aktiv ist. |
|
||||||
| `biztalk_endpoint_probe_duration_ms` | Gesamtlaufzeit der parallelisierten Socket-Probes in Millisekunden. |
|
| `biztalk_endpoint_probe_duration_ms` | Gesamtlaufzeit der parallelisierten Socket-Probes in Millisekunden. |
|
||||||
|
|||||||
+27
-1
@@ -15,12 +15,26 @@ Abgleich alle `EndpointDiscoveryIntervalHours` (Default: 168 Stunden). Ein
|
|||||||
Fehler beim Abgleich ersetzt keinen letzten gültigen Katalog. Checkmk meldet
|
Fehler beim Abgleich ersetzt keinen letzten gültigen Katalog. Checkmk meldet
|
||||||
den überfälligen/fehlgeschlagenen Abgleich trotzdem als `UNKNOWN`.
|
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.
|
||||||
|
|
||||||
|
Ab Setup 2.2.6 blockiert ein technisch gültiger Abnahmelauf nicht mehr allein
|
||||||
|
deshalb, weil `BizTalk Endpoint Reachability` fachlich `UNKNOWN` meldet. Die
|
||||||
|
Installation wird mit einer sichtbaren Warnung abgeschlossen; Status, Metriken
|
||||||
|
und Detailtext bleiben im Snapshot erhalten. Ein fehlender, ungültiger oder
|
||||||
|
nicht in diesem Abnahmelauf synchronisierter Katalog bleibt dagegen ein harter
|
||||||
|
Installationsfehler.
|
||||||
|
|
||||||
Unabhängig vom Wochenabgleich wird bei jedem Minutenlauf gegen den aktuellen
|
Unabhängig vom Wochenabgleich wird bei jedem Minutenlauf gegen den aktuellen
|
||||||
BizTalk-Runtimezustand gefiltert:
|
BizTalk-Runtimezustand gefiltert:
|
||||||
|
|
||||||
- Receive Location nur bei `IsDisabled=false`
|
- Receive Location nur bei `IsDisabled=false`
|
||||||
- Send Port nur bei `Status=Started`
|
- Send Port nur bei `Status=Started`
|
||||||
- dynamischer Send Port nie als statisches Ziel
|
- dynamischer Send Port nie als statisches Ziel
|
||||||
|
- Scheduling-Komponente nie als Netzwerkziel
|
||||||
|
|
||||||
Damit wird ein inzwischen gestopptes/deaktiviertes Artefakt sofort nicht mehr
|
Damit wird ein inzwischen gestopptes/deaktiviertes Artefakt sofort nicht mehr
|
||||||
geprüft. Prüffähige aktuelle WMI-Adressen werden bei jedem Minutenlauf direkt
|
geprüft. Prüffähige aktuelle WMI-Adressen werden bei jedem Minutenlauf direkt
|
||||||
@@ -58,7 +72,7 @@ werden nur:
|
|||||||
- Adaptername
|
- Adaptername
|
||||||
- `TCP` oder `UDP`
|
- `TCP` oder `UDP`
|
||||||
- Host und Port
|
- Host und Port
|
||||||
- `enabled` und `autoDiscovered`
|
- `enabled`, `autoDiscovered` und die Best-Effort-Kennzeichnung
|
||||||
|
|
||||||
URI-Benutzerinformationen, Kennwörter, Pfade und Querystrings werden nicht
|
URI-Benutzerinformationen, Kennwörter, Pfade und Querystrings werden nicht
|
||||||
geschrieben. Der Katalog liegt im bereits ACL-geschützten `data`-Verzeichnis;
|
geschrieben. Der Katalog liegt im bereits ACL-geschützten `data`-Verzeichnis;
|
||||||
@@ -122,6 +136,18 @@ passende nebenwirkungsfreie Netzwerktest. Er bestätigt DNS, Routing,
|
|||||||
Firewall und einen annehmenden TCP-Port, aber nicht die fachliche Antwort der
|
Firewall und einen annehmenden TCP-Port, aber nicht die fachliche Antwort der
|
||||||
Zielanwendung.
|
Zielanwendung.
|
||||||
|
|
||||||
|
Oracle-`DATABASE`-Adressen mit explizitem Anbietersegment `/Oracle` verwenden
|
||||||
|
den angegebenen Port oder den Oracle-Standard TCP 1521. WCF-SAP-Adressen
|
||||||
|
verwenden vorrangig den ersten `SAPROUTER=/H/host[/S/port]`-Hop, andernfalls
|
||||||
|
`ListenerGwHost`/`ListenerGwServ` beziehungsweise `GwHost`/`GwServ`.
|
||||||
|
`sapgwNN` wird als TCP `33NN` abgeleitet; der SAProuter-Standard ist TCP 3299.
|
||||||
|
Diese automatisch erkannten Adapterziele sind `bestEffort="true"`: Die Probe
|
||||||
|
wird ausgeführt und gemessen, ein Fehlschlag aber nur in
|
||||||
|
`biztalk_endpoints_best_effort_ignored` gezählt. Nicht sicher ableitbare
|
||||||
|
DATABASE-/WCF-SAP-Adressen werden erwartbar ausgenommen. Ein manueller
|
||||||
|
Katalogeintrag bleibt standardmäßig streng alarmierend und ist die bewusste
|
||||||
|
Option für verbindliche Überwachung.
|
||||||
|
|
||||||
UDP ist verbindungslos. Bei `udp://host:port` bestätigt der Check nur DNS,
|
UDP ist verbindungslos. Bei `udp://host:port` bestätigt der Check nur DNS,
|
||||||
Route und erfolgreichen lokalen Datagrammversand. Für einen belastbaren
|
Route und erfolgreichen lokalen Datagrammversand. Für einen belastbaren
|
||||||
UDP-Dienstcheck wäre ein adapter-/protokollspezifisches Request/Response-
|
UDP-Dienstcheck wäre ein adapter-/protokollspezifisches Request/Response-
|
||||||
|
|||||||
@@ -61,15 +61,18 @@ Artefakte. Nur die Textliste ist begrenzt.
|
|||||||
Gesund, ohne Auflistung aller Ziele:
|
Gesund, ohne Auflistung aller Ziele:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
0 "BizTalk ACC Endpoint Reachability" biztalk_endpoints_configured=63;;;0|biztalk_endpoints_active=61;;;0|biztalk_endpoints_unique_targets=44;;;0|biztalk_endpoints_tested=61;;;0|biztalk_endpoints_available=61;;;0|biztalk_endpoints_failed=0;;1;0|biztalk_endpoints_unresolved=0;;1;0|biztalk_endpoints_expected_non_socket=12;;;0|biztalk_endpoints_manual_overrides=0;;;0|biztalk_endpoints_inactive_skipped=2;;;0|biztalk_endpoint_probe_duration_ms=842;;;0 Alle 61 aktiven, pruefbaren Send-/Receive-Endpunkte sind erreichbar (unique_targets=44, expected_non_socket=12, manual_overrides=0, probe_ms=842); catalog_utc=2026-08-04T10:00:00.0000000Z
|
0 "BizTalk ACC Endpoint Reachability" biztalk_endpoints_configured=63;;;0|biztalk_endpoints_active=61;;;0|biztalk_endpoints_unique_targets=44;;;0|biztalk_endpoints_tested=61;;;0|biztalk_endpoints_available=60;;;0|biztalk_endpoints_failed=0;;1;0|biztalk_endpoints_unresolved=0;;1;0|biztalk_endpoints_expected_non_socket=12;;;0|biztalk_endpoints_best_effort=9;;;0|biztalk_endpoints_best_effort_ignored=1;;;0|biztalk_endpoints_manual_overrides=0;;;0|biztalk_endpoints_inactive_skipped=2;;;0|biztalk_endpoint_probe_duration_ms=842;;;0 Alle regulaer alarmierenden aktiven Send-/Receive-Endpunkte sind erreichbar (active=61, available=60, unique_targets=44, expected_non_socket=12, best_effort=9, best_effort_ignored=1, manual_overrides=0, probe_ms=842); catalog_utc=2026-08-04T10:00:00.0000000Z
|
||||||
```
|
```
|
||||||
|
|
||||||
Fehlerhaft, nur mit nicht erreichbaren Zielen:
|
Fehlerhaft, nur mit nicht erreichbaren Zielen:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
2 "BizTalk ACC Endpoint Reachability" ... Endpoint reachability active=61, tested=61, available=59, failed=2, unresolved=0, expected_non_socket=12, manual_overrides=0; unavailable=SendPort:Orders\SP_Order_API[Primary]->orders.example:443/TCP(TCP timeout), ReceiveLocation:Invoices\RL_SFTP[Inbound]->sftp.example:22/TCP(SocketError=ConnectionRefused)
|
2 "BizTalk ACC Endpoint Reachability" ... Endpoint reachability active=61, tested=61, available=58, failed=2, unresolved=0, expected_non_socket=12, best_effort=9, best_effort_ignored=1, manual_overrides=0; unavailable=SendPort:Orders\SP_Order_API[Primary]->orders.example:443/TCP(TCP timeout), ReceiveLocation:Invoices\RL_SFTP[Inbound]->sftp.example:22/TCP(SocketError=ConnectionRefused)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Der nicht verfügbare Best-Effort-Endpunkt wird nur gezählt und nicht in
|
||||||
|
`unavailable=` ausgegeben. Die beiden regulären Fehler bleiben CRIT.
|
||||||
|
|
||||||
## Fehlender oder veralteter Provider-Snapshot
|
## Fehlender oder veralteter Provider-Snapshot
|
||||||
|
|
||||||
Alle neun Services werden `UNKNOWN`, zum Beispiel:
|
Alle neun Services werden `UNKNOWN`, zum Beispiel:
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Analyse: Runtime-Abnahme mit Endpoint-Reachability `UNKNOWN`
|
||||||
|
|
||||||
|
## Beobachtung
|
||||||
|
|
||||||
|
Setup 2.2.5 schaltete die neue Version um, startete den Provider erfolgreich
|
||||||
|
und validierte danach die Runtime-Artefakte. Die Abnahme endete trotzdem mit:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Runtime validation failed: Snapshot enthaelt unzuverlaessige UNKNOWN-Zustaende
|
||||||
|
in stabilen Services. UNKNOWN=[BizTalk Endpoint Reachability].
|
||||||
|
```
|
||||||
|
|
||||||
|
Anschließend stellte der Installer die vorherige Version samt Task und frischem
|
||||||
|
Snapshot erfolgreich wieder her.
|
||||||
|
|
||||||
|
## Einordnung
|
||||||
|
|
||||||
|
`LastTaskResult=0` belegt, dass der Providerlauf technisch abgeschlossen und
|
||||||
|
ein Snapshot geschrieben wurde. Zusätzlich waren Snapshot-Zeitpunkt,
|
||||||
|
SHA-256, Maschine, Collector-Identität, alle neun Servicenamen und der im
|
||||||
|
Abnahmelauf synchronisierte Endpoint-Katalog valide. Das verbleibende
|
||||||
|
`UNKNOWN` war daher kein Installations- oder Transaktionsfehler, sondern der
|
||||||
|
fachliche Zustand der Endpoint-Prüfung.
|
||||||
|
|
||||||
|
Der Endpoint-Service verwendet `UNKNOWN`, wenn die Prüfabdeckung nicht
|
||||||
|
vollständig verlässlich ist, zum Beispiel bei einer nicht sicher auf
|
||||||
|
Host/Port reduzierbaren aktiven Adresse, einem Katalog-/Probehinweis oder
|
||||||
|
fehlendem manuellem Override. Diesen Zustand auf `OK` zu ändern würde die
|
||||||
|
Monitoringlücke verbergen.
|
||||||
|
|
||||||
|
## Korrektur in Version 2.2.6
|
||||||
|
|
||||||
|
- Snapshot, Erzeugungszeit, Integrität, Maschine und Collector-Identität
|
||||||
|
werden unverändert strikt geprüft.
|
||||||
|
- Alle neun stabilen Services müssen eindeutig vorhanden sein.
|
||||||
|
- Bei aktivierter Endpoint-Prüfung muss der Katalog weiterhin im gestarteten
|
||||||
|
Abnahmelauf erfolgreich synchronisiert worden sein.
|
||||||
|
- `UNKNOWN` in einem Kernservice bleibt ein Abnahmefehler.
|
||||||
|
- Ausschließlich `BizTalk Endpoint Reachability = UNKNOWN` wird als
|
||||||
|
nicht blockierende Betriebswarnung akzeptiert.
|
||||||
|
- Die vollständige UNKNOWN-Zeile erscheint in Setup-Ausgabe und Setup-Log und
|
||||||
|
bleibt unverändert im Snapshot für Checkmk erhalten.
|
||||||
|
|
||||||
|
Damit wird die Installation abgeschlossen, ohne einen ungeklärten
|
||||||
|
Monitoringzustand grünzufärben. Der normale Minutentask läuft weiter und die
|
||||||
|
Endpoint-Ursache kann anhand von `unresolved_endpoints`,
|
||||||
|
`catalog_or_probe_error`, `biztalk_endpoints_unresolved`, Katalog und
|
||||||
|
Provider-Log gezielt bearbeitet werden.
|
||||||
|
|
||||||
|
## Weiteres Vorgehen in ACC
|
||||||
|
|
||||||
|
1. Setup 2.2.6 als Update ausführen und die ausgegebene
|
||||||
|
`RUNTIME_VALIDATION_WARNING` sichern.
|
||||||
|
2. Im Snapshot die vollständige Zeile `BizTalk Endpoint Reachability` prüfen.
|
||||||
|
3. Bei `unresolved_endpoints` Adapter, Artefakt und Grund auswerten und nur
|
||||||
|
bei einem tatsächlich externen Ziel einen geheimnisfreien manuellen
|
||||||
|
Override in `endpoints.xml` ergänzen.
|
||||||
|
4. Bei `catalog_or_probe_error` zuerst Provider-Log und Katalog-XML prüfen.
|
||||||
|
5. Nach der Bereinigung zwei Minutenläufe und den Checkmk-Agent-Dump prüfen.
|
||||||
|
|
||||||
|
## Regressionstests
|
||||||
|
|
||||||
|
- Ein isoliertes Endpoint-Reachability-`UNKNOWN` wird als Warnung akzeptiert
|
||||||
|
und sein vollständiger Detailtext bleibt erhalten.
|
||||||
|
- `BizTalk Platform = UNKNOWN` bleibt blockierend.
|
||||||
|
- Fehlender Service, alter Snapshot, falsche Identität und alter Katalog
|
||||||
|
bleiben blockierend.
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Analyse: Update-Abbruch mit `Exitcode=0, Zeilen=8`
|
||||||
|
|
||||||
|
## Beobachtung
|
||||||
|
|
||||||
|
Der Installer meldete nach erfolgreicher Paketprüfung, Anmeldung und
|
||||||
|
Staging-Validierung:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Installation/Update fehlgeschlagen. Rollback erfolgreich.
|
||||||
|
Ursache: Self-Test fehlgeschlagen. Exitcode=0, Zeilen=8.
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Reihenfolge im Statusfenster zeigt, dass das neue Paket und das Staging
|
||||||
|
bereits erfolgreich neun Services geliefert hatten. Der danach ausgeführte
|
||||||
|
Self-Test gehörte zur noch installierten Ausgangsversion.
|
||||||
|
|
||||||
|
## Ursache
|
||||||
|
|
||||||
|
Version 2.2.4 verwendete dieselbe starre Erwartung von exakt neun Zeilen für
|
||||||
|
beide Seiten des Updatevergleichs. Eine ältere, intakte Installation vor
|
||||||
|
Einführung von `BizTalk Endpoint Reachability` liefert jedoch acht gültige
|
||||||
|
Checkmk-Zeilen und Exitcode 0. Der Updater deutete diese erwartete
|
||||||
|
Versionsdifferenz fälschlich als defekten Self-Test. Der Abbruch erfolgte vor
|
||||||
|
dem Stoppen des Tasks und vor dem Austausch von Dateien. Die Meldung
|
||||||
|
`Rollback erfolgreich` war deshalb technisch irreführend; es war keine
|
||||||
|
Systemänderung zurückzusetzen.
|
||||||
|
|
||||||
|
## Korrektur in Version 2.2.5
|
||||||
|
|
||||||
|
- Paket, Staging und aktivierte Zielversion müssen weiterhin exakt neun
|
||||||
|
eindeutige `OK`-Zeilen liefern.
|
||||||
|
- Die installierte Ausgangsversion darf eine andere positive Anzahl
|
||||||
|
eindeutiger `OK`-Zeilen liefern. Damit wird ihr tatsächlicher
|
||||||
|
versionsspezifischer Servicevertrag gelesen.
|
||||||
|
- Rein neu hinzukommende Services sind updatekompatibel. Sie werden als
|
||||||
|
Discovery-Hinweis gemeldet, blockieren aber die Umschaltung nicht.
|
||||||
|
- Entfernte Namen – und damit auch Renames – bleiben ohne aktivierte
|
||||||
|
Discovery-Bestätigung ein Sicherheitsstopp.
|
||||||
|
- Tritt ein Fehler vor der ersten Systemänderung auf, lautet der Status jetzt
|
||||||
|
`kein Rollback erforderlich`.
|
||||||
|
|
||||||
|
## Erweiterte Diagnose
|
||||||
|
|
||||||
|
Jeder Lauf erzeugt
|
||||||
|
`%ProgramData%\BizTalkCheckmkPulse\logs\setup-<UTC>-<PID>.log`. Das Log enthält:
|
||||||
|
|
||||||
|
- aktuelle Installationsphase und Setup-Version,
|
||||||
|
- Pfad, Dateiversion und Größe jeder geprüften EXE,
|
||||||
|
- erwartete und tatsächliche Zeilenzahl, Exitcode sowie vollständiges stdout
|
||||||
|
und stderr jedes Self-Tests,
|
||||||
|
- Task-Scheduler-State und `LastTaskResult` dezimal und hexadezimal,
|
||||||
|
- vollständige Exception-Kette mit HRESULT und Stacktrace,
|
||||||
|
- bei Runtime-Abnahmefehlern bis zu 40 letzte Provider-Logzeilen,
|
||||||
|
- Ergebnis jedes Rollback-Schritts.
|
||||||
|
|
||||||
|
Das Kennwort wird ausschließlich an `LogonUser` und die
|
||||||
|
Task-Scheduler-COM-Schnittstelle übergeben. Die Logging-Komponente erhält es
|
||||||
|
nicht.
|
||||||
|
|
||||||
|
## Regressionstests
|
||||||
|
|
||||||
|
Automatisiert geprüft werden insbesondere:
|
||||||
|
|
||||||
|
- acht gültige Zeilen einer Legacy-Ausgangsversion werden akzeptiert,
|
||||||
|
- acht Zeilen eines neuen Stagings werden weiterhin abgewiesen,
|
||||||
|
- eine reine Service-Erweiterung wird ohne Rename-Bestätigung akzeptiert,
|
||||||
|
- ein frisch beendeter Task mit Fehlercode wird sofort erkannt und der Code
|
||||||
|
enthält zusätzlich die hexadezimale Darstellung.
|
||||||
@@ -53,6 +53,29 @@ Verbindlicher Agent-Dump:
|
|||||||
|
|
||||||
## Service Discovery
|
## Service Discovery
|
||||||
|
|
||||||
|
Das Setup ab 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 vorliegen. `UNKNOWN` in einem Kernservice
|
||||||
|
bleibt blockierend. Ab Version 2.2.6 wird ein isoliertes
|
||||||
|
`BizTalk Endpoint Reachability = UNKNOWN` als fachliche Betriebswarnung
|
||||||
|
akzeptiert, sofern Snapshot, Identität und frisch synchronisierter Katalog
|
||||||
|
valide sind. Die UNKNOWN-Zeile bleibt in Checkmk sichtbar. 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.
|
||||||
|
|
||||||
|
Ab Setup 2.2.5 darf der gültige Servicevertrag der installierten
|
||||||
|
Ausgangsversion weniger als neun Services enthalten. So ist das Update einer
|
||||||
|
Legacy-Version mit acht Services möglich; nur Paket, Staging und Zielversion
|
||||||
|
müssen exakt neun Services liefern. Additive Services werden gemeldet, aber
|
||||||
|
nicht blockiert. Entfernte oder umbenannte Services erfordern weiterhin die
|
||||||
|
bewusste Bestätigung im Setup. Laufbezogene Details stehen in
|
||||||
|
`%ProgramData%\BizTalkCheckmkPulse\logs\setup-*.log`.
|
||||||
|
|
||||||
1. Provider dreimal erfolgreich laufen lassen.
|
1. Provider dreimal erfolgreich laufen lassen.
|
||||||
2. `LastTaskResult=0`, frischen Snapshot und Log prüfen.
|
2. `LastTaskResult=0`, frischen Snapshot und Log prüfen.
|
||||||
3. Agent-Dump im `LocalSystem`-Kontext prüfen.
|
3. Agent-Dump im `LocalSystem`-Kontext prüfen.
|
||||||
@@ -109,6 +132,9 @@ Soll:
|
|||||||
- Receive Locations und Send Ports zeigen getrennte expected/unexpected Werte.
|
- Receive Locations und Send Ports zeigen getrennte expected/unexpected Werte.
|
||||||
- Endpoint Reachability zeigt bei Erfolg nur die Gesamtzahl und bei Fehlern
|
- Endpoint Reachability zeigt bei Erfolg nur die Gesamtzahl und bei Fehlern
|
||||||
ausschließlich nicht erreichbare oder wirklich unaufgelöste Ziele.
|
ausschließlich nicht erreichbare oder wirklich unaufgelöste Ziele.
|
||||||
|
- Schedule ist vollständig ausgenommen; Oracle-DATABASE und WCF-SAP laufen
|
||||||
|
als TCP-Best-Effort und können keinen Alarm auslösen. Die zugehörigen
|
||||||
|
Erfolgs-/Ignoriert-Zähler bleiben für die Betriebsanalyse sichtbar.
|
||||||
- keine berechtigungsbedingten `UNKNOWN`-Services.
|
- keine berechtigungsbedingten `UNKNOWN`-Services.
|
||||||
|
|
||||||
## Alarmierung der Transportkette
|
## Alarmierung der Transportkette
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# RTM-Adapterregeln für Endpoint Reachability
|
||||||
|
|
||||||
|
## Anlass
|
||||||
|
|
||||||
|
Die ACC-Ausgabe vom 11.08.2026 zeigte `BizTalk Endpoint Reachability =
|
||||||
|
UNKNOWN`, obwohl die eigentlichen Socket-Probes in wenigen Millisekunden
|
||||||
|
abgeschlossen waren. Die Detailausgabe bestand überwiegend aus drei
|
||||||
|
Adapterklassen, die der generische URI-Parser nicht fachgerecht einordnen
|
||||||
|
konnte:
|
||||||
|
|
||||||
|
- Scheduling-Komponenten ohne externes Netzwerkziel,
|
||||||
|
- kundenspezifische `DATABASE`-Adressen, nahezu ausschließlich Oracle,
|
||||||
|
- WCF-SAP-Verbindungs-URIs mit Gateway- und SAProuter-Parametern.
|
||||||
|
|
||||||
|
Diese Fälle dürfen den RTM-Service weder mit Auflösungslücken noch mit
|
||||||
|
Fehlalarmen belasten.
|
||||||
|
|
||||||
|
## Verbindliche Klassifizierung ab Version 2.2.7
|
||||||
|
|
||||||
|
### Scheduling
|
||||||
|
|
||||||
|
Adaptername oder Adresse mit `Schedule`/`Scheduler` wird vollständig als
|
||||||
|
erwartbar ausgenommen. Es erfolgt keine Netzwerkprobe und kein UNKNOWN.
|
||||||
|
|
||||||
|
### DATABASE / Oracle
|
||||||
|
|
||||||
|
Nur die eindeutig erkennbare Form
|
||||||
|
|
||||||
|
```text
|
||||||
|
Database://host[:port]/.../Oracle
|
||||||
|
```
|
||||||
|
|
||||||
|
wird automatisch aufgelöst. Ein expliziter Port gewinnt; andernfalls wird
|
||||||
|
TCP 1521 verwendet. Andere DATABASE-Anbieter oder nicht eindeutig lesbare
|
||||||
|
Adressen werden erwartbar ausgenommen. Es wird kein UDP-Test erzeugt.
|
||||||
|
|
||||||
|
Oracle dokumentiert TCP 1521 als Standardadresse des Listeners und nimmt bei
|
||||||
|
fehlender Portangabe im Easy-Connect-Format ebenfalls 1521 an:
|
||||||
|
|
||||||
|
- [Oracle: Configuring the Network Environment](https://docs.oracle.com/en/database/oracle/oracle-database/19/admqs/configuring-the-network-environment.html)
|
||||||
|
- [Oracle: Configuring and Administering Oracle Net Listener](https://docs.oracle.com/en/database/oracle/oracle-database/21/netag/configuring-and-administering-oracle-net-listener.html)
|
||||||
|
|
||||||
|
### WCF-SAP
|
||||||
|
|
||||||
|
Microsoft dokumentiert `ListenerGwHost`, `ListenerGwServ` und `SAPROUTER` als
|
||||||
|
Bestandteile der SAP-Verbindungs-URI. Der Parser verwendet in dieser
|
||||||
|
Reihenfolge:
|
||||||
|
|
||||||
|
1. erster SAProuter-Hop aus `/H/host/S/port`,
|
||||||
|
2. `ListenerGwHost` und `ListenerGwServ`,
|
||||||
|
3. `GwHost` und `GwServ` als Fallback.
|
||||||
|
|
||||||
|
Numerische Dienste werden direkt übernommen. `sapgwNN` wird auf TCP `33NN`
|
||||||
|
abgebildet. Fehlt bei einem SAProuter-Hop `/S/`, gilt der dokumentierte
|
||||||
|
Standard TCP 3299. URI-Escapes werden vor der Auswertung dekodiert; Kennwörter,
|
||||||
|
Program-ID, Pfade und Querystrings gelangen weiterhin nicht in Katalog, Log
|
||||||
|
oder Checkmk-Ausgabe.
|
||||||
|
|
||||||
|
Quellen:
|
||||||
|
|
||||||
|
- [Microsoft Learn: Create the SAP system connection URI](https://learn.microsoft.com/en-us/biztalk/adapters-and-accelerators/adapter-sap/create-the-sap-system-connection-uri)
|
||||||
|
- [SAP Help: Entries in the Services File](https://help.sap.com/docs/SLTOOLSET/6bc963751d0840f996c5c9d7a7f83473/bb9faaebe0c74a58bbcc259c83f59437.html)
|
||||||
|
- [SAP Help: SAProuter Host and Port](https://help.sap.com/docs/SAP_NETWEAVER_AS_ABAP_FOR_SOH_740/e245703406684d8a81812f4c6334eb2f/486b5b0cb74c07bee10000000a42189d.html)
|
||||||
|
|
||||||
|
## Best-Effort statt Alarm
|
||||||
|
|
||||||
|
Automatisch abgeleitete DATABASE-/WCF-SAP-Ziele erhalten im lokalen Katalog
|
||||||
|
`bestEffort="true"`. Sie werden normal dedupliziert und per TCP verbunden.
|
||||||
|
|
||||||
|
- Erfolg erhöht die getesteten und verfügbaren Zähler.
|
||||||
|
- Timeout, DNS- oder Verbindungsfehler erhöht
|
||||||
|
`biztalk_endpoints_best_effort_ignored`.
|
||||||
|
- Der Fehler erscheint nicht als `unavailable=`, erzeugt keinen Warn-Logeintrag
|
||||||
|
pro Minute und ändert den Checkmk-Status nicht.
|
||||||
|
- Eine nicht ableitbare Adapteradresse zählt zu
|
||||||
|
`biztalk_endpoints_expected_non_socket`, nicht zu `unresolved`.
|
||||||
|
|
||||||
|
Damit erfüllt der Check die gewünschte „versuchen, sonst ignorieren“-Semantik.
|
||||||
|
Soll ein solches Ziel später verbindlich alarmieren, wird ein manueller
|
||||||
|
Katalogeintrag mit `autoDiscovered="false"` und ohne
|
||||||
|
`bestEffort="true"` hinterlegt. Reguläre automatisch erkannte Ziele bleiben
|
||||||
|
ebenfalls streng: Ihre Nichterreichbarkeit ist weiterhin CRIT.
|
||||||
|
|
||||||
|
## Regression und Abnahme
|
||||||
|
|
||||||
|
Die automatisierten Tests decken Scheduler-Ausschluss, Oracle-Standard- und
|
||||||
|
Explizitport, nicht unterstützte DATABASE-Anbieter, WCF-SAP-Gateway,
|
||||||
|
URI-kodierten SAProuter, zielbasierte SAP-Konfiguration ohne ableitbaren Host,
|
||||||
|
Geheimnisschutz und die nicht alarmierende Best-Effort-Ausgabe ab.
|
||||||
|
|
||||||
|
Für die Windows-/ACC-Abnahme nach Installation von 2.2.7:
|
||||||
|
|
||||||
|
1. Im Provider-Log muss der erzwungene Katalogabgleich erfolgreich sein.
|
||||||
|
2. `BizTalk Endpoint Reachability` darf wegen Schedule, DATABASE oder WCF-SAP
|
||||||
|
nicht mehr UNKNOWN werden.
|
||||||
|
3. `biztalk_endpoints_unresolved` darf diese drei Klassen nicht enthalten.
|
||||||
|
4. `biztalk_endpoints_best_effort` zeigt die tatsächlich abgeleiteten Oracle-
|
||||||
|
und SAP-Ziele; nicht erreichbare davon erscheinen nur in
|
||||||
|
`biztalk_endpoints_best_effort_ignored`.
|
||||||
|
5. Ein absichtlich nicht erreichbares reguläres Testziel muss weiterhin CRIT
|
||||||
|
auslösen, damit die strenge Standardalarmierung belegt bleibt.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 233 KiB |
@@ -0,0 +1,587 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Generate the external BizTalk Checkmk Pulse architecture overview as DOCX."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from docx import Document
|
||||||
|
from docx.enum.section import WD_SECTION
|
||||||
|
from docx.enum.table import WD_ALIGN_VERTICAL, WD_CELL_VERTICAL_ALIGNMENT, WD_TABLE_ALIGNMENT
|
||||||
|
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK
|
||||||
|
from docx.oxml import OxmlElement
|
||||||
|
from docx.oxml.ns import qn
|
||||||
|
from docx.shared import Cm, Inches, Pt, RGBColor
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
DOCS = ROOT / "docs"
|
||||||
|
OUTPUT = DOCS / "BizTalk_Checkmk_Pulse_Architekturueberblick.docx"
|
||||||
|
ARCHITECTURE_IMAGE = DOCS / "architecture.png"
|
||||||
|
|
||||||
|
NAVY = "17365D"
|
||||||
|
BLUE = "24547C"
|
||||||
|
MID_BLUE = "2B6F9F"
|
||||||
|
LIGHT_BLUE = "EEF6FB"
|
||||||
|
LIGHTER_BLUE = "F4F7FA"
|
||||||
|
GREEN = "16845B"
|
||||||
|
LIGHT_GREEN = "ECFDF5"
|
||||||
|
ORANGE = "DD6B20"
|
||||||
|
LIGHT_ORANGE = "FFF7ED"
|
||||||
|
TEXT = "1F2937"
|
||||||
|
MUTED = "5B6770"
|
||||||
|
GRID = "CBD5E1"
|
||||||
|
WHITE = "FFFFFF"
|
||||||
|
|
||||||
|
|
||||||
|
def set_cell_shading(cell, fill):
|
||||||
|
tc_pr = cell._tc.get_or_add_tcPr()
|
||||||
|
shd = tc_pr.find(qn("w:shd"))
|
||||||
|
if shd is None:
|
||||||
|
shd = OxmlElement("w:shd")
|
||||||
|
tc_pr.append(shd)
|
||||||
|
shd.set(qn("w:fill"), fill)
|
||||||
|
|
||||||
|
|
||||||
|
def set_cell_margins(cell, top=120, start=120, bottom=120, end=120):
|
||||||
|
tc = cell._tc
|
||||||
|
tc_pr = tc.get_or_add_tcPr()
|
||||||
|
tc_mar = tc_pr.first_child_found_in("w:tcMar")
|
||||||
|
if tc_mar is None:
|
||||||
|
tc_mar = OxmlElement("w:tcMar")
|
||||||
|
tc_pr.append(tc_mar)
|
||||||
|
for margin, value in (("top", top), ("start", start), ("bottom", bottom), ("end", end)):
|
||||||
|
node = tc_mar.find(qn(f"w:{margin}"))
|
||||||
|
if node is None:
|
||||||
|
node = OxmlElement(f"w:{margin}")
|
||||||
|
tc_mar.append(node)
|
||||||
|
node.set(qn("w:w"), str(value))
|
||||||
|
node.set(qn("w:type"), "dxa")
|
||||||
|
|
||||||
|
|
||||||
|
def set_repeat_table_header(row):
|
||||||
|
tr_pr = row._tr.get_or_add_trPr()
|
||||||
|
repeat = OxmlElement("w:tblHeader")
|
||||||
|
repeat.set(qn("w:val"), "true")
|
||||||
|
tr_pr.append(repeat)
|
||||||
|
|
||||||
|
|
||||||
|
def prevent_row_split(row):
|
||||||
|
tr_pr = row._tr.get_or_add_trPr()
|
||||||
|
cant_split = OxmlElement("w:cantSplit")
|
||||||
|
tr_pr.append(cant_split)
|
||||||
|
|
||||||
|
|
||||||
|
def set_table_fixed(table):
|
||||||
|
table.autofit = False
|
||||||
|
tbl_pr = table._tbl.tblPr
|
||||||
|
layout = tbl_pr.find(qn("w:tblLayout"))
|
||||||
|
if layout is None:
|
||||||
|
layout = OxmlElement("w:tblLayout")
|
||||||
|
tbl_pr.append(layout)
|
||||||
|
layout.set(qn("w:type"), "fixed")
|
||||||
|
|
||||||
|
|
||||||
|
def set_paragraph_shading(paragraph, fill, border=None):
|
||||||
|
p_pr = paragraph._p.get_or_add_pPr()
|
||||||
|
shd = OxmlElement("w:shd")
|
||||||
|
shd.set(qn("w:fill"), fill)
|
||||||
|
p_pr.append(shd)
|
||||||
|
if border:
|
||||||
|
p_bdr = OxmlElement("w:pBdr")
|
||||||
|
left = OxmlElement("w:left")
|
||||||
|
left.set(qn("w:val"), "single")
|
||||||
|
left.set(qn("w:sz"), "20")
|
||||||
|
left.set(qn("w:space"), "8")
|
||||||
|
left.set(qn("w:color"), border)
|
||||||
|
p_bdr.append(left)
|
||||||
|
p_pr.append(p_bdr)
|
||||||
|
|
||||||
|
|
||||||
|
def add_bottom_border(paragraph, color="8FB8DC"):
|
||||||
|
p_pr = paragraph._p.get_or_add_pPr()
|
||||||
|
p_bdr = OxmlElement("w:pBdr")
|
||||||
|
bottom = OxmlElement("w:bottom")
|
||||||
|
bottom.set(qn("w:val"), "single")
|
||||||
|
bottom.set(qn("w:sz"), "8")
|
||||||
|
bottom.set(qn("w:space"), "5")
|
||||||
|
bottom.set(qn("w:color"), color)
|
||||||
|
p_bdr.append(bottom)
|
||||||
|
p_pr.append(p_bdr)
|
||||||
|
|
||||||
|
|
||||||
|
def set_keep(paragraph, keep_next=False, keep_lines=True):
|
||||||
|
p_pr = paragraph._p.get_or_add_pPr()
|
||||||
|
if keep_next:
|
||||||
|
p_pr.append(OxmlElement("w:keepNext"))
|
||||||
|
if keep_lines:
|
||||||
|
p_pr.append(OxmlElement("w:keepLines"))
|
||||||
|
|
||||||
|
|
||||||
|
def add_page_field(paragraph):
|
||||||
|
run = paragraph.add_run()
|
||||||
|
fld_char_1 = OxmlElement("w:fldChar")
|
||||||
|
fld_char_1.set(qn("w:fldCharType"), "begin")
|
||||||
|
instr_text = OxmlElement("w:instrText")
|
||||||
|
instr_text.set(qn("xml:space"), "preserve")
|
||||||
|
instr_text.text = " PAGE "
|
||||||
|
fld_char_2 = OxmlElement("w:fldChar")
|
||||||
|
fld_char_2.set(qn("w:fldCharType"), "end")
|
||||||
|
run._r.extend((fld_char_1, instr_text, fld_char_2))
|
||||||
|
|
||||||
|
|
||||||
|
def format_run(run, *, size=None, bold=None, color=None, italic=None, font="Liberation Sans"):
|
||||||
|
run.font.name = font
|
||||||
|
run._element.rPr.rFonts.set(qn("w:eastAsia"), font)
|
||||||
|
if size is not None:
|
||||||
|
run.font.size = Pt(size)
|
||||||
|
if bold is not None:
|
||||||
|
run.bold = bold
|
||||||
|
if color:
|
||||||
|
run.font.color.rgb = RGBColor.from_string(color)
|
||||||
|
if italic is not None:
|
||||||
|
run.italic = italic
|
||||||
|
return run
|
||||||
|
|
||||||
|
|
||||||
|
def add_rich_paragraph(document, parts, *, style=None, before=0, after=5, align=None):
|
||||||
|
paragraph = document.add_paragraph(style=style)
|
||||||
|
paragraph.paragraph_format.space_before = Pt(before)
|
||||||
|
paragraph.paragraph_format.space_after = Pt(after)
|
||||||
|
paragraph.paragraph_format.line_spacing = 1.12
|
||||||
|
if align is not None:
|
||||||
|
paragraph.alignment = align
|
||||||
|
for text, options in parts:
|
||||||
|
run = paragraph.add_run(text)
|
||||||
|
format_run(run, **options)
|
||||||
|
return paragraph
|
||||||
|
|
||||||
|
|
||||||
|
def add_body(document, text, *, bold_lead=None, after=5):
|
||||||
|
paragraph = document.add_paragraph()
|
||||||
|
paragraph.paragraph_format.space_after = Pt(after)
|
||||||
|
paragraph.paragraph_format.line_spacing = 1.13
|
||||||
|
if bold_lead and text.startswith(bold_lead):
|
||||||
|
format_run(paragraph.add_run(bold_lead), bold=True, color=TEXT)
|
||||||
|
format_run(paragraph.add_run(text[len(bold_lead):]), color=TEXT)
|
||||||
|
else:
|
||||||
|
format_run(paragraph.add_run(text), color=TEXT)
|
||||||
|
return paragraph
|
||||||
|
|
||||||
|
|
||||||
|
def add_heading(document, text, level=1):
|
||||||
|
paragraph = document.add_paragraph()
|
||||||
|
paragraph.paragraph_format.space_before = Pt(12 if level == 1 else 8)
|
||||||
|
paragraph.paragraph_format.space_after = Pt(6 if level == 1 else 4)
|
||||||
|
size = 17 if level == 1 else 12
|
||||||
|
format_run(paragraph.add_run(text), size=size, bold=True, color=NAVY)
|
||||||
|
if level == 1:
|
||||||
|
add_bottom_border(paragraph)
|
||||||
|
set_keep(paragraph, keep_next=True)
|
||||||
|
return paragraph
|
||||||
|
|
||||||
|
|
||||||
|
def add_bullet(document, text, *, level=0):
|
||||||
|
paragraph = document.add_paragraph(style="List Bullet" if level == 0 else "List Bullet 2")
|
||||||
|
paragraph.paragraph_format.left_indent = Cm(0.65 + 0.45 * level)
|
||||||
|
paragraph.paragraph_format.first_line_indent = Cm(-0.25)
|
||||||
|
paragraph.paragraph_format.space_after = Pt(2.5)
|
||||||
|
paragraph.paragraph_format.line_spacing = 1.08
|
||||||
|
format_run(paragraph.add_run(text), color=TEXT)
|
||||||
|
return paragraph
|
||||||
|
|
||||||
|
|
||||||
|
def add_numbered(document, text):
|
||||||
|
paragraph = document.add_paragraph(style="List Number")
|
||||||
|
paragraph.paragraph_format.left_indent = Cm(0.7)
|
||||||
|
paragraph.paragraph_format.first_line_indent = Cm(-0.3)
|
||||||
|
paragraph.paragraph_format.space_after = Pt(3)
|
||||||
|
paragraph.paragraph_format.line_spacing = 1.08
|
||||||
|
format_run(paragraph.add_run(text), color=TEXT)
|
||||||
|
return paragraph
|
||||||
|
|
||||||
|
|
||||||
|
def add_callout(document, label, text, *, fill=LIGHT_BLUE, border=MID_BLUE):
|
||||||
|
table = document.add_table(rows=1, cols=1)
|
||||||
|
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||||||
|
table.autofit = False
|
||||||
|
cell = table.cell(0, 0)
|
||||||
|
cell.width = Cm(17.2)
|
||||||
|
set_cell_shading(cell, fill)
|
||||||
|
set_cell_margins(cell, top=170, start=230, bottom=170, end=230)
|
||||||
|
paragraph = cell.paragraphs[0]
|
||||||
|
paragraph.paragraph_format.space_after = Pt(0)
|
||||||
|
paragraph.paragraph_format.line_spacing = 1.12
|
||||||
|
format_run(paragraph.add_run(label), bold=True, color=border)
|
||||||
|
format_run(paragraph.add_run(text), color=TEXT)
|
||||||
|
table.rows[0]._tr.get_or_add_trPr().append(OxmlElement("w:cantSplit"))
|
||||||
|
after = document.add_paragraph()
|
||||||
|
after.paragraph_format.space_after = Pt(1)
|
||||||
|
return table
|
||||||
|
|
||||||
|
|
||||||
|
def add_table(document, headers, rows, widths, *, font_size=8.7):
|
||||||
|
table = document.add_table(rows=1, cols=len(headers))
|
||||||
|
table.style = "Table Grid"
|
||||||
|
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||||||
|
set_table_fixed(table)
|
||||||
|
header = table.rows[0]
|
||||||
|
set_repeat_table_header(header)
|
||||||
|
prevent_row_split(header)
|
||||||
|
for index, title in enumerate(headers):
|
||||||
|
cell = header.cells[index]
|
||||||
|
cell.width = Cm(widths[index])
|
||||||
|
cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER
|
||||||
|
set_cell_shading(cell, BLUE)
|
||||||
|
set_cell_margins(cell, top=100, start=110, bottom=100, end=110)
|
||||||
|
p = cell.paragraphs[0]
|
||||||
|
p.paragraph_format.space_after = Pt(0)
|
||||||
|
format_run(p.add_run(title), bold=True, color=WHITE, size=font_size)
|
||||||
|
for row_index, values in enumerate(rows):
|
||||||
|
row = table.add_row()
|
||||||
|
prevent_row_split(row)
|
||||||
|
for col_index, value in enumerate(values):
|
||||||
|
cell = row.cells[col_index]
|
||||||
|
cell.width = Cm(widths[col_index])
|
||||||
|
cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.TOP
|
||||||
|
set_cell_margins(cell, top=90, start=110, bottom=90, end=110)
|
||||||
|
if row_index % 2 == 1:
|
||||||
|
set_cell_shading(cell, LIGHTER_BLUE)
|
||||||
|
p = cell.paragraphs[0]
|
||||||
|
p.paragraph_format.space_after = Pt(0)
|
||||||
|
p.paragraph_format.line_spacing = 1.03
|
||||||
|
format_run(p.add_run(str(value)), color=TEXT, size=font_size)
|
||||||
|
spacer = document.add_paragraph()
|
||||||
|
spacer.paragraph_format.space_after = Pt(1)
|
||||||
|
return table
|
||||||
|
|
||||||
|
|
||||||
|
def add_code_block(document, lines):
|
||||||
|
paragraph = document.add_paragraph()
|
||||||
|
paragraph.paragraph_format.left_indent = Cm(0.15)
|
||||||
|
paragraph.paragraph_format.right_indent = Cm(0.15)
|
||||||
|
paragraph.paragraph_format.space_before = Pt(3)
|
||||||
|
paragraph.paragraph_format.space_after = Pt(7)
|
||||||
|
paragraph.paragraph_format.line_spacing = 1.0
|
||||||
|
set_paragraph_shading(paragraph, "F3F6F8", MID_BLUE)
|
||||||
|
for index, line in enumerate(lines):
|
||||||
|
run = paragraph.add_run(line)
|
||||||
|
format_run(run, font="Liberation Mono", size=7.8, color="0F3E5E")
|
||||||
|
if index < len(lines) - 1:
|
||||||
|
run.add_break()
|
||||||
|
set_keep(paragraph)
|
||||||
|
return paragraph
|
||||||
|
|
||||||
|
|
||||||
|
def configure_document(document):
|
||||||
|
section = document.sections[0]
|
||||||
|
section.page_width = Cm(21.0)
|
||||||
|
section.page_height = Cm(29.7)
|
||||||
|
section.top_margin = Cm(1.55)
|
||||||
|
section.bottom_margin = Cm(1.55)
|
||||||
|
section.left_margin = Cm(1.7)
|
||||||
|
section.right_margin = Cm(1.7)
|
||||||
|
section.header_distance = Cm(0.65)
|
||||||
|
section.footer_distance = Cm(0.65)
|
||||||
|
section.different_first_page_header_footer = True
|
||||||
|
|
||||||
|
styles = document.styles
|
||||||
|
normal = styles["Normal"]
|
||||||
|
normal.font.name = "Liberation Sans"
|
||||||
|
normal._element.rPr.rFonts.set(qn("w:eastAsia"), "Liberation Sans")
|
||||||
|
normal.font.size = Pt(9.6)
|
||||||
|
normal.font.color.rgb = RGBColor.from_string(TEXT)
|
||||||
|
normal.paragraph_format.space_after = Pt(4)
|
||||||
|
|
||||||
|
for name in ("List Bullet", "List Bullet 2", "List Number"):
|
||||||
|
styles[name].font.name = "Liberation Sans"
|
||||||
|
styles[name]._element.rPr.rFonts.set(qn("w:eastAsia"), "Liberation Sans")
|
||||||
|
styles[name].font.size = Pt(9.3)
|
||||||
|
|
||||||
|
header = section.header
|
||||||
|
p = header.paragraphs[0]
|
||||||
|
p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
|
||||||
|
p.paragraph_format.space_after = Pt(0)
|
||||||
|
format_run(p.add_run("BizTalk Checkmk Pulse | Architektur- und Lösungsüberblick"), size=7.5, color=MUTED)
|
||||||
|
|
||||||
|
footer = section.footer
|
||||||
|
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.6 | Stand 11.08.2026 | Seite "), size=7.5, color=MUTED)
|
||||||
|
add_page_field(p)
|
||||||
|
|
||||||
|
|
||||||
|
def build_document():
|
||||||
|
if not ARCHITECTURE_IMAGE.exists():
|
||||||
|
raise FileNotFoundError(f"Architecture image not found: {ARCHITECTURE_IMAGE}")
|
||||||
|
|
||||||
|
document = Document()
|
||||||
|
configure_document(document)
|
||||||
|
core = document.core_properties
|
||||||
|
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.6"
|
||||||
|
|
||||||
|
# Cover
|
||||||
|
banner = document.add_table(rows=1, cols=1)
|
||||||
|
banner.autofit = False
|
||||||
|
banner.cell(0, 0).width = Cm(17.3)
|
||||||
|
set_cell_shading(banner.cell(0, 0), NAVY)
|
||||||
|
set_cell_margins(banner.cell(0, 0), top=170, start=170, bottom=170, end=170)
|
||||||
|
p = banner.cell(0, 0).paragraphs[0]
|
||||||
|
p.paragraph_format.space_after = Pt(0)
|
||||||
|
format_run(p.add_run("LÖSUNGSÜBERSICHT · EXTERNE DARSTELLUNG"), bold=True, color=WHITE, size=9)
|
||||||
|
|
||||||
|
p = document.add_paragraph()
|
||||||
|
p.paragraph_format.space_before = Pt(32)
|
||||||
|
p.paragraph_format.space_after = Pt(10)
|
||||||
|
format_run(p.add_run("BizTalk Checkmk Pulse"), size=28, bold=True, color=NAVY)
|
||||||
|
p = document.add_paragraph()
|
||||||
|
p.paragraph_format.space_after = Pt(26)
|
||||||
|
format_run(p.add_run("Architektur, Funktionsumfang und Datenaustausch des lokalen Checkmk-Monitorings für Microsoft BizTalk Server"), size=15, color="4B6478")
|
||||||
|
|
||||||
|
add_callout(
|
||||||
|
document,
|
||||||
|
"",
|
||||||
|
"Die Lösung übersetzt den technischen Laufzeitzustand einer BizTalk-Umgebung in neun kompakte Checkmk-Services. Ein dediziertes Konto erfasst die benötigten Daten; der Checkmk-Agent selbst erhält keine BizTalk- oder SQL-Berechtigungen.",
|
||||||
|
fill=BLUE,
|
||||||
|
border=WHITE,
|
||||||
|
)
|
||||||
|
# Callout helper uses dark text; force white on the cover callout.
|
||||||
|
for run in document.tables[-1].cell(0, 0).paragraphs[0].runs:
|
||||||
|
run.font.color.rgb = RGBColor.from_string(WHITE)
|
||||||
|
run.font.size = Pt(12.5)
|
||||||
|
|
||||||
|
facts = document.add_table(rows=1, cols=4)
|
||||||
|
facts.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||||||
|
set_table_fixed(facts)
|
||||||
|
for cell, value, label in zip(
|
||||||
|
facts.rows[0].cells,
|
||||||
|
("9", "1 min", "180 s", "Read-only"),
|
||||||
|
("stabile Services", "Sammelintervall", "Frischegrenze", "BizTalk-Zugriff"),
|
||||||
|
):
|
||||||
|
cell.width = Cm(4.3)
|
||||||
|
set_cell_shading(cell, "EAF2F8")
|
||||||
|
set_cell_margins(cell, top=180, start=80, bottom=180, end=80)
|
||||||
|
p = cell.paragraphs[0]
|
||||||
|
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
p.paragraph_format.space_after = Pt(2)
|
||||||
|
format_run(p.add_run(value), size=15, bold=True, color=NAVY)
|
||||||
|
p = cell.add_paragraph()
|
||||||
|
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
p.paragraph_format.space_after = Pt(0)
|
||||||
|
format_run(p.add_run(label), size=8.3, color=TEXT)
|
||||||
|
|
||||||
|
document.add_paragraph().paragraph_format.space_after = Pt(3)
|
||||||
|
add_table(
|
||||||
|
document,
|
||||||
|
("Dokumentmerkmal", "Angabe"),
|
||||||
|
(
|
||||||
|
("Lösungsstand", "BizTalk Checkmk Pulse 2.2.6"),
|
||||||
|
("Zielplattform", "Microsoft BizTalk Server 2020 · Windows Server 2019 · Checkmk 2.4"),
|
||||||
|
("Dokumentstand", "11. August 2026"),
|
||||||
|
("Dokumentzweck", "Architektur- und Leistungsüberblick für technische Stakeholder"),
|
||||||
|
),
|
||||||
|
(4.2, 13.0),
|
||||||
|
font_size=8.8,
|
||||||
|
)
|
||||||
|
add_rich_paragraph(
|
||||||
|
document,
|
||||||
|
(("Dieses Dokument beschreibt den implementierten Lösungsstand. Umgebungsspezifische Parameter und die jeweilige Betriebsfreigabe werden im Rollout bestätigt.", {"size": 8.2, "color": MUTED}),),
|
||||||
|
before=10,
|
||||||
|
after=0,
|
||||||
|
)
|
||||||
|
document.add_page_break()
|
||||||
|
|
||||||
|
# 1–2: Summary and Checkmk overview
|
||||||
|
add_heading(document, "1. Zusammenfassung")
|
||||||
|
add_body(document, "BizTalk Checkmk Pulse ergänzt das zentrale Monitoring um eine BizTalk-spezifische Sicht. Die Lösung erkennt typische Betriebsstörungen – zum Beispiel suspendierte Instanzen, gestoppte Host Instances, unerwartet inaktive Ports, nicht erreichbare externe Ziele oder aktuelle BizTalk-Ereignisse – und stellt sie als eigenständige Services in Checkmk dar.")
|
||||||
|
add_body(document, "Die Architektur folgt einer klaren Aufgabentrennung:")
|
||||||
|
add_numbered(document, "Ein minütlicher Windows Scheduled Task sammelt unter einem dedizierten, eingeschränkt berechtigten AD-Servicekonto die BizTalk-Betriebsdaten.")
|
||||||
|
add_numbered(document, "Die Ergebnisse werden in einen kompakten, versionierten und integritätsgeprüften lokalen Snapshot geschrieben.")
|
||||||
|
add_numbered(document, "Der Checkmk Windows Agent läuft weiterhin als LocalSystem und liest ausschließlich diesen Snapshot.")
|
||||||
|
add_numbered(document, "Checkmk übernimmt Status, Metriken und Kurztexte in das zentrale Monitoring.")
|
||||||
|
add_callout(document, "Nutzen: ", "BizTalk-Störungen werden zentral sichtbar, ohne dem Checkmk-Agenten direkte BizTalk- oder SQL-Rechte zu erteilen. Die Ausgabe bleibt kompakt, graphfähig und alarmierbar.", fill=LIGHT_GREEN, border=GREEN)
|
||||||
|
|
||||||
|
add_heading(document, "2. Was ist Checkmk – und was ist ein Local Check?")
|
||||||
|
add_body(document, "Checkmk ist eine zentrale Monitoring-Plattform für IT-Infrastrukturen und Anwendungen. Auf überwachten Servern liefert ein Agent technische Zustände und Messwerte an die Checkmk-Instanz. Dort werden die Daten als Hosts und Services dargestellt, historisiert und für Dashboards, Schwellwerte und Benachrichtigungen verwendet.")
|
||||||
|
add_body(document, "Ein Local Check ist eine bewusst einfache Erweiterungsmöglichkeit des Checkmk-Agenten. Die Prüfung läuft auf dem Zielsystem und gibt pro Service eine Textzeile mit vier Bestandteilen aus:")
|
||||||
|
add_code_block(document, ('0 "BizTalk Send Ports" biztalk_send_ports_total=31;;;0|biztalk_send_ports_started=31;;;0', 'Alle Send Ports sind aktiv.'))
|
||||||
|
add_table(
|
||||||
|
document,
|
||||||
|
("Bestandteil", "Bedeutung"),
|
||||||
|
(
|
||||||
|
("0", "Status: 0 OK, 1 WARN, 2 CRIT, 3 UNKNOWN"),
|
||||||
|
('"BizTalk …"', "Eindeutiger Servicename, der in Checkmk angezeigt wird"),
|
||||||
|
("biztalk_…=Wert", "Metriken für Schwellwerte, Auswertungen und Zeitreihen"),
|
||||||
|
("Kurztext", "Lesbare Zusammenfassung; bei Fehlern mit begrenzter Liste betroffener Objekte"),
|
||||||
|
),
|
||||||
|
(4.0, 13.2),
|
||||||
|
)
|
||||||
|
add_body(document, "Der lokale Check dieser Lösung führt im Agentenpfad keine aufwendige BizTalk-Abfrage aus. Er startet lediglich den Consumer, validiert den vorbereiteten Snapshot und schreibt die bereits erzeugten Checkmk-Zeilen auf die Standardausgabe. Damit bleibt der Agentenaufruf schnell und seine Berechtigungsfläche klein.")
|
||||||
|
add_rich_paragraph(
|
||||||
|
document,
|
||||||
|
(
|
||||||
|
("Referenz: ", {"size": 8.0, "bold": True, "color": MUTED}),
|
||||||
|
("Checkmk User Guide, „Local checks“ – https://docs.checkmk.com/latest/en/localchecks.html", {"size": 8.0, "color": MID_BLUE}),
|
||||||
|
),
|
||||||
|
after=0,
|
||||||
|
)
|
||||||
|
document.add_page_break()
|
||||||
|
|
||||||
|
# 3: Architecture
|
||||||
|
add_heading(document, "3. Gesamtarchitektur")
|
||||||
|
p = document.add_paragraph()
|
||||||
|
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
p.paragraph_format.space_after = Pt(3)
|
||||||
|
run = p.add_run()
|
||||||
|
run.add_picture(str(ARCHITECTURE_IMAGE), width=Cm(16.9))
|
||||||
|
caption = document.add_paragraph()
|
||||||
|
caption.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
caption.paragraph_format.space_after = Pt(6)
|
||||||
|
format_run(caption.add_run("Abbildung 1: Trennung von privilegierter Datenerfassung, lokalem Datentransport und Checkmk-Consumer"), size=7.8, color=MUTED, italic=True)
|
||||||
|
add_table(
|
||||||
|
document,
|
||||||
|
("Baustein", "Aufgabe", "Berechtigungsprofil"),
|
||||||
|
(
|
||||||
|
("Scheduled Task", "Startet den Collector jede Minute und verhindert parallele Läufe.", "Dediziertes AD-Servicekonto, RunLevel Limited"),
|
||||||
|
("Collector (--collect)", "Liest BizTalk-WMI, prüft integrierten SQL-Zugriff, Event Log und aktive Endpunkte.", "BizTalk Read-only; kein lokaler Administrator, kein SQL-sysadmin"),
|
||||||
|
("Snapshot & Katalog", "Lokaler, ACL-geschützter Übergabepunkt zwischen Collector und Consumer.", "Collector schreibt; LocalSystem liest"),
|
||||||
|
("Consumer (--consume)", "Prüft Format, Maschine, Alter, Zeilenanzahl und SHA-256; gibt gültige Checkmk-Zeilen aus.", "LocalSystem; kein WMI-, SQL- oder Endpoint-Zugriff"),
|
||||||
|
("Checkmk", "Erkennt neun Services, übernimmt Metriken und löst regelbasiert Benachrichtigungen aus.", "Zentraler Monitoring-Betrieb"),
|
||||||
|
),
|
||||||
|
(3.5, 8.1, 5.6),
|
||||||
|
font_size=7.9,
|
||||||
|
)
|
||||||
|
add_callout(document, "Fail-safe-Verhalten: ", "Fehlt der Snapshot, ist er älter als standardmäßig 180 Sekunden oder schlägt eine Integritätsprüfung fehl, liefert der Consumer neun gültige UNKNOWN-Services. Ein Transportfehler wird sichtbar und nicht als gesunder Zustand interpretiert.")
|
||||||
|
add_heading(document, "Architekturprinzipien", level=2)
|
||||||
|
add_bullet(document, "Least Privilege: Nur das Collector-Konto erhält den erforderlichen lesenden BizTalk-Zugriff.")
|
||||||
|
add_bullet(document, "Atomare Übergabe: Der Collector ersetzt den Snapshot erst nach vollständigem Schreiben.")
|
||||||
|
add_bullet(document, "Begrenzte Laufzeit: Timeouts, Parallelität, maximale Endpunktzahl und Textlängen sind konfiguriert.")
|
||||||
|
add_bullet(document, "Stabile Schnittstelle: Die neun Basisservices bleiben auch bei Fehlern vorhanden.")
|
||||||
|
add_bullet(document, "Stabile Namen: Die Umgebungskennzeichnung ändert vorhandene Checkmk-Services standardmäßig nicht.")
|
||||||
|
document.add_page_break()
|
||||||
|
|
||||||
|
# 4: Monitoring scope
|
||||||
|
add_heading(document, "4. Was überwacht die Lösung?")
|
||||||
|
add_body(document, "Die Lösung erzeugt standardmäßig neun kompakte Services. Die einzelnen Zustände können in Checkmk separat visualisiert, alarmiert und historisiert werden.")
|
||||||
|
add_table(
|
||||||
|
document,
|
||||||
|
("Service", "Überwachte Aussage", "Typische Messwerte"),
|
||||||
|
(
|
||||||
|
("BizTalk Platform", "WMI-Zugang, BizTalk-Gruppe und zentrale Datenbankziele sind ermittelbar.", "Plattform- und Zielinformationen"),
|
||||||
|
("BizTalk SQL Access", "Collector-Konto kann die BizTalk-Datenbanken per Windows-Authentifizierung öffnen.", "Ziele gesamt, erreichbar, fehlgeschlagen"),
|
||||||
|
("BizTalk Suspended Instances", "Resumable/non-resumable Instanzen und Routing Failure Reports.", "Anzahlen je Kategorie"),
|
||||||
|
("BizTalk Host Instances", "Lokale Host Instances laufen oder befinden sich in Stop-/Übergangszuständen.", "Started, stopped, pending, unknown"),
|
||||||
|
("BizTalk Receive Locations", "Aktivierungen sowie erwartete oder unerwartete Deaktivierungen.", "Enabled, expected/unexpected disabled"),
|
||||||
|
("BizTalk Send Ports", "Gestartete sowie erwartet oder unerwartet inaktive Send Ports.", "Started, stopped, bound, unknown"),
|
||||||
|
("BizTalk Endpoint Reachability", "Technische Ziele aktiver Send-/Receive-Artefakte sind über Host/Port erreichbar.", "Aktiv, getestet, erreichbar, fehlerhaft, Dauer"),
|
||||||
|
("BizTalk Orchestrations", "Gestartete, gestoppte, gebundene, ungebundene oder unbekannte Zustände.", "Anzahlen je Laufzeitstatus"),
|
||||||
|
("BizTalk Event Log", "Aktuelle BizTalk-bezogene Fehler und Warnungen im Application Log.", "Errors und Warnings im Zeitfenster"),
|
||||||
|
),
|
||||||
|
(4.5, 8.2, 4.5),
|
||||||
|
font_size=8.0,
|
||||||
|
)
|
||||||
|
add_heading(document, "Endpoint-Prüfung", level=2)
|
||||||
|
add_body(document, "Die Erreichbarkeitsprüfung beschränkt sich auf aktuell gestartete Send Ports und aktivierte Receive Locations. Adressen werden auf Protokoll, Host und Port reduziert, dedupliziert und mit begrenzter Parallelität geprüft. Im gesunden Zustand erscheint nur eine Gesamtaussage; bei Fehlern werden ausschließlich die nicht erreichbaren Ziele begrenzt aufgelistet.")
|
||||||
|
add_callout(document, "Wichtige Abgrenzung: ", "Die Endpoint-Prüfung ist ein technischer Netzwerkcheck. Sie bestätigt je nach Protokoll DNS, Route, Firewall und einen annehmenden TCP-Port beziehungsweise den lokalen UDP-Versand. Sie führt keine Anmeldung durch, sendet keine BizTalk-Nachricht und bewertet nicht die fachliche Funktion des Zielsystems.", fill=LIGHT_ORANGE, border=ORANGE)
|
||||||
|
document.add_page_break()
|
||||||
|
|
||||||
|
# 5: Data exchange
|
||||||
|
add_heading(document, "5. Welche Daten werden ausgetauscht?")
|
||||||
|
add_body(document, "Der Datenaustausch besteht aus drei klar getrennten Stufen. Verarbeitet werden Betriebs- und Konfigurationsmetadaten, keine fachlichen Nachrichteninhalte.")
|
||||||
|
add_table(
|
||||||
|
document,
|
||||||
|
("Stufe", "Dateninhalt", "Transport und Schutz"),
|
||||||
|
(
|
||||||
|
("1. Quellen → Collector", "BizTalk-Gruppe und Datenbankziele; Namen und Laufzeitzustände von Host Instances, suspendierten Instanzen, Receive Locations, Send Ports und Orchestrations; Event-Log-Zähler; Host/Port aktiver Endpunkte.", "Lokale WMI-Abfragen, integrierter SQL-Verbindungstest, lokales Event Log und begrenzte TCP-/UDP-Probes."),
|
||||||
|
("2. Collector → Snapshot", "Erzeugungszeit, Quellmaschine, Collector-Identität, Zeilenanzahl, SHA-256 und neun Checkmk-Zeilen mit Status, Metriken und Kurztext.", "Lokale Datei, atomar ersetzt, ACL-geschützt, standardmäßig maximal 1 MiB."),
|
||||||
|
("3. Consumer → Checkmk", "Validierte Local-Check-Zeilen: Status, Zähler, Summaries sowie bei Störungen begrenzte Objekt- oder Ziellisten.", "Standardausgabe des Local Checks als Bestandteil der Checkmk-Agentenausgabe."),
|
||||||
|
),
|
||||||
|
(3.7, 8.2, 5.3),
|
||||||
|
font_size=8.0,
|
||||||
|
)
|
||||||
|
add_heading(document, "Beispiel des lokalen Snapshot-Vertrags", level=2)
|
||||||
|
add_code_block(
|
||||||
|
document,
|
||||||
|
(
|
||||||
|
"BIZTALK_CHECKMK_PULSE_SNAPSHOT_V2",
|
||||||
|
"generatedUtc=2026-08-10T08:15:00.0000000Z",
|
||||||
|
"machineBase64=QlRaLVBSSC0wMQ==",
|
||||||
|
"identityBase64=RE9NQUlOXHN2Y19iaXp0YWxrX21vbml0b3Jpbmc=",
|
||||||
|
"payloadLines=9",
|
||||||
|
"payloadSha256=<64 hexadezimale Zeichen>",
|
||||||
|
"",
|
||||||
|
'0 "BizTalk Suspended Instances" biztalk_suspended_total=0;;;0|... Suspended total=0.',
|
||||||
|
'0 "BizTalk Endpoint Reachability" biztalk_endpoints_active=61;;;0|... Alle 61 aktiven Endpunkte sind erreichbar.',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
add_body(document, "Die Base64-Felder verhindern problematische Trennzeichen im Header; sie sind keine Verschlüsselung. SHA-256 erkennt unvollständige oder veränderte Snapshots. Der Schutz vor unberechtigtem lokalem Zugriff erfolgt über Windows-Dateirechte.")
|
||||||
|
add_heading(document, "Enthaltene und nicht enthaltene Informationen", level=2)
|
||||||
|
add_table(
|
||||||
|
document,
|
||||||
|
("Im Monitoring enthalten", "Nicht erhoben oder übertragen"),
|
||||||
|
(
|
||||||
|
("Technische Status- und Mengeninformationen", "BizTalk-Nachrichten oder fachliche Payloads"),
|
||||||
|
("Relevante BizTalk-Anwendungs- und Artefaktnamen", "Passwörter, Tokens oder Endpoint-Zugangsdaten"),
|
||||||
|
("DB-Server/-Namen und Ausführungsidentität zur Diagnose", "Vollständige Inhalte der BizTalk-Datenbanken"),
|
||||||
|
("Bei Endpoint-Fehlern: Protokoll, Host und Port", "Anmeldungen oder fachliche Requests an Zielsysteme"),
|
||||||
|
("Fehlerkategorien und begrenzte Diagnosetexte", "Dateiinhalte von UNC-/SMB-Freigaben"),
|
||||||
|
),
|
||||||
|
(8.6, 8.6),
|
||||||
|
font_size=8.2,
|
||||||
|
)
|
||||||
|
add_callout(document, "Dateneinordnung: ", "Die Ausgabe enthält technische Betriebsmetadaten. Artefakt-, Server- und Zielnamen können Rückschlüsse auf Integrationen zulassen und sollten innerhalb der bestehenden Zugriffs- und Aufbewahrungsregeln des Monitorings behandelt werden.")
|
||||||
|
document.add_page_break()
|
||||||
|
|
||||||
|
# 6–7: Operation and conclusion
|
||||||
|
add_heading(document, "6. Betrieb, Sicherheit und Grenzen")
|
||||||
|
add_heading(document, "Betriebsmodell", level=2)
|
||||||
|
add_bullet(document, "Collector jede Minute; parallele Läufe werden verhindert.")
|
||||||
|
add_bullet(document, "Endpoint-Katalog übernimmt relevante Kandidatenänderungen im nächsten Minutenlauf und wird spätestens wöchentlich vollständig abgeglichen.")
|
||||||
|
add_bullet(document, "Nicht automatisch prüfbare Adapter bleiben als Abdeckungsmetriken sichtbar, ohne erfolgreiche Netzwerkprobes pauschal zu entwerten.")
|
||||||
|
add_bullet(document, "Getrennte Tageslogs für Provider und Consumer; Standardaufbewahrung 30 Tage.")
|
||||||
|
add_bullet(document, "Erwartet inaktive Receive Locations und Send Ports können exakt allowlisted werden und bleiben als Messwert sichtbar.")
|
||||||
|
add_bullet(document, "Endpoint-Prüfung standardmäßig mit maximal 100 Zielen, 16 parallelen Probes und 3 Sekunden Timeout je Ziel.")
|
||||||
|
add_bullet(document, "Der Installer validiert neue Versionen auf exakt neun Services, akzeptiert vollständige Legacy-Verträge mit weniger Services und stoppt entfernte oder umbenannte Servicenamen vor jeder Umschaltung.")
|
||||||
|
add_bullet(document, "Ein laufbezogenes Setup-Log protokolliert Phase, Binary-Version, Self-Test-Ausgabe, Taskcodes, Exception-Kette und Rollback ohne Kennwort.")
|
||||||
|
add_bullet(document, "Nach der Umschaltung erzwingt das Setup einen frischen Katalogabgleich. Kernservice-UNKNOWN bleibt blockierend; ein isoliertes Endpoint-Reachability-UNKNOWN bleibt als sichtbare Betriebswarnung erhalten und löst keinen Rollback aus.")
|
||||||
|
|
||||||
|
add_heading(document, "Sicherheitsmodell", level=2)
|
||||||
|
add_table(
|
||||||
|
document,
|
||||||
|
("Kontrolle", "Wirkung"),
|
||||||
|
(
|
||||||
|
("Dediziertes Servicekonto", "BizTalk-/SQL-Zugriff ist auf den Collector begrenzt und wird nicht auf alle LocalSystem-Dienste ausgeweitet."),
|
||||||
|
("BizTalk Read-Only-Gruppe", "Lesende BizTalk-Rolle; individuelle SQL-Rechte oder SQL-sysadmin sind nicht vorgesehen."),
|
||||||
|
("ACL-getrennter Snapshot", "Collector schreibt, LocalSystem liest; Konfiguration und Logs werden getrennt behandelt."),
|
||||||
|
("Integritäts- und Altersprüfung", "Falsche Maschine, fehlerhaftes Format, abweichende Prüfsumme oder veraltete Daten führen zu UNKNOWN."),
|
||||||
|
("Begrenzte Ausgabe", "Listen und Diagnosen werden gekürzt; vollständige Zähler bleiben als Metriken erhalten."),
|
||||||
|
),
|
||||||
|
(4.6, 12.6),
|
||||||
|
font_size=8.3,
|
||||||
|
)
|
||||||
|
|
||||||
|
add_heading(document, "Bewusste Grenzen", level=2)
|
||||||
|
add_bullet(document, "Die Lösung ersetzt nicht das allgemeine Checkmk-MSSQL-Plugin und ist kein vollständiges SQL-Monitoring.")
|
||||||
|
add_bullet(document, "Sie bewertet keine fachliche End-to-End-Verarbeitung einer BizTalk-Schnittstelle.")
|
||||||
|
add_bullet(document, "Ein erreichbarer Port beweist nicht, dass Anmeldung, Protokoll oder Zielanwendung fachlich funktionieren.")
|
||||||
|
add_bullet(document, "SHA-256 schützt die Übergabe vor unbemerkten Fehlern, nicht vor einem lokalen Administrator.")
|
||||||
|
add_bullet(document, "Schwellwerte, Allowlist-Einträge und Benachrichtigungen bleiben Teil der betrieblichen Konfiguration.")
|
||||||
|
|
||||||
|
add_heading(document, "7. Fazit")
|
||||||
|
add_body(document, "BizTalk Checkmk Pulse schafft eine klar abgegrenzte Brücke zwischen BizTalk Server und Checkmk. Die Lösung liefert eine verständliche, alarmierbare Sicht auf die wichtigsten Laufzeitkomponenten, hält den Checkmk-Agenten von privilegierten Datenquellen fern und macht auch Fehler im Datentransport selbst sichtbar. Der ausgetauschte Datenumfang bleibt auf technische Betriebsmetadaten beschränkt.")
|
||||||
|
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.6 sowie Checkmk User Guide „Local checks“, abgerufen am 10. August 2026.", {"size": 7.8, "color": MUTED}),),
|
||||||
|
before=8,
|
||||||
|
after=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
document.save(OUTPUT)
|
||||||
|
print(OUTPUT)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
build_document()
|
||||||
@@ -43,6 +43,7 @@
|
|||||||
<Compile Include="InstallerEngine.cs" />
|
<Compile Include="InstallerEngine.cs" />
|
||||||
<Compile Include="MainForm.cs" />
|
<Compile Include="MainForm.cs" />
|
||||||
<Compile Include="Program.cs" />
|
<Compile Include="Program.cs" />
|
||||||
|
<Compile Include="SetupOperationLog.cs" />
|
||||||
<Compile Include="TaskSchedulerService.cs" />
|
<Compile Include="TaskSchedulerService.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ using System.Collections.Generic;
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Security.AccessControl;
|
using System.Security.AccessControl;
|
||||||
using System.Security.Principal;
|
using System.Security.Principal;
|
||||||
|
using System.Text;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
|
|
||||||
namespace BizTalkCheckmkPulse.Setup
|
namespace BizTalkCheckmkPulse.Setup
|
||||||
@@ -14,6 +16,7 @@ namespace BizTalkCheckmkPulse.Setup
|
|||||||
internal sealed class InstallerEngine
|
internal sealed class InstallerEngine
|
||||||
{
|
{
|
||||||
internal const string TaskName = "BizTalk Checkmk Pulse Provider";
|
internal const string TaskName = "BizTalk Checkmk Pulse Provider";
|
||||||
|
private static readonly TimeSpan PostInstallValidationTimeout = TimeSpan.FromMinutes(4);
|
||||||
private readonly string packageDirectory;
|
private readonly string packageDirectory;
|
||||||
private readonly string installDirectory;
|
private readonly string installDirectory;
|
||||||
private readonly string runtimeDirectory;
|
private readonly string runtimeDirectory;
|
||||||
@@ -27,42 +30,70 @@ namespace BizTalkCheckmkPulse.Setup
|
|||||||
checkmkLocalDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "checkmk", "agent", "local");
|
checkmkLocalDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "checkmk", "agent", "local");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Install(string account, string password, bool isGmsa, string environmentName, Action<string> report)
|
public void Install(
|
||||||
|
string account,
|
||||||
|
string password,
|
||||||
|
bool isGmsa,
|
||||||
|
string environmentName,
|
||||||
|
bool allowServiceNameChange,
|
||||||
|
Action<string> report)
|
||||||
{
|
{
|
||||||
Validate(account, password, isGmsa, environmentName);
|
var operationStartedUtc = DateTime.UtcNow;
|
||||||
report = report ?? delegate { };
|
var uiReport = report ?? delegate { };
|
||||||
var accountSid = (SecurityIdentifier)new NTAccount(account).Translate(typeof(SecurityIdentifier));
|
var setupLog = SetupOperationLog.Create(runtimeDirectory, "install-or-update");
|
||||||
report("Konto aufgeloest: " + account + " (" + accountSid.Value + ")");
|
report = delegate(string message)
|
||||||
|
{
|
||||||
|
setupLog.Write("INFO", message);
|
||||||
|
uiReport(message);
|
||||||
|
};
|
||||||
|
if (setupLog.FilePath.Length > 0)
|
||||||
|
report("Diagnoselog: " + setupLog.FilePath);
|
||||||
|
else
|
||||||
|
uiReport("WARNUNG: Diagnoselog konnte nicht angelegt werden: " + setupLog.CreationError);
|
||||||
|
|
||||||
|
var phase = "Initialisierung";
|
||||||
var sourceApplication = Path.Combine(packageDirectory, "application");
|
var sourceApplication = Path.Combine(packageDirectory, "application");
|
||||||
var sourceExe = Path.Combine(sourceApplication, "BizTalkCheckmkPulse.exe");
|
var sourceExe = Path.Combine(sourceApplication, "BizTalkCheckmkPulse.exe");
|
||||||
var sourceConfig = sourceExe + ".config";
|
var sourceConfig = sourceExe + ".config";
|
||||||
var sourceWrapper = Path.Combine(packageDirectory, "biztalk_checkmk_pulse.cmd");
|
var sourceWrapper = Path.Combine(packageDirectory, "biztalk_checkmk_pulse.cmd");
|
||||||
RequireFile(sourceExe);
|
|
||||||
RequireFile(sourceConfig);
|
|
||||||
RequireFile(sourceWrapper);
|
|
||||||
var targetExe = Path.Combine(installDirectory, "BizTalkCheckmkPulse.exe");
|
var targetExe = Path.Combine(installDirectory, "BizTalkCheckmkPulse.exe");
|
||||||
var targetConfig = targetExe + ".config";
|
var targetConfig = targetExe + ".config";
|
||||||
var stagingDirectory = installDirectory + ".staging." + Guid.NewGuid().ToString("N");
|
var stagingDirectory = installDirectory + ".staging." + Guid.NewGuid().ToString("N");
|
||||||
var backupDirectory = installDirectory + ".backup." + Guid.NewGuid().ToString("N");
|
var backupDirectory = installDirectory + ".backup." + Guid.NewGuid().ToString("N");
|
||||||
var targetWrapper = Path.Combine(checkmkLocalDirectory, "biztalk_checkmk_pulse.cmd");
|
var targetWrapper = Path.Combine(checkmkLocalDirectory, "biztalk_checkmk_pulse.cmd");
|
||||||
var previousWrapper = File.Exists(targetWrapper) ? File.ReadAllBytes(targetWrapper) : null;
|
byte[] previousWrapper = null;
|
||||||
var hadExistingInstallation = Directory.Exists(installDirectory);
|
var hadExistingInstallation = false;
|
||||||
var taskRemoved = false;
|
var hadExistingExecutable = false;
|
||||||
|
var mutationStarted = false;
|
||||||
var backupCreated = false;
|
var backupCreated = false;
|
||||||
var filesActivated = false;
|
var filesActivated = false;
|
||||||
var scheduler = new TaskSchedulerService();
|
var scheduler = new TaskSchedulerService();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
RunSelfTest(sourceExe);
|
phase = "Eingaben und Paket pruefen";
|
||||||
|
report("Phase: " + phase + ". Paket=" + packageDirectory + ".");
|
||||||
|
Validate(account, password, isGmsa, environmentName);
|
||||||
|
var accountSid = (SecurityIdentifier)new NTAccount(account).Translate(typeof(SecurityIdentifier));
|
||||||
|
report("Konto aufgeloest: " + account + " (" + accountSid.Value + ")");
|
||||||
|
RequireFile(sourceExe);
|
||||||
|
RequireFile(sourceConfig);
|
||||||
|
RequireFile(sourceWrapper);
|
||||||
|
previousWrapper = File.Exists(targetWrapper) ? File.ReadAllBytes(targetWrapper) : null;
|
||||||
|
hadExistingInstallation = Directory.Exists(installDirectory);
|
||||||
|
hadExistingExecutable = File.Exists(targetExe);
|
||||||
|
|
||||||
|
RunSelfTest(sourceExe, 9, "Paket", setupLog);
|
||||||
report("Paket-Vorpruefung erfolgreich: neun Checkmk-Services.");
|
report("Paket-Vorpruefung erfolgreich: neun Checkmk-Services.");
|
||||||
if (!isGmsa)
|
if (!isGmsa)
|
||||||
{
|
{
|
||||||
|
phase = "Collector-Anmeldung pruefen";
|
||||||
ValidateBatchLogon(account, password);
|
ValidateBatchLogon(account, password);
|
||||||
report("Collector-Anmeldung und 'Log on as a batch job' vor dem Update bestaetigt.");
|
report("Collector-Anmeldung und 'Log on as a batch job' vor dem Update bestaetigt.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
phase = "Update-Staging vorbereiten";
|
||||||
|
report("Phase: " + phase + ".");
|
||||||
Directory.CreateDirectory(stagingDirectory);
|
Directory.CreateDirectory(stagingDirectory);
|
||||||
var stagedExe = Path.Combine(stagingDirectory, "BizTalkCheckmkPulse.exe");
|
var stagedExe = Path.Combine(stagingDirectory, "BizTalkCheckmkPulse.exe");
|
||||||
var stagedConfig = stagedExe + ".config";
|
var stagedConfig = stagedExe + ".config";
|
||||||
@@ -72,26 +103,45 @@ namespace BizTalkCheckmkPulse.Setup
|
|||||||
stagedConfig,
|
stagedConfig,
|
||||||
File.Exists(targetConfig) ? targetConfig : null,
|
File.Exists(targetConfig) ? targetConfig : null,
|
||||||
environmentName);
|
environmentName);
|
||||||
RunSelfTest(stagedExe);
|
var stagedServiceNames = RunSelfTest(stagedExe, 9, "Staging", setupLog);
|
||||||
report("Update-Staging validiert. Umgebung=" + (effectiveEnvironment.Length == 0 ? "(keine)" : effectiveEnvironment) + ".");
|
report("Update-Staging validiert. Umgebung=" + (effectiveEnvironment.Length == 0 ? "(keine)" : effectiveEnvironment) + ".");
|
||||||
|
|
||||||
|
if (File.Exists(targetExe))
|
||||||
|
{
|
||||||
|
phase = "Installierten Servicevertrag vergleichen";
|
||||||
|
report("Phase: " + phase + ". Legacy-Versionen mit weniger stabilen Services werden als Updatequelle akzeptiert.");
|
||||||
|
var installedServiceNames = RunSelfTest(targetExe, null, "Installierte Version", setupLog);
|
||||||
|
var serviceNameChange = EnsureServiceNameCompatibility(
|
||||||
|
installedServiceNames,
|
||||||
|
stagedServiceNames,
|
||||||
|
allowServiceNameChange);
|
||||||
|
report(serviceNameChange.Length == 0
|
||||||
|
? "Checkmk-Servicevertrag unveraendert: keine Service Discovery erforderlich."
|
||||||
|
: "Checkmk-Servicevertrag geaendert; Service Discovery erforderlich. " + serviceNameChange);
|
||||||
|
}
|
||||||
|
|
||||||
// Erst nach vollstaendiger Staging-Pruefung wird der laufende Provider angehalten.
|
// Erst nach vollstaendiger Staging-Pruefung wird der laufende Provider angehalten.
|
||||||
|
phase = "Vorhandenen Scheduled Task anhalten";
|
||||||
|
report("Phase: " + phase + ".");
|
||||||
|
mutationStarted = true;
|
||||||
scheduler.DeleteIfExists(TaskName);
|
scheduler.DeleteIfExists(TaskName);
|
||||||
taskRemoved = true;
|
|
||||||
report("Vorhandener Scheduled Task angehalten und fuer das Update entfernt.");
|
report("Vorhandener Scheduled Task angehalten und fuer das Update entfernt.");
|
||||||
|
|
||||||
if (hadExistingInstallation)
|
if (hadExistingInstallation)
|
||||||
{
|
{
|
||||||
|
phase = "Vorhandene Programmversion sichern";
|
||||||
Directory.Move(installDirectory, backupDirectory);
|
Directory.Move(installDirectory, backupDirectory);
|
||||||
backupCreated = true;
|
backupCreated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
phase = "Neue Programmversion aktivieren";
|
||||||
Directory.Move(stagingDirectory, installDirectory);
|
Directory.Move(stagingDirectory, installDirectory);
|
||||||
filesActivated = true;
|
filesActivated = true;
|
||||||
report(hadExistingInstallation
|
report(hadExistingInstallation
|
||||||
? "Programmdateien atomar auf die neue Version umgestellt."
|
? "Programmdateien atomar auf die neue Version umgestellt."
|
||||||
: "Programmdateien installiert: " + installDirectory);
|
: "Programmdateien installiert: " + installDirectory);
|
||||||
|
|
||||||
|
phase = "Runtime-Verzeichnisse und ACLs einrichten";
|
||||||
var dataDirectory = Path.Combine(runtimeDirectory, "data");
|
var dataDirectory = Path.Combine(runtimeDirectory, "data");
|
||||||
var logDirectory = Path.Combine(runtimeDirectory, "logs");
|
var logDirectory = Path.Combine(runtimeDirectory, "logs");
|
||||||
Directory.CreateDirectory(runtimeDirectory);
|
Directory.CreateDirectory(runtimeDirectory);
|
||||||
@@ -104,35 +154,75 @@ namespace BizTalkCheckmkPulse.Setup
|
|||||||
ApplyDirectoryAcl(logDirectory, accountSid, FileSystemRights.Modify, FileSystemRights.Modify);
|
ApplyDirectoryAcl(logDirectory, accountSid, FileSystemRights.Modify, FileSystemRights.Modify);
|
||||||
report("Least-Privilege-Verzeichnisrechte gesetzt; vorhandene Runtime-Daten bleiben erhalten.");
|
report("Least-Privilege-Verzeichnisrechte gesetzt; vorhandene Runtime-Daten bleiben erhalten.");
|
||||||
|
|
||||||
|
phase = "Checkmk Local Check installieren";
|
||||||
Directory.CreateDirectory(checkmkLocalDirectory);
|
Directory.CreateDirectory(checkmkLocalDirectory);
|
||||||
File.Copy(sourceWrapper, targetWrapper, true);
|
File.Copy(sourceWrapper, targetWrapper, true);
|
||||||
report("Checkmk Local Check installiert: " + checkmkLocalDirectory);
|
report("Checkmk Local Check installiert: " + checkmkLocalDirectory);
|
||||||
|
|
||||||
RunSelfTest(targetExe);
|
phase = "Installierte Programmversion pruefen";
|
||||||
|
RunSelfTest(targetExe, 9, "Aktivierte Version", setupLog);
|
||||||
report("Installierter Self-Test erfolgreich: neun Checkmk-Services.");
|
report("Installierter Self-Test erfolgreich: neun Checkmk-Services.");
|
||||||
|
|
||||||
scheduler.RegisterAndStart(
|
phase = "Provider-Abnahmelauf registrieren und starten";
|
||||||
|
var runtimeValidationStartedUtc = DateTime.UtcNow;
|
||||||
|
scheduler.RegisterValidationAndStart(
|
||||||
TaskName,
|
TaskName,
|
||||||
targetExe,
|
targetExe,
|
||||||
installDirectory,
|
installDirectory,
|
||||||
account,
|
account,
|
||||||
isGmsa ? null : password,
|
isGmsa ? null : password,
|
||||||
isGmsa);
|
isGmsa);
|
||||||
taskRemoved = false;
|
report("Einmaliger Provider-Abnahmelauf mit erzwungenem Endpoint-Katalogabgleich gestartet.");
|
||||||
report("Scheduled Task registriert und einmalig gestartet: " + TaskName);
|
|
||||||
|
phase = "Provider-Abnahmelauf abwarten";
|
||||||
|
var completedRun = scheduler.WaitForSuccessfulRun(
|
||||||
|
TaskName,
|
||||||
|
runtimeValidationStartedUtc,
|
||||||
|
PostInstallValidationTimeout,
|
||||||
|
report);
|
||||||
|
report("Provider-Abnahmelauf erfolgreich: LastTaskResult=" + completedRun.ExitCode + ".");
|
||||||
|
|
||||||
|
phase = "Runtime-Artefakte validieren";
|
||||||
|
var runtimeValidation = RunRuntimeValidation(
|
||||||
|
targetExe,
|
||||||
|
runtimeValidationStartedUtc,
|
||||||
|
account);
|
||||||
|
report("Post-Install-Runtime-Abnahme erfolgreich: " + runtimeValidation);
|
||||||
|
|
||||||
|
phase = "Minuetlichen Scheduled Task registrieren";
|
||||||
|
scheduler.RegisterRecurring(
|
||||||
|
TaskName,
|
||||||
|
targetExe,
|
||||||
|
installDirectory,
|
||||||
|
account,
|
||||||
|
isGmsa ? null : password,
|
||||||
|
isGmsa);
|
||||||
|
report("Validierter minuetlicher Scheduled Task registriert: " + TaskName);
|
||||||
|
|
||||||
TryDeleteDirectory(backupDirectory, report);
|
TryDeleteDirectory(backupDirectory, report);
|
||||||
|
report("Installation/Update erfolgreich abgeschlossen.");
|
||||||
}
|
}
|
||||||
catch (Exception installException)
|
catch (Exception installException)
|
||||||
{
|
{
|
||||||
var rollbackFailures = new List<string>();
|
setupLog.WriteException(phase, installException);
|
||||||
try
|
if (phase.StartsWith("Provider-", StringComparison.Ordinal)
|
||||||
|
|| phase.StartsWith("Runtime-", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
scheduler.DeleteIfExists(TaskName);
|
WriteRecentProviderLog(setupLog, runtimeDirectory, operationStartedUtc);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
var rollbackFailures = new List<string>();
|
||||||
|
if (mutationStarted)
|
||||||
{
|
{
|
||||||
rollbackFailures.Add("Task stoppen: " + ex.Message);
|
try
|
||||||
|
{
|
||||||
|
scheduler.DeleteIfExists(TaskName);
|
||||||
|
report("Rollback: Scheduled Task gestoppt und entfernt.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
rollbackFailures.Add("Task stoppen: " + ex.Message);
|
||||||
|
setupLog.WriteException("Rollback: Task stoppen", ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filesActivated || backupCreated)
|
if (filesActivated || backupCreated)
|
||||||
@@ -146,47 +236,68 @@ namespace BizTalkCheckmkPulse.Setup
|
|||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
rollbackFailures.Add("Programmdateien wiederherstellen: " + ex.Message);
|
rollbackFailures.Add("Programmdateien wiederherstellen: " + ex.Message);
|
||||||
|
setupLog.WriteException("Rollback: Programmdateien wiederherstellen", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
if (mutationStarted)
|
||||||
{
|
|
||||||
RestoreWrapper(targetWrapper, previousWrapper);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
rollbackFailures.Add("Checkmk-Wrapper wiederherstellen: " + ex.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (taskRemoved && hadExistingInstallation && File.Exists(targetExe))
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
scheduler.RegisterAndStart(
|
RestoreWrapper(targetWrapper, previousWrapper);
|
||||||
|
report("Rollback: Checkmk-Wrapper auf den vorherigen Stand zurueckgesetzt.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
rollbackFailures.Add("Checkmk-Wrapper wiederherstellen: " + ex.Message);
|
||||||
|
setupLog.WriteException("Rollback: Checkmk-Wrapper wiederherstellen", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mutationStarted && hadExistingExecutable && File.Exists(targetExe))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var rollbackRunStartedUtc = DateTime.UtcNow;
|
||||||
|
scheduler.RegisterRecurringAndStart(
|
||||||
TaskName,
|
TaskName,
|
||||||
targetExe,
|
targetExe,
|
||||||
installDirectory,
|
installDirectory,
|
||||||
account,
|
account,
|
||||||
isGmsa ? null : password,
|
isGmsa ? null : password,
|
||||||
isGmsa);
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
rollbackFailures.Add("Scheduled Task wiederherstellen: " + ex.Message);
|
rollbackFailures.Add("Scheduled Task wiederherstellen: " + ex.Message);
|
||||||
|
setupLog.WriteException("Rollback: Scheduled Task wiederherstellen", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var rollback = rollbackFailures.Count == 0
|
var rollback = !mutationStarted
|
||||||
? "Rollback erfolgreich."
|
? "Keine Systemaenderung vorgenommen; kein Rollback erforderlich."
|
||||||
: "Rollback unvollstaendig: " + string.Join(" | ", rollbackFailures);
|
: rollbackFailures.Count == 0
|
||||||
|
? "Rollback erfolgreich."
|
||||||
|
: "Rollback unvollstaendig: " + string.Join(" | ", rollbackFailures);
|
||||||
|
setupLog.Write("ERROR", rollback);
|
||||||
|
var logReference = setupLog.FilePath.Length == 0
|
||||||
|
? "Diagnoselog nicht verfuegbar (" + setupLog.CreationError + ")."
|
||||||
|
: "Diagnoselog: " + setupLog.FilePath;
|
||||||
throw new InvalidOperationException(
|
throw new InvalidOperationException(
|
||||||
"Installation/Update fehlgeschlagen. " + rollback + " Ursache: " + installException.Message,
|
"Installation/Update fehlgeschlagen in Phase '" + phase + "'. "
|
||||||
|
+ rollback + " Ursache: " + installException.Message + " " + logReference,
|
||||||
installException);
|
installException);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
TryDeleteDirectory(stagingDirectory, null);
|
TryDeleteDirectory(stagingDirectory, null);
|
||||||
|
setupLog.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,8 +508,20 @@ namespace BizTalkCheckmkPulse.Setup
|
|||||||
new DirectoryInfo(path).SetAccessControl(security);
|
new DirectoryInfo(path).SetAccessControl(security);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RunSelfTest(string executable)
|
private static IReadOnlyList<string> RunSelfTest(
|
||||||
|
string executable,
|
||||||
|
int? expectedServiceCount,
|
||||||
|
string label,
|
||||||
|
SetupOperationLog setupLog)
|
||||||
{
|
{
|
||||||
|
var version = FileVersionInfo.GetVersionInfo(executable).FileVersion ?? "(unbekannt)";
|
||||||
|
setupLog.Write(
|
||||||
|
"INFO",
|
||||||
|
"Self-Test startet. label=" + label
|
||||||
|
+ ", path=" + executable
|
||||||
|
+ ", file_version=" + version
|
||||||
|
+ ", size_bytes=" + new FileInfo(executable).Length
|
||||||
|
+ ", expected_lines=" + (expectedServiceCount.HasValue ? expectedServiceCount.Value.ToString(CultureInfo.InvariantCulture) : "legacy-compatible") + ".");
|
||||||
var start = new ProcessStartInfo(executable, "--self-test")
|
var start = new ProcessStartInfo(executable, "--self-test")
|
||||||
{
|
{
|
||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
@@ -409,16 +532,199 @@ namespace BizTalkCheckmkPulse.Setup
|
|||||||
using (var process = Process.Start(start))
|
using (var process = Process.Start(start))
|
||||||
{
|
{
|
||||||
if (process == null) throw new InvalidOperationException("Self-Test konnte nicht gestartet werden.");
|
if (process == null) throw new InvalidOperationException("Self-Test konnte nicht gestartet werden.");
|
||||||
var output = process.StandardOutput.ReadToEnd();
|
var outputRead = process.StandardOutput.ReadToEndAsync();
|
||||||
var error = process.StandardError.ReadToEnd();
|
var errorRead = process.StandardError.ReadToEndAsync();
|
||||||
if (!process.WaitForExit(30000))
|
if (!process.WaitForExit(30000))
|
||||||
{
|
{
|
||||||
process.Kill();
|
process.Kill();
|
||||||
throw new InvalidOperationException("Self-Test hat das Zeitlimit ueberschritten.");
|
process.WaitForExit();
|
||||||
|
System.Threading.Tasks.Task.WaitAll(outputRead, errorRead);
|
||||||
|
var timeoutOutput = outputRead.Result;
|
||||||
|
var timeoutError = errorRead.Result;
|
||||||
|
setupLog.Write("ERROR", "Self-Test Timeout. label=" + label + ", stdout=" + timeoutOutput + ", stderr=" + timeoutError + ".");
|
||||||
|
throw new InvalidOperationException("Self-Test '" + label + "' hat das Zeitlimit von 30 Sekunden ueberschritten.");
|
||||||
}
|
}
|
||||||
var lines = output.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries);
|
System.Threading.Tasks.Task.WaitAll(outputRead, errorRead);
|
||||||
if (process.ExitCode != 0 || lines.Length != 9 || lines.Any(x => !x.StartsWith("0 ", StringComparison.Ordinal)))
|
var output = outputRead.Result;
|
||||||
throw new InvalidOperationException("Self-Test fehlgeschlagen. Exitcode=" + process.ExitCode + ", Zeilen=" + lines.Length + ". " + error);
|
var error = errorRead.Result;
|
||||||
|
setupLog.Write(
|
||||||
|
"INFO",
|
||||||
|
"Self-Test beendet. label=" + label
|
||||||
|
+ ", exit_code=" + process.ExitCode
|
||||||
|
+ ", stdout=" + output
|
||||||
|
+ ", stderr=" + error + ".");
|
||||||
|
return ParseSelfTestOutput(output, error, process.ExitCode, expectedServiceCount, label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static IReadOnlyList<string> ParseSelfTestOutput(
|
||||||
|
string output,
|
||||||
|
string error,
|
||||||
|
int exitCode,
|
||||||
|
int? expectedServiceCount,
|
||||||
|
string label)
|
||||||
|
{
|
||||||
|
var lines = (output ?? string.Empty).Split(
|
||||||
|
new[] { "\r\n", "\n" },
|
||||||
|
StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var expected = expectedServiceCount.HasValue
|
||||||
|
? expectedServiceCount.Value.ToString(CultureInfo.InvariantCulture)
|
||||||
|
: "mindestens 1 (Legacy-kompatibel)";
|
||||||
|
if (exitCode != 0
|
||||||
|
|| lines.Length == 0
|
||||||
|
|| expectedServiceCount.HasValue && lines.Length != expectedServiceCount.Value
|
||||||
|
|| lines.Any(x => !x.StartsWith("0 ", StringComparison.Ordinal)))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Self-Test '" + label + "' fehlgeschlagen. Exitcode=" + exitCode
|
||||||
|
+ ", Zeilen=" + lines.Length + ", Erwartet=" + expected
|
||||||
|
+ ", Stdout=" + CompactProcessText(output)
|
||||||
|
+ ", Stderr=" + CompactProcessText(error) + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
var serviceNames = lines.Select(ExtractServiceName).ToArray();
|
||||||
|
if (serviceNames.Distinct(StringComparer.Ordinal).Count() != serviceNames.Length)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Self-Test '" + label + "' fehlgeschlagen: Checkmk-Servicenamen sind nicht eindeutig. "
|
||||||
|
+ "Services=[" + string.Join(", ", serviceNames) + "].");
|
||||||
|
return serviceNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CompactProcessText(string value)
|
||||||
|
{
|
||||||
|
var text = (value ?? string.Empty)
|
||||||
|
.Replace("\r", string.Empty)
|
||||||
|
.Replace("\n", " | ")
|
||||||
|
.Trim();
|
||||||
|
const int maxLength = 2000;
|
||||||
|
return text.Length <= maxLength ? text : text.Substring(0, maxLength) + "...[gekuerzt]";
|
||||||
|
}
|
||||||
|
|
||||||
|
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 outputRead = process.StandardOutput.ReadToEndAsync();
|
||||||
|
var errorRead = process.StandardError.ReadToEndAsync();
|
||||||
|
if (!process.WaitForExit(30000))
|
||||||
|
{
|
||||||
|
process.Kill();
|
||||||
|
process.WaitForExit();
|
||||||
|
System.Threading.Tasks.Task.WaitAll(outputRead, errorRead);
|
||||||
|
throw new TimeoutException(
|
||||||
|
"Runtime-Abnahme hat das Zeitlimit von 30 Sekunden ueberschritten. Stdout="
|
||||||
|
+ CompactProcessText(outputRead.Result)
|
||||||
|
+ ", Stderr=" + CompactProcessText(errorRead.Result) + ".");
|
||||||
|
}
|
||||||
|
System.Threading.Tasks.Task.WaitAll(outputRead, errorRead);
|
||||||
|
var output = outputRead.Result.Trim();
|
||||||
|
var error = errorRead.Result.Trim();
|
||||||
|
|
||||||
|
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('"');
|
||||||
|
var secondQuote = firstQuote < 0 ? -1 : line.IndexOf('"', firstQuote + 1);
|
||||||
|
if (firstQuote < 0 || secondQuote <= firstQuote + 1)
|
||||||
|
throw new InvalidOperationException("Self-Test fehlgeschlagen: Checkmk-Servicename kann nicht gelesen werden: " + line);
|
||||||
|
return line.Substring(firstQuote + 1, secondQuote - firstQuote - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string EnsureServiceNameCompatibility(
|
||||||
|
IEnumerable<string> installedServiceNames,
|
||||||
|
IEnumerable<string> stagedServiceNames,
|
||||||
|
bool allowServiceNameChange)
|
||||||
|
{
|
||||||
|
var installed = new HashSet<string>(installedServiceNames ?? Enumerable.Empty<string>(), StringComparer.Ordinal);
|
||||||
|
var staged = new HashSet<string>(stagedServiceNames ?? Enumerable.Empty<string>(), StringComparer.Ordinal);
|
||||||
|
if (installed.SetEquals(staged)) return string.Empty;
|
||||||
|
|
||||||
|
var removed = installed.Except(staged, StringComparer.Ordinal).OrderBy(x => x, StringComparer.Ordinal).ToArray();
|
||||||
|
var added = staged.Except(installed, StringComparer.Ordinal).OrderBy(x => x, StringComparer.Ordinal).ToArray();
|
||||||
|
var description = "Entfernt=[" + string.Join(", ", removed) + "]; Neu=[" + string.Join(", ", added) + "].";
|
||||||
|
// Reine Erweiterungen lassen bestehende Checkmk-Services unangetastet und
|
||||||
|
// muessen Legacy-Updates deshalb nicht blockieren. Entfernte Namen sind
|
||||||
|
// dagegen ein Rename oder ein Vertragsbruch und bleiben bestaetigungspflichtig.
|
||||||
|
if (removed.Length > 0 && !allowServiceNameChange)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Sicherheitsstopp: Das Update wuerde Checkmk-Servicenamen aendern. "
|
||||||
|
+ description
|
||||||
|
+ " Ohne Service Discovery entstehen verwaiste bzw. fehlende Services. "
|
||||||
|
+ "Nur wenn die Aenderung beabsichtigt ist, im Setup 'Service Discovery ist eingeplant' bestaetigen und anschliessend die Discovery ausfuehren.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteRecentProviderLog(
|
||||||
|
SetupOperationLog setupLog,
|
||||||
|
string runtimeRoot,
|
||||||
|
DateTime operationStartedUtc)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var directory = Path.Combine(runtimeRoot, "logs");
|
||||||
|
if (!Directory.Exists(directory))
|
||||||
|
{
|
||||||
|
setupLog.Write("INFO", "Provider-Logverzeichnis existiert nicht: " + directory + ".");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var file = Directory.GetFiles(directory, "biztalk-checkmk-pulse-*.log")
|
||||||
|
.OrderByDescending(File.GetLastWriteTimeUtc)
|
||||||
|
.FirstOrDefault();
|
||||||
|
if (file == null)
|
||||||
|
{
|
||||||
|
setupLog.Write("INFO", "Kein Provider-Log fuer die Fehlerdiagnose gefunden.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastWriteUtc = File.GetLastWriteTimeUtc(file);
|
||||||
|
if (lastWriteUtc < operationStartedUtc.AddSeconds(-5))
|
||||||
|
{
|
||||||
|
setupLog.Write(
|
||||||
|
"WARN",
|
||||||
|
"Neuestes Provider-Log ist aelter als dieser Setup-Lauf und wird nicht als Fehlerkontext uebernommen. "
|
||||||
|
+ "path=" + file + ", last_write_utc=" + lastWriteUtc.ToString("o") + ".");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines = File.ReadAllLines(file);
|
||||||
|
var tail = lines.Skip(Math.Max(0, lines.Length - 40));
|
||||||
|
setupLog.Write(
|
||||||
|
"ERROR",
|
||||||
|
"Letzte Provider-Logzeilen. path=" + file + ", tail=" + string.Join(" || ", tail) + ".");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
setupLog.Write("WARN", "Provider-Log konnte nicht fuer die Diagnose gelesen werden: " + ex.Message + ".");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ namespace BizTalkCheckmkPulse.Setup
|
|||||||
private readonly TextBox password = new TextBox();
|
private readonly TextBox password = new TextBox();
|
||||||
private readonly ComboBox environment = new ComboBox();
|
private readonly ComboBox environment = new ComboBox();
|
||||||
private readonly CheckBox gmsa = new CheckBox();
|
private readonly CheckBox gmsa = new CheckBox();
|
||||||
|
private readonly CheckBox allowServiceNameChange = new CheckBox();
|
||||||
private readonly Button install = new Button();
|
private readonly Button install = new Button();
|
||||||
private readonly Button uninstall = new Button();
|
private readonly Button uninstall = new Button();
|
||||||
private readonly TextBox status = new TextBox();
|
private readonly TextBox status = new TextBox();
|
||||||
@@ -20,8 +21,8 @@ namespace BizTalkCheckmkPulse.Setup
|
|||||||
{
|
{
|
||||||
this.engine = engine;
|
this.engine = engine;
|
||||||
Text = "BizTalk Checkmk Pulse Setup";
|
Text = "BizTalk Checkmk Pulse Setup";
|
||||||
ClientSize = new Size(650, 485);
|
ClientSize = new Size(650, 535);
|
||||||
MinimumSize = new Size(666, 524);
|
MinimumSize = new Size(666, 574);
|
||||||
StartPosition = FormStartPosition.CenterScreen;
|
StartPosition = FormStartPosition.CenterScreen;
|
||||||
Font = new Font("Segoe UI", 9F);
|
Font = new Font("Segoe UI", 9F);
|
||||||
|
|
||||||
@@ -68,18 +69,23 @@ namespace BizTalkCheckmkPulse.Setup
|
|||||||
if (gmsa.Checked) password.Clear();
|
if (gmsa.Checked) password.Clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
install.Location = new Point(190, 267);
|
allowServiceNameChange.Location = new Point(190, 262);
|
||||||
|
allowServiceNameChange.Size = new Size(420, 34);
|
||||||
|
allowServiceNameChange.Text = "Service-Rename ist beabsichtigt; Checkmk Service Discovery ist eingeplant";
|
||||||
|
allowServiceNameChange.Enabled = engine.IsInstalled;
|
||||||
|
|
||||||
|
install.Location = new Point(190, 306);
|
||||||
install.Size = new Size(200, 34);
|
install.Size = new Size(200, 34);
|
||||||
install.Text = "Installieren / aktualisieren";
|
install.Text = "Installieren / aktualisieren";
|
||||||
install.Click += async delegate { await InstallAsync(); };
|
install.Click += async delegate { await InstallAsync(); };
|
||||||
|
|
||||||
uninstall.Location = new Point(410, 267);
|
uninstall.Location = new Point(410, 306);
|
||||||
uninstall.Size = new Size(200, 34);
|
uninstall.Size = new Size(200, 34);
|
||||||
uninstall.Text = "Deinstallieren";
|
uninstall.Text = "Deinstallieren";
|
||||||
uninstall.Click += async delegate { await UninstallAsync(); };
|
uninstall.Click += async delegate { await UninstallAsync(); };
|
||||||
|
|
||||||
status.Location = new Point(22, 320);
|
status.Location = new Point(22, 359);
|
||||||
status.Size = new Size(588, 135);
|
status.Size = new Size(588, 146);
|
||||||
status.Multiline = true;
|
status.Multiline = true;
|
||||||
status.ReadOnly = true;
|
status.ReadOnly = true;
|
||||||
status.ScrollBars = ScrollBars.Vertical;
|
status.ScrollBars = ScrollBars.Vertical;
|
||||||
@@ -89,7 +95,7 @@ namespace BizTalkCheckmkPulse.Setup
|
|||||||
|
|
||||||
Controls.AddRange(new Control[]
|
Controls.AddRange(new Control[]
|
||||||
{
|
{
|
||||||
title, description, account, password, environment, gmsa, install, uninstall, status
|
title, description, account, password, environment, gmsa, allowServiceNameChange, install, uninstall, status
|
||||||
});
|
});
|
||||||
AcceptButton = install;
|
AcceptButton = install;
|
||||||
}
|
}
|
||||||
@@ -114,6 +120,7 @@ namespace BizTalkCheckmkPulse.Setup
|
|||||||
collectorPassword,
|
collectorPassword,
|
||||||
collectorIsGmsa,
|
collectorIsGmsa,
|
||||||
environmentName,
|
environmentName,
|
||||||
|
allowServiceNameChange.Checked,
|
||||||
Report));
|
Report));
|
||||||
password.Clear();
|
password.Clear();
|
||||||
Report("Installation abgeschlossen. Der Provider-Task wurde einmalig gestartet.");
|
Report("Installation abgeschlossen. Der Provider-Task wurde einmalig gestartet.");
|
||||||
@@ -166,6 +173,7 @@ namespace BizTalkCheckmkPulse.Setup
|
|||||||
account.Enabled = !busy;
|
account.Enabled = !busy;
|
||||||
environment.Enabled = !busy;
|
environment.Enabled = !busy;
|
||||||
gmsa.Enabled = !busy;
|
gmsa.Enabled = !busy;
|
||||||
|
allowServiceNameChange.Enabled = !busy && engine.IsInstalled;
|
||||||
password.Enabled = !busy && !gmsa.Checked;
|
password.Enabled = !busy && !gmsa.Checked;
|
||||||
UseWaitCursor = busy;
|
UseWaitCursor = busy;
|
||||||
if (message != null) Report(message);
|
if (message != null) Report(message);
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ using System.Runtime.CompilerServices;
|
|||||||
[assembly: AssemblyDescription("PowerShell-free installer and updater for BizTalk Checkmk Pulse")]
|
[assembly: AssemblyDescription("PowerShell-free installer and updater for BizTalk Checkmk Pulse")]
|
||||||
[assembly: AssemblyCompany("BEW")]
|
[assembly: AssemblyCompany("BEW")]
|
||||||
[assembly: AssemblyProduct("BizTalk Checkmk Pulse")]
|
[assembly: AssemblyProduct("BizTalk Checkmk Pulse")]
|
||||||
[assembly: AssemblyVersion("2.2.1.0")]
|
[assembly: AssemblyVersion("2.2.7.0")]
|
||||||
[assembly: AssemblyFileVersion("2.2.1.0")]
|
[assembly: AssemblyFileVersion("2.2.7.0")]
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Security.Principal;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace BizTalkCheckmkPulse.Setup
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Best-effort-Diagnoselog fuer einen einzelnen Setup-Lauf. Das Kennwort
|
||||||
|
/// wird dieser Klasse nie uebergeben und kann deshalb nicht protokolliert werden.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class SetupOperationLog : IDisposable
|
||||||
|
{
|
||||||
|
private readonly object sync = new object();
|
||||||
|
private StreamWriter writer;
|
||||||
|
|
||||||
|
private SetupOperationLog(string filePath, StreamWriter writer)
|
||||||
|
{
|
||||||
|
FilePath = filePath;
|
||||||
|
this.writer = writer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string FilePath { get; private set; }
|
||||||
|
|
||||||
|
public static SetupOperationLog Create(string runtimeDirectory, string operation)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var directory = Path.Combine(runtimeDirectory, "logs");
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
var path = Path.Combine(
|
||||||
|
directory,
|
||||||
|
"setup-" + DateTime.UtcNow.ToString("yyyyMMdd-HHmmssfff", CultureInfo.InvariantCulture)
|
||||||
|
+ "-" + Process.GetCurrentProcess().Id + ".log");
|
||||||
|
var stream = new FileStream(path, FileMode.CreateNew, FileAccess.Write, FileShare.Read);
|
||||||
|
var log = new SetupOperationLog(path, new StreamWriter(stream, new UTF8Encoding(false)) { AutoFlush = true });
|
||||||
|
log.Write("INFO", "Setup-Log gestartet. operation=" + operation
|
||||||
|
+ ", setup_version=" + typeof(SetupOperationLog).Assembly.GetName().Version
|
||||||
|
+ ", os=" + Environment.OSVersion.VersionString
|
||||||
|
+ ", process_bitness=" + (Environment.Is64BitProcess ? "64" : "32")
|
||||||
|
+ ", identity=" + CurrentIdentity() + ".");
|
||||||
|
return log;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return new SetupOperationLog(string.Empty, null)
|
||||||
|
{
|
||||||
|
CreationError = ex.Message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CreationError { get; private set; }
|
||||||
|
|
||||||
|
public void Write(string level, string message)
|
||||||
|
{
|
||||||
|
lock (sync)
|
||||||
|
{
|
||||||
|
if (writer == null) return;
|
||||||
|
writer.WriteLine(
|
||||||
|
DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture)
|
||||||
|
+ " level=" + level
|
||||||
|
+ " " + SingleLine(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteException(string phase, Exception exception)
|
||||||
|
{
|
||||||
|
Write("ERROR", "Fehler in Phase '" + phase + "'. " + FormatException(exception));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string FormatException(Exception exception)
|
||||||
|
{
|
||||||
|
var result = new StringBuilder();
|
||||||
|
var current = exception;
|
||||||
|
var depth = 0;
|
||||||
|
while (current != null && depth < 10)
|
||||||
|
{
|
||||||
|
if (depth > 0) result.Append(" | inner[").Append(depth).Append("] ");
|
||||||
|
result.Append(current.GetType().FullName)
|
||||||
|
.Append(" hresult=0x")
|
||||||
|
.Append(current.HResult.ToString("X8", CultureInfo.InvariantCulture))
|
||||||
|
.Append(": ")
|
||||||
|
.Append(current.Message);
|
||||||
|
if (!string.IsNullOrWhiteSpace(current.StackTrace))
|
||||||
|
result.Append(" stack=").Append(current.StackTrace);
|
||||||
|
current = current.InnerException;
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
return SingleLine(result.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
lock (sync)
|
||||||
|
{
|
||||||
|
if (writer == null) return;
|
||||||
|
writer.Dispose();
|
||||||
|
writer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CurrentIdentity()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var identity = WindowsIdentity.GetCurrent();
|
||||||
|
return identity == null ? "(unknown)" : identity.Name;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return "(unknown)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SingleLine(string value)
|
||||||
|
{
|
||||||
|
return (value ?? string.Empty)
|
||||||
|
.Replace("\r", "\\r")
|
||||||
|
.Replace("\n", "\\n")
|
||||||
|
.Replace("\t", " ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Globalization;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
@@ -11,20 +13,67 @@ namespace BizTalkCheckmkPulse.Setup
|
|||||||
private const int TaskTriggerTime = 1;
|
private const int TaskTriggerTime = 1;
|
||||||
private const int TaskActionExecute = 0;
|
private const int TaskActionExecute = 0;
|
||||||
private const int TaskInstancesIgnoreNew = 2;
|
private const int TaskInstancesIgnoreNew = 2;
|
||||||
|
private const int TaskStateReady = 3;
|
||||||
private const int TaskStateRunning = 4;
|
private const int TaskStateRunning = 4;
|
||||||
|
|
||||||
public void RegisterAndStart(
|
public void RegisterValidationAndStart(
|
||||||
string taskName,
|
string taskName,
|
||||||
string executable,
|
string executable,
|
||||||
string workingDirectory,
|
string workingDirectory,
|
||||||
string account,
|
string account,
|
||||||
string password,
|
string password,
|
||||||
bool isGmsa)
|
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 service = null;
|
||||||
dynamic root = null;
|
dynamic root = null;
|
||||||
dynamic definition = null;
|
dynamic definition = null;
|
||||||
dynamic registeredTask = null;
|
dynamic registeredTask = null;
|
||||||
|
dynamic runningTask = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var schedulerType = Type.GetTypeFromProgID("Schedule.Service", true);
|
var schedulerType = Type.GetTypeFromProgID("Schedule.Service", true);
|
||||||
@@ -52,12 +101,12 @@ namespace BizTalkCheckmkPulse.Setup
|
|||||||
|
|
||||||
dynamic trigger = definition.Triggers.Create(TaskTriggerTime);
|
dynamic trigger = definition.Triggers.Create(TaskTriggerTime);
|
||||||
trigger.StartBoundary = DateTime.Now.AddMinutes(1).ToString("s");
|
trigger.StartBoundary = DateTime.Now.AddMinutes(1).ToString("s");
|
||||||
trigger.Enabled = true;
|
trigger.Enabled = enableRecurringTrigger;
|
||||||
trigger.Repetition.Interval = "PT1M";
|
trigger.Repetition.Interval = "PT1M";
|
||||||
|
|
||||||
dynamic action = definition.Actions.Create(TaskActionExecute);
|
dynamic action = definition.Actions.Create(TaskActionExecute);
|
||||||
action.Path = executable;
|
action.Path = executable;
|
||||||
action.Arguments = "--collect";
|
action.Arguments = arguments;
|
||||||
action.WorkingDirectory = workingDirectory;
|
action.WorkingDirectory = workingDirectory;
|
||||||
|
|
||||||
registeredTask = root.RegisterTaskDefinition(
|
registeredTask = root.RegisterTaskDefinition(
|
||||||
@@ -68,10 +117,14 @@ namespace BizTalkCheckmkPulse.Setup
|
|||||||
isGmsa ? null : password,
|
isGmsa ? null : password,
|
||||||
TaskLogonPassword,
|
TaskLogonPassword,
|
||||||
null);
|
null);
|
||||||
registeredTask.Run(null);
|
if (startImmediately)
|
||||||
|
{
|
||||||
|
runningTask = registeredTask.Run(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
Release(runningTask);
|
||||||
Release(registeredTask);
|
Release(registeredTask);
|
||||||
Release(definition);
|
Release(definition);
|
||||||
Release(root);
|
Release(root);
|
||||||
@@ -79,6 +132,154 @@ 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 (IsFreshCompletedFailure(lastState, lastResult, lastRunUtc, notBeforeUtc))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Providerlauf ist fehlgeschlagen. state=" + DescribeState(lastState)
|
||||||
|
+ ", last_result=" + DescribeResult(lastResult)
|
||||||
|
+ ", last_run=" + lastRunUtc.Value.ToString("o")
|
||||||
|
+ ". Provider- und Setup-Log pruefen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (report != null && DateTime.UtcNow >= nextReportUtc)
|
||||||
|
{
|
||||||
|
report(
|
||||||
|
"Warte auf erfolgreichen Providerlauf: state=" + DescribeState(lastState)
|
||||||
|
+ ", last_result=" + DescribeResult(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=" + DescribeState(lastState)
|
||||||
|
+ ", last_result=" + DescribeResult(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 == TaskStateReady
|
||||||
|
&& lastTaskResult == 0
|
||||||
|
&& lastRunUtc.HasValue
|
||||||
|
// Task Scheduler kann LastRunTime mit geringerer Aufloesung als DateTime.UtcNow liefern.
|
||||||
|
&& lastRunUtc.Value >= notBeforeUtc.ToUniversalTime().AddSeconds(-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool IsFreshCompletedFailure(
|
||||||
|
int taskState,
|
||||||
|
int lastTaskResult,
|
||||||
|
DateTime? lastRunUtc,
|
||||||
|
DateTime notBeforeUtc)
|
||||||
|
{
|
||||||
|
return taskState == TaskStateReady
|
||||||
|
&& lastTaskResult != 0
|
||||||
|
&& lastRunUtc.HasValue
|
||||||
|
&& lastRunUtc.Value >= notBeforeUtc.ToUniversalTime().AddSeconds(-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string DescribeResult(int result)
|
||||||
|
{
|
||||||
|
var unsigned = unchecked((uint)result);
|
||||||
|
string description;
|
||||||
|
switch (unsigned)
|
||||||
|
{
|
||||||
|
case 0: description = "Erfolgreich"; break;
|
||||||
|
case 1: description = "Provider-Laufzeitfehler; Provider-Log pruefen"; break;
|
||||||
|
case 2: description = "Provider bereits aktiv oder Snapshot-I/O fehlgeschlagen"; break;
|
||||||
|
case 0x00041300: description = "Task ist bereit"; break;
|
||||||
|
case 0x00041301: description = "Task wird ausgefuehrt"; break;
|
||||||
|
case 0x00041302: description = "Task ist deaktiviert"; break;
|
||||||
|
case 0x00041303: description = "Task wurde noch nicht ausgefuehrt"; break;
|
||||||
|
case 0x00041306: description = "Letzter Tasklauf wurde beendet"; break;
|
||||||
|
default:
|
||||||
|
if ((unsigned & 0xFFFF0000u) == 0x80070000u)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
description = new Win32Exception((int)(unsigned & 0xFFFFu)).Message;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
description = "Windows-/Task-Scheduler-Fehler";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
description = "Anwendungs- oder Task-Scheduler-Code";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return result.ToString(CultureInfo.InvariantCulture)
|
||||||
|
+ " (0x" + unsigned.ToString("X8", CultureInfo.InvariantCulture) + ")"
|
||||||
|
+ " '" + description + "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DescribeState(int state)
|
||||||
|
{
|
||||||
|
string name;
|
||||||
|
switch (state)
|
||||||
|
{
|
||||||
|
case 0: name = "Unknown"; break;
|
||||||
|
case 1: name = "Disabled"; break;
|
||||||
|
case 2: name = "Queued"; break;
|
||||||
|
case 3: name = "Ready"; break;
|
||||||
|
case 4: name = "Running"; break;
|
||||||
|
default: name = "Unbekannt"; break;
|
||||||
|
}
|
||||||
|
return state.ToString(CultureInfo.InvariantCulture) + " (" + name + ")";
|
||||||
|
}
|
||||||
|
|
||||||
public void DeleteIfExists(string taskName)
|
public void DeleteIfExists(string taskName)
|
||||||
{
|
{
|
||||||
dynamic service = null;
|
dynamic service = null;
|
||||||
@@ -131,4 +332,16 @@ namespace BizTalkCheckmkPulse.Setup
|
|||||||
if (value != null && Marshal.IsComObject(value)) Marshal.FinalReleaseComObject(value);
|
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="Server" value="." />
|
||||||
<add key="ServicePrefix" value="BizTalk" />
|
<add key="ServicePrefix" value="BizTalk" />
|
||||||
<add key="EnvironmentName" value="" />
|
<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. -->
|
<!-- Provider/Consumer-Datei. Der Scheduled Task schreibt, LocalSystem liest. -->
|
||||||
<add key="SnapshotPath" value="%ProgramData%\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot" />
|
<add key="SnapshotPath" value="%ProgramData%\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot" />
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
<Compile Include="MonitoringOptions.cs" />
|
<Compile Include="MonitoringOptions.cs" />
|
||||||
<Compile Include="Models.cs" />
|
<Compile Include="Models.cs" />
|
||||||
<Compile Include="Program.cs" />
|
<Compile Include="Program.cs" />
|
||||||
|
<Compile Include="RuntimeValidator.cs" />
|
||||||
<Compile Include="SqlConnectivityProbe.cs" />
|
<Compile Include="SqlConnectivityProbe.cs" />
|
||||||
<Compile Include="SnapshotStore.cs" />
|
<Compile Include="SnapshotStore.cs" />
|
||||||
<Compile Include="WmiBizTalkProbe.cs" />
|
<Compile Include="WmiBizTalkProbe.cs" />
|
||||||
|
|||||||
@@ -357,6 +357,8 @@ namespace BizTalkCheckmkPulse
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Formatiert die Netzwerk-Erreichbarkeit aller aktuell aktiven, pruefbaren BizTalk-Endpunkte.
|
/// Formatiert die Netzwerk-Erreichbarkeit aller aktuell aktiven, pruefbaren BizTalk-Endpunkte.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="result">Vollstaendiges Provider-Ergebnis mit Endpoint-Zustand.</param>
|
||||||
|
/// <returns>Eine syntaktisch gueltige Checkmk-Local-Check-Zeile.</returns>
|
||||||
private string FormatEndpointConnectivity(ProbeResult result)
|
private string FormatEndpointConnectivity(ProbeResult result)
|
||||||
{
|
{
|
||||||
var endpointState = result.EndpointConnectivity;
|
var endpointState = result.EndpointConnectivity;
|
||||||
@@ -378,7 +380,12 @@ namespace BizTalkCheckmkPulse
|
|||||||
"Endpoint-Pruefung nicht verlaesslich moeglich. " + EmptyAsUnknown(endpointState.Failure));
|
"Endpoint-Pruefung nicht verlaesslich moeglich. " + EmptyAsUnknown(endpointState.Failure));
|
||||||
}
|
}
|
||||||
|
|
||||||
var failed = endpointState.Results.Where(x => !x.Available).ToArray();
|
var failed = endpointState.Results
|
||||||
|
.Where(x => !x.Available && !x.Endpoint.BestEffort)
|
||||||
|
.ToArray();
|
||||||
|
var ignoredBestEffort = endpointState.Results
|
||||||
|
.Where(x => !x.Available && x.Endpoint.BestEffort)
|
||||||
|
.ToArray();
|
||||||
var available = endpointState.Results.Count(x => x.Available);
|
var available = endpointState.Results.Count(x => x.Available);
|
||||||
var incomplete = endpointState.Results.Count != endpointState.Active
|
var incomplete = endpointState.Results.Count != endpointState.Active
|
||||||
|| endpointState.UnresolvedActive > 0
|
|| endpointState.UnresolvedActive > 0
|
||||||
@@ -391,7 +398,7 @@ namespace BizTalkCheckmkPulse
|
|||||||
: CheckState.Ok;
|
: CheckState.Ok;
|
||||||
var metrics = string.Format(
|
var metrics = string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
"biztalk_endpoints_configured={0};;;0|biztalk_endpoints_active={1};;;0|biztalk_endpoints_unique_targets={2};;;0|biztalk_endpoints_tested={3};;;0|biztalk_endpoints_available={4};;;0|biztalk_endpoints_failed={5};;1;0|biztalk_endpoints_unresolved={6};;1;0|biztalk_endpoints_expected_non_socket={7};;;0|biztalk_endpoints_manual_overrides={8};;;0|biztalk_endpoints_inactive_skipped={9};;;0|biztalk_endpoint_probe_duration_ms={10};;;0",
|
"biztalk_endpoints_configured={0};;;0|biztalk_endpoints_active={1};;;0|biztalk_endpoints_unique_targets={2};;;0|biztalk_endpoints_tested={3};;;0|biztalk_endpoints_available={4};;;0|biztalk_endpoints_failed={5};;1;0|biztalk_endpoints_unresolved={6};;1;0|biztalk_endpoints_expected_non_socket={7};;;0|biztalk_endpoints_best_effort={8};;;0|biztalk_endpoints_best_effort_ignored={9};;;0|biztalk_endpoints_manual_overrides={10};;;0|biztalk_endpoints_inactive_skipped={11};;;0|biztalk_endpoint_probe_duration_ms={12};;;0",
|
||||||
endpointState.Configured,
|
endpointState.Configured,
|
||||||
endpointState.Active,
|
endpointState.Active,
|
||||||
endpointState.UniqueTargets,
|
endpointState.UniqueTargets,
|
||||||
@@ -400,15 +407,21 @@ namespace BizTalkCheckmkPulse
|
|||||||
failed.Length,
|
failed.Length,
|
||||||
endpointState.UnresolvedActive,
|
endpointState.UnresolvedActive,
|
||||||
endpointState.ExpectedNonProbeableActive,
|
endpointState.ExpectedNonProbeableActive,
|
||||||
|
endpointState.BestEffortActive,
|
||||||
|
ignoredBestEffort.Length,
|
||||||
endpointState.ManualOverridesActive,
|
endpointState.ManualOverridesActive,
|
||||||
endpointState.SkippedInactive,
|
endpointState.SkippedInactive,
|
||||||
endpointState.ProbeDurationMilliseconds);
|
endpointState.ProbeDurationMilliseconds);
|
||||||
var detail = new StringBuilder();
|
var detail = new StringBuilder();
|
||||||
if (failed.Length == 0 && !incomplete)
|
if (failed.Length == 0 && !incomplete)
|
||||||
{
|
{
|
||||||
detail.Append("Alle ").Append(endpointState.Active).Append(" aktiven, pruefbaren Send-/Receive-Endpunkte sind erreichbar")
|
detail.Append("Alle regulaer alarmierenden aktiven Send-/Receive-Endpunkte sind erreichbar")
|
||||||
.Append(" (unique_targets=").Append(endpointState.UniqueTargets)
|
.Append(" (active=").Append(endpointState.Active)
|
||||||
|
.Append(", available=").Append(available)
|
||||||
|
.Append(", unique_targets=").Append(endpointState.UniqueTargets)
|
||||||
.Append(", expected_non_socket=").Append(endpointState.ExpectedNonProbeableActive)
|
.Append(", expected_non_socket=").Append(endpointState.ExpectedNonProbeableActive)
|
||||||
|
.Append(", best_effort=").Append(endpointState.BestEffortActive)
|
||||||
|
.Append(", best_effort_ignored=").Append(ignoredBestEffort.Length)
|
||||||
.Append(", manual_overrides=").Append(endpointState.ManualOverridesActive)
|
.Append(", manual_overrides=").Append(endpointState.ManualOverridesActive)
|
||||||
.Append(", probe_ms=").Append(endpointState.ProbeDurationMilliseconds).Append(")");
|
.Append(", probe_ms=").Append(endpointState.ProbeDurationMilliseconds).Append(")");
|
||||||
}
|
}
|
||||||
@@ -420,6 +433,8 @@ namespace BizTalkCheckmkPulse
|
|||||||
.Append(", failed=").Append(failed.Length)
|
.Append(", failed=").Append(failed.Length)
|
||||||
.Append(", unresolved=").Append(endpointState.UnresolvedActive)
|
.Append(", unresolved=").Append(endpointState.UnresolvedActive)
|
||||||
.Append(", expected_non_socket=").Append(endpointState.ExpectedNonProbeableActive)
|
.Append(", expected_non_socket=").Append(endpointState.ExpectedNonProbeableActive)
|
||||||
|
.Append(", best_effort=").Append(endpointState.BestEffortActive)
|
||||||
|
.Append(", best_effort_ignored=").Append(ignoredBestEffort.Length)
|
||||||
.Append(", manual_overrides=").Append(endpointState.ManualOverridesActive)
|
.Append(", manual_overrides=").Append(endpointState.ManualOverridesActive)
|
||||||
.Append(", unique_targets=").Append(endpointState.UniqueTargets)
|
.Append(", unique_targets=").Append(endpointState.UniqueTargets)
|
||||||
.Append(", probe_ms=").Append(endpointState.ProbeDurationMilliseconds);
|
.Append(", probe_ms=").Append(endpointState.ProbeDurationMilliseconds);
|
||||||
|
|||||||
@@ -13,9 +13,17 @@ namespace BizTalkCheckmkPulse
|
|||||||
@"^(?<host>\[[0-9a-fA-F:]+\]|[a-zA-Z0-9._-]+):(?<port>\d{1,5})(?:[/\\].*)?$",
|
@"^(?<host>\[[0-9a-fA-F:]+\]|[a-zA-Z0-9._-]+):(?<port>\d{1,5})(?:[/\\].*)?$",
|
||||||
RegexOptions.CultureInvariant);
|
RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
|
private static readonly Regex DatabaseProviderPattern = new Regex(
|
||||||
|
@"/(?:Oracle|SQLServer)(?:[/;?]|$)",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Erstellt aus einer aktiven BizTalk-Transportadresse einen TCP-/UDP-Katalogeintrag.
|
/// Erstellt aus einer aktiven BizTalk-Transportadresse einen TCP-/UDP-Katalogeintrag.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="candidate">Aktiver BizTalk-Transport mit Adaptername und Adresse.</param>
|
||||||
|
/// <param name="entry">Geheimnisfreier Katalogeintrag, sofern ein Ziel bestimmt wurde.</param>
|
||||||
|
/// <param name="reason">Begruendung, wenn kein pruefbares Ziel bestimmt wurde.</param>
|
||||||
|
/// <returns><see langword="true"/>, wenn Host, Port und Protokoll sicher bestimmt wurden.</returns>
|
||||||
public static bool TryCreate(
|
public static bool TryCreate(
|
||||||
EndpointCandidate candidate,
|
EndpointCandidate candidate,
|
||||||
out EndpointCatalogEntry entry,
|
out EndpointCatalogEntry entry,
|
||||||
@@ -30,6 +38,8 @@ namespace BizTalkCheckmkPulse
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Trennt sicher pruefbare Ziele von erwartbar lokalen/adapterinternen Adressen und echten Aufloesungsluecken.
|
/// Trennt sicher pruefbare Ziele von erwartbar lokalen/adapterinternen Adressen und echten Aufloesungsluecken.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="candidate">Zu klassifizierender BizTalk-Transport.</param>
|
||||||
|
/// <returns>Klassifizierung sowie gegebenenfalls das pruefbare Socket-Ziel.</returns>
|
||||||
public static EndpointResolution Analyze(EndpointCandidate candidate)
|
public static EndpointResolution Analyze(EndpointCandidate candidate)
|
||||||
{
|
{
|
||||||
if (candidate == null)
|
if (candidate == null)
|
||||||
@@ -42,6 +52,13 @@ namespace BizTalkCheckmkPulse
|
|||||||
return Expected("Dynamischer Send Port besitzt kein statisch pruefbares Ziel.");
|
return Expected("Dynamischer Send Port besitzt kein statisch pruefbares Ziel.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scheduling-Adapter bilden zeitliche BizTalk-Steuerung ab und besitzen kein
|
||||||
|
// fachlich relevantes Remoteziel. Sie werden bewusst vollstaendig ausgeklammert.
|
||||||
|
if (IsScheduler(candidate))
|
||||||
|
{
|
||||||
|
return Expected("Scheduling-Komponente ist von der Endpoint-Pruefung ausgenommen.");
|
||||||
|
}
|
||||||
|
|
||||||
var address = (candidate.Address ?? string.Empty).Trim();
|
var address = (candidate.Address ?? string.Empty).Trim();
|
||||||
if (address.Length == 0)
|
if (address.Length == 0)
|
||||||
{
|
{
|
||||||
@@ -80,6 +97,33 @@ namespace BizTalkCheckmkPulse
|
|||||||
return Expected("Relative Receive-Listener-Adresse enthaelt keine eindeutige Site-Bindung.");
|
return Expected("Relative Receive-Listener-Adresse enthaelt keine eindeutige Site-Bindung.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DATABASE und WCF-SAP liefern Adapteradressen, deren Socket-Ziel nicht von
|
||||||
|
// System.Uri abgeleitet werden kann. Eine fehlende sichere Ableitung ist bei
|
||||||
|
// diesen Best-Effort-Adaptern kein Abdeckungsfehler.
|
||||||
|
if (IsDatabaseAdapter(candidate))
|
||||||
|
{
|
||||||
|
EndpointCatalogEntry databaseEntry;
|
||||||
|
string databaseReason;
|
||||||
|
if (TryResolveDatabase(candidate, address, out databaseEntry, out databaseReason))
|
||||||
|
{
|
||||||
|
return Probeable(databaseEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Expected(databaseReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsWcfSapAdapter(candidate))
|
||||||
|
{
|
||||||
|
EndpointCatalogEntry sapEntry;
|
||||||
|
string sapReason;
|
||||||
|
if (TryResolveWcfSap(candidate, address, out sapEntry, out sapReason))
|
||||||
|
{
|
||||||
|
return Probeable(sapEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Expected(sapReason);
|
||||||
|
}
|
||||||
|
|
||||||
EndpointCatalogEntry entry;
|
EndpointCatalogEntry entry;
|
||||||
string protocol;
|
string protocol;
|
||||||
string host;
|
string host;
|
||||||
@@ -115,8 +159,306 @@ namespace BizTalkCheckmkPulse
|
|||||||
Host = host,
|
Host = host,
|
||||||
Port = port,
|
Port = port,
|
||||||
Enabled = true,
|
Enabled = true,
|
||||||
AutoDiscovered = true
|
AutoDiscovered = true,
|
||||||
|
BestEffort = false
|
||||||
};
|
};
|
||||||
|
return Probeable(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unterscheidet externe, aber nicht automatisch aufloesbare Ziele von bewusst lokalen/variablen Adressen.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="candidate">Zu klassifizierender BizTalk-Transport.</param>
|
||||||
|
/// <returns><see langword="true"/> nur bei einer echten Aufloesungsluecke.</returns>
|
||||||
|
public static bool IsPotentialExternalEndpoint(EndpointCandidate candidate)
|
||||||
|
{
|
||||||
|
return Analyze(candidate).Status == EndpointResolutionStatus.Unresolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Leitet das Oracle-TCP-Ziel aus der expliziten DATABASE-Adresse ab.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="candidate">BizTalk-Artefakt, aus dem der Katalogeintrag aufgebaut wird.</param>
|
||||||
|
/// <param name="address">Vollstaendige DATABASE-Transportadresse.</param>
|
||||||
|
/// <param name="entry">Abgeleiteter geheimnisfreier Best-Effort-Eintrag.</param>
|
||||||
|
/// <param name="reason">Begruendung, wenn Host oder Anbieter nicht sicher erkennbar ist.</param>
|
||||||
|
/// <returns><see langword="true"/>, wenn ein Oracle-Host und TCP-Port bestimmt wurden.</returns>
|
||||||
|
private static bool TryResolveDatabase(
|
||||||
|
EndpointCandidate candidate,
|
||||||
|
string address,
|
||||||
|
out EndpointCatalogEntry entry,
|
||||||
|
out string reason)
|
||||||
|
{
|
||||||
|
entry = null;
|
||||||
|
reason = "DATABASE-Adresse enthaelt keinen sicher bestimmbaren Oracle-Listener.";
|
||||||
|
if (!address.StartsWith("database://", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| !DatabaseProviderPattern.IsMatch(address))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uri uri;
|
||||||
|
if (!Uri.TryCreate(address, UriKind.Absolute, out uri)
|
||||||
|
|| string.IsNullOrWhiteSpace(uri.Host))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var oracle = Regex.IsMatch(
|
||||||
|
address,
|
||||||
|
@"/Oracle(?:[/;?]|$)",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
if (!oracle)
|
||||||
|
{
|
||||||
|
reason = "Nicht-Oracle-DATABASE-Ziel wird ohne explizite Betriebsfreigabe nicht automatisch geprueft.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var port = HasExplicitPort(address) ? uri.Port : 1521;
|
||||||
|
string validationReason;
|
||||||
|
var host = NormalizeHost(uri.Host);
|
||||||
|
if (!Validate(host, port, out validationReason))
|
||||||
|
{
|
||||||
|
reason = validationReason;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry = CreateAdapterEntry(candidate, host, port);
|
||||||
|
reason = string.Empty;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Leitet den ersten erreichbaren TCP-Hop einer WCF-SAP-Verbindung ab.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="candidate">BizTalk-Artefakt, aus dem der Katalogeintrag aufgebaut wird.</param>
|
||||||
|
/// <param name="address">Vollstaendige SAP-Verbindungs-URI.</param>
|
||||||
|
/// <param name="entry">Abgeleiteter geheimnisfreier Best-Effort-Eintrag.</param>
|
||||||
|
/// <param name="reason">Begruendung, wenn Gateway oder SAProuter nicht sicher bestimmbar ist.</param>
|
||||||
|
/// <returns><see langword="true"/>, wenn ein SAProuter- oder Gateway-Ziel bestimmt wurde.</returns>
|
||||||
|
private static bool TryResolveWcfSap(
|
||||||
|
EndpointCandidate candidate,
|
||||||
|
string address,
|
||||||
|
out EndpointCatalogEntry entry,
|
||||||
|
out string reason)
|
||||||
|
{
|
||||||
|
entry = null;
|
||||||
|
reason = "WCF-SAP-Adresse enthaelt keinen sicher bestimmbaren Gateway- oder SAProuter-Port.";
|
||||||
|
var decoded = DecodeAdapterAddress(address);
|
||||||
|
|
||||||
|
string route;
|
||||||
|
if (TryReadParameter(decoded, "SAPROUTER", out route))
|
||||||
|
{
|
||||||
|
string routerHost;
|
||||||
|
int routerPort;
|
||||||
|
if (TryParseSapRouter(route, out routerHost, out routerPort))
|
||||||
|
{
|
||||||
|
entry = CreateAdapterEntry(candidate, routerHost, routerPort);
|
||||||
|
reason = string.Empty;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string gatewayHost;
|
||||||
|
string gatewayService;
|
||||||
|
if ((!TryReadParameter(decoded, "ListenerGwHost", out gatewayHost)
|
||||||
|
|| !TryReadParameter(decoded, "ListenerGwServ", out gatewayService))
|
||||||
|
&& (!TryReadParameter(decoded, "GwHost", out gatewayHost)
|
||||||
|
|| !TryReadParameter(decoded, "GwServ", out gatewayService)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int gatewayPort;
|
||||||
|
if (!TryResolveSapService(gatewayService, out gatewayPort))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
gatewayHost = NormalizeHost(gatewayHost);
|
||||||
|
string validationReason;
|
||||||
|
if (!Validate(gatewayHost, gatewayPort, out validationReason))
|
||||||
|
{
|
||||||
|
reason = validationReason;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry = CreateAdapterEntry(candidate, gatewayHost, gatewayPort);
|
||||||
|
reason = string.Empty;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Baut einen TCP-Katalogeintrag fuer einen automatisch abgeleiteten Best-Effort-Adapter.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="candidate">Quellartefakt fuer Metadaten und stabilen Schluessel.</param>
|
||||||
|
/// <param name="host">Bereinigter DNS-Name oder IP-Adresse.</param>
|
||||||
|
/// <param name="port">Gueltiger TCP-Zielport.</param>
|
||||||
|
/// <returns>Automatisch verwalteter, nicht alarmierender Katalogeintrag.</returns>
|
||||||
|
private static EndpointCatalogEntry CreateAdapterEntry(EndpointCandidate candidate, string host, int port)
|
||||||
|
{
|
||||||
|
return new EndpointCatalogEntry
|
||||||
|
{
|
||||||
|
Key = candidate.Key,
|
||||||
|
ArtifactType = candidate.ArtifactType,
|
||||||
|
ApplicationName = candidate.ApplicationName,
|
||||||
|
ArtifactName = candidate.ArtifactName,
|
||||||
|
TransportRole = candidate.TransportRole,
|
||||||
|
AdapterName = candidate.AdapterName,
|
||||||
|
Protocol = "TCP",
|
||||||
|
Host = host,
|
||||||
|
Port = port,
|
||||||
|
Enabled = true,
|
||||||
|
AutoDiscovered = true,
|
||||||
|
BestEffort = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Liest einen URI-Parameter auch aus SAP-URIs mit wiederholtem Fragezeichen.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">Dekodierte SAP-Verbindungsadresse.</param>
|
||||||
|
/// <param name="name">Name des gesuchten Query-Parameters.</param>
|
||||||
|
/// <param name="value">Bereinigter Parameterwert.</param>
|
||||||
|
/// <returns><see langword="true"/>, wenn ein nicht leerer Wert gefunden wurde.</returns>
|
||||||
|
private static bool TryReadParameter(string address, string name, out string value)
|
||||||
|
{
|
||||||
|
value = string.Empty;
|
||||||
|
var match = Regex.Match(
|
||||||
|
address ?? string.Empty,
|
||||||
|
@"(?:^|[?&])" + Regex.Escape(name) + @"=(?<value>[^?&]+)",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = match.Groups["value"].Value.Trim();
|
||||||
|
return value.Length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dekodiert URI-Escapes, ohne eine fehlerhafte Adapteradresse zum Laufzeitfehler zu machen.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="address">Rohe BizTalk-Transportadresse.</param>
|
||||||
|
/// <returns>Bestmoeglich dekodierte Adresse.</returns>
|
||||||
|
private static string DecodeAdapterAddress(string address)
|
||||||
|
{
|
||||||
|
var normalized = (address ?? string.Empty).Replace("&", "&");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Uri.UnescapeDataString(normalized);
|
||||||
|
}
|
||||||
|
catch (UriFormatException)
|
||||||
|
{
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bestimmt Host und Port des ersten SAProuter-Hops einer Route.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="route">SAProuter-Route im Format <c>/H/host/S/port</c>.</param>
|
||||||
|
/// <param name="host">Host des ersten Router-Hops.</param>
|
||||||
|
/// <param name="port">Expliziter Port oder SAProuter-Standardport 3299.</param>
|
||||||
|
/// <returns><see langword="true"/>, wenn der erste Hop gueltig ist.</returns>
|
||||||
|
private static bool TryParseSapRouter(string route, out string host, out int port)
|
||||||
|
{
|
||||||
|
host = string.Empty;
|
||||||
|
port = 0;
|
||||||
|
var match = Regex.Match(
|
||||||
|
route ?? string.Empty,
|
||||||
|
@"/H/(?<host>[^/]+)(?:/S/(?<service>[^/]+))?",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
host = NormalizeHost(match.Groups["host"].Value);
|
||||||
|
port = 3299;
|
||||||
|
var service = match.Groups["service"].Value;
|
||||||
|
if (service.Length > 0 && !TryResolveSapService(service, out port))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string reason;
|
||||||
|
return Validate(host, port, out reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Uebersetzt numerische SAP-Dienste und sapgwNN in einen TCP-Port.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="service">Numerischer Port oder SAP-Gateway-Dienstname.</param>
|
||||||
|
/// <param name="port">Aufgeloester TCP-Port.</param>
|
||||||
|
/// <returns><see langword="true"/> bei einem gueltigen Port oder <c>sapgwNN</c>.</returns>
|
||||||
|
private static bool TryResolveSapService(string service, out int port)
|
||||||
|
{
|
||||||
|
port = 0;
|
||||||
|
var normalized = (service ?? string.Empty).Trim();
|
||||||
|
if (int.TryParse(normalized, out port))
|
||||||
|
{
|
||||||
|
return port >= 1 && port <= 65535;
|
||||||
|
}
|
||||||
|
|
||||||
|
var match = Regex.Match(
|
||||||
|
normalized,
|
||||||
|
@"^sapgw(?<instance>\d{2})$",
|
||||||
|
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||||
|
int instance;
|
||||||
|
if (!match.Success || !int.TryParse(match.Groups["instance"].Value, out instance))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
port = 3300 + instance;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Erkennt BizTalk-Scheduling-Komponenten, die kein Remoteziel darstellen.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="candidate">Zu pruefender BizTalk-Transport.</param>
|
||||||
|
/// <returns><see langword="true"/> fuer Schedule-/Scheduler-Konfigurationen.</returns>
|
||||||
|
private static bool IsScheduler(EndpointCandidate candidate)
|
||||||
|
{
|
||||||
|
return Contains(candidate.AdapterName, "SCHEDUL")
|
||||||
|
|| (candidate.Address ?? string.Empty).StartsWith("schedule", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Erkennt den kundenspezifischen DATABASE-Adapter und dessen URI-Schema.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="candidate">Zu pruefender BizTalk-Transport.</param>
|
||||||
|
/// <returns><see langword="true"/> fuer DATABASE-Konfigurationen.</returns>
|
||||||
|
private static bool IsDatabaseAdapter(EndpointCandidate candidate)
|
||||||
|
{
|
||||||
|
return Contains(candidate.AdapterName, "DATABASE")
|
||||||
|
|| (candidate.Address ?? string.Empty).StartsWith("database://", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Erkennt WCF-SAP-Adapter, ohne beliebige Adapter mit SAP im Namen einzubeziehen.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="candidate">Zu pruefender BizTalk-Transport.</param>
|
||||||
|
/// <returns><see langword="true"/> fuer eindeutig erkannte WCF-SAP-Konfigurationen.</returns>
|
||||||
|
private static bool IsWcfSapAdapter(EndpointCandidate candidate)
|
||||||
|
{
|
||||||
|
return Contains(candidate.AdapterName, "WCF-SAP")
|
||||||
|
|| Contains(candidate.AdapterName, "WCF SAP")
|
||||||
|
// BizTalk meldet den SAP Binding-Typ in manchen Umgebungen nur als
|
||||||
|
// WCF-Custom; das eindeutige sap://-Schema bleibt dennoch erhalten.
|
||||||
|
|| (candidate.Address ?? string.Empty).StartsWith("sap://", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verpackt einen gueltigen Katalogeintrag als pruefbare Aufloesung.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="entry">Vollstaendig validierter Katalogeintrag.</param>
|
||||||
|
/// <returns>Pruefbare Endpoint-Aufloesung ohne Fehlergrund.</returns>
|
||||||
|
private static EndpointResolution Probeable(EndpointCatalogEntry entry)
|
||||||
|
{
|
||||||
return new EndpointResolution
|
return new EndpointResolution
|
||||||
{
|
{
|
||||||
Status = EndpointResolutionStatus.Probeable,
|
Status = EndpointResolutionStatus.Probeable,
|
||||||
@@ -125,14 +467,6 @@ namespace BizTalkCheckmkPulse
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unterscheidet externe, aber nicht automatisch aufloesbare Ziele von bewusst lokalen/variablen Adressen.
|
|
||||||
/// </summary>
|
|
||||||
public static bool IsPotentialExternalEndpoint(EndpointCandidate candidate)
|
|
||||||
{
|
|
||||||
return Analyze(candidate).Status == EndpointResolutionStatus.Unresolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryResolve(
|
private static bool TryResolve(
|
||||||
string address,
|
string address,
|
||||||
string adapterName,
|
string adapterName,
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ namespace BizTalkCheckmkPulse
|
|||||||
private readonly int _maxBytes;
|
private readonly int _maxBytes;
|
||||||
private readonly int _maxEntries;
|
private readonly int _maxEntries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialisiert den validierenden XML-Katalogspeicher.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="path">Absoluter Zielpfad der Katalogdatei.</param>
|
||||||
|
/// <param name="maxBytes">Maximal akzeptierte Dateigroesse.</param>
|
||||||
|
/// <param name="maxEntries">Maximal akzeptierte Anzahl Eintraege.</param>
|
||||||
public EndpointCatalogStore(string path, int maxBytes, int maxEntries)
|
public EndpointCatalogStore(string path, int maxBytes, int maxEntries)
|
||||||
{
|
{
|
||||||
_path = path;
|
_path = path;
|
||||||
@@ -27,6 +33,11 @@ namespace BizTalkCheckmkPulse
|
|||||||
_maxEntries = maxEntries;
|
_maxEntries = maxEntries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Liest und validiert einen maschinen- und umgebungsgebundenen Katalog.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="environmentName">Erwartete Monitoring-Umgebung.</param>
|
||||||
|
/// <returns>Vollstaendig validierter Endpoint-Katalog.</returns>
|
||||||
public EndpointCatalog Read(string environmentName)
|
public EndpointCatalog Read(string environmentName)
|
||||||
{
|
{
|
||||||
var info = new FileInfo(_path);
|
var info = new FileInfo(_path);
|
||||||
@@ -112,7 +123,9 @@ namespace BizTalkCheckmkPulse
|
|||||||
Host = ReadAttribute(node, "host"),
|
Host = ReadAttribute(node, "host"),
|
||||||
Port = ReadInt(node, "port", 1, 65535),
|
Port = ReadInt(node, "port", 1, 65535),
|
||||||
Enabled = ReadBool(node, "enabled"),
|
Enabled = ReadBool(node, "enabled"),
|
||||||
AutoDiscovered = ReadBool(node, "autoDiscovered")
|
AutoDiscovered = ReadBool(node, "autoDiscovered"),
|
||||||
|
// Das optionale Attribut haelt bestehende Kataloge der Version 1 lesbar.
|
||||||
|
BestEffort = ReadOptionalBool(node, "bestEffort", false)
|
||||||
};
|
};
|
||||||
ValidateEntry(entry);
|
ValidateEntry(entry);
|
||||||
catalog.Entries.Add(entry);
|
catalog.Entries.Add(entry);
|
||||||
@@ -126,6 +139,10 @@ namespace BizTalkCheckmkPulse
|
|||||||
return catalog;
|
return catalog;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validiert und ersetzt den Endpoint-Katalog atomar.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="catalog">Zu persistierender geheimnisfreier Katalog.</param>
|
||||||
public void Write(EndpointCatalog catalog)
|
public void Write(EndpointCatalog catalog)
|
||||||
{
|
{
|
||||||
if (catalog == null)
|
if (catalog == null)
|
||||||
@@ -170,7 +187,8 @@ namespace BizTalkCheckmkPulse
|
|||||||
new XAttribute("host", entry.Host),
|
new XAttribute("host", entry.Host),
|
||||||
new XAttribute("port", entry.Port),
|
new XAttribute("port", entry.Port),
|
||||||
new XAttribute("enabled", entry.Enabled),
|
new XAttribute("enabled", entry.Enabled),
|
||||||
new XAttribute("autoDiscovered", entry.AutoDiscovered)));
|
new XAttribute("autoDiscovered", entry.AutoDiscovered),
|
||||||
|
new XAttribute("bestEffort", entry.BestEffort)));
|
||||||
}
|
}
|
||||||
|
|
||||||
var document = new XDocument(new XDeclaration("1.0", "utf-8", null), root);
|
var document = new XDocument(new XDeclaration("1.0", "utf-8", null), root);
|
||||||
@@ -284,5 +302,29 @@ namespace BizTalkCheckmkPulse
|
|||||||
|
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Liest ein optionales boolesches Attribut mit rueckwaertskompatiblem Standardwert.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="element">XML-Element mit dem optionalen Attribut.</param>
|
||||||
|
/// <param name="name">Name des Attributs.</param>
|
||||||
|
/// <param name="defaultValue">Rueckgabewert, wenn das Attribut fehlt.</param>
|
||||||
|
/// <returns>Gelesener Wert oder der angegebene Standardwert.</returns>
|
||||||
|
private static bool ReadOptionalBool(XElement element, string name, bool defaultValue)
|
||||||
|
{
|
||||||
|
var attribute = element.Attribute(name);
|
||||||
|
if (attribute == null)
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool value;
|
||||||
|
if (!bool.TryParse(attribute.Value.Trim(), out value))
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("Endpoint catalog boolean is invalid: " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,12 +18,21 @@ namespace BizTalkCheckmkPulse
|
|||||||
private readonly MonitoringOptions _options;
|
private readonly MonitoringOptions _options;
|
||||||
private readonly FileLogger _logger;
|
private readonly FileLogger _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialisiert die Endpoint-Pruefung mit Laufzeitgrenzen und Dateiprotokoll.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options">Validierte Monitoring-Konfiguration.</param>
|
||||||
|
/// <param name="logger">Gemeinsames Provider-Dateiprotokoll.</param>
|
||||||
public EndpointConnectivityProbe(MonitoringOptions options, FileLogger logger)
|
public EndpointConnectivityProbe(MonitoringOptions options, FileLogger logger)
|
||||||
{
|
{
|
||||||
_options = options;
|
_options = options;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Synchronisiert den Katalog und ergaenzt das Gesamtergebnis um Socket-Probes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="result">Bereits mit aktuellen BizTalk-Artefakten gefuelltes Ergebnis.</param>
|
||||||
public void Query(ProbeResult result)
|
public void Query(ProbeResult result)
|
||||||
{
|
{
|
||||||
var state = result.EndpointConnectivity;
|
var state = result.EndpointConnectivity;
|
||||||
@@ -62,7 +71,8 @@ namespace BizTalkCheckmkPulse
|
|||||||
}
|
}
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
state.RefreshRequired = catalog == null
|
state.RefreshRequired = _options.ForceEndpointCatalogRefresh
|
||||||
|
|| catalog == null
|
||||||
|| catalog.SynchronizedUtc > now.AddMinutes(5)
|
|| catalog.SynchronizedUtc > now.AddMinutes(5)
|
||||||
|| now - catalog.SynchronizedUtc >= TimeSpan.FromHours(_options.EndpointDiscoveryIntervalHours);
|
|| now - catalog.SynchronizedUtc >= TimeSpan.FromHours(_options.EndpointDiscoveryIntervalHours);
|
||||||
if (state.RefreshRequired)
|
if (state.RefreshRequired)
|
||||||
@@ -140,7 +150,13 @@ namespace BizTalkCheckmkPulse
|
|||||||
foreach (var probeResult in probeResults)
|
foreach (var probeResult in probeResults)
|
||||||
{
|
{
|
||||||
state.Results.Add(probeResult);
|
state.Results.Add(probeResult);
|
||||||
if (!probeResult.Available)
|
if (!probeResult.Available && probeResult.Endpoint.BestEffort)
|
||||||
|
{
|
||||||
|
// DATABASE-/WCF-SAP-Ziele sind bewusst Diagnose-Probes: Ein
|
||||||
|
// Fehlschlag darf weder Tageslog noch Checkmk mit Alarmen fluten.
|
||||||
|
state.BestEffortIgnoredFailures++;
|
||||||
|
}
|
||||||
|
else if (!probeResult.Available)
|
||||||
{
|
{
|
||||||
_logger.Warning(
|
_logger.Warning(
|
||||||
"Endpoint unavailable. endpoint=" + Display(probeResult.Endpoint)
|
"Endpoint unavailable. endpoint=" + Display(probeResult.Endpoint)
|
||||||
@@ -156,6 +172,13 @@ namespace BizTalkCheckmkPulse
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fuehrt manuelle Overrides mit den aktuell automatisch erkannten Zielen zusammen.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="existing">Vorhandener Katalog oder <see langword="null"/>.</param>
|
||||||
|
/// <param name="candidates">Aktuelle BizTalk-Send-/Receive-Transporte.</param>
|
||||||
|
/// <param name="synchronizedUtc">UTC-Zeitpunkt des vollstaendigen Abgleichs.</param>
|
||||||
|
/// <returns>Neuer, geheimnisfreier Endpoint-Katalog.</returns>
|
||||||
internal static EndpointCatalog SynchronizeCatalog(
|
internal static EndpointCatalog SynchronizeCatalog(
|
||||||
EndpointCatalog existing,
|
EndpointCatalog existing,
|
||||||
IEnumerable<EndpointCandidate> candidates,
|
IEnumerable<EndpointCandidate> candidates,
|
||||||
@@ -222,6 +245,10 @@ namespace BizTalkCheckmkPulse
|
|||||||
/// Ermittelt aus aktuellen WMI-Kandidaten und manuellen Overrides die in diesem Lauf zu pruefenden Ziele.
|
/// Ermittelt aus aktuellen WMI-Kandidaten und manuellen Overrides die in diesem Lauf zu pruefenden Ziele.
|
||||||
/// Automatisch erkannte Katalogeintraege werden bewusst nicht als veraltete Laufzeitquelle verwendet.
|
/// Automatisch erkannte Katalogeintraege werden bewusst nicht als veraltete Laufzeitquelle verwendet.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="catalog">Gueltiger lokaler Endpoint-Katalog.</param>
|
||||||
|
/// <param name="candidates">Aktuelle aktive und inaktive BizTalk-Transporte.</param>
|
||||||
|
/// <param name="state">Zu aktualisierende Laufzeitzaehler und Aufloesungsdiagnosen.</param>
|
||||||
|
/// <returns>Aktuell aktive, deduplizierte Katalogeintraege.</returns>
|
||||||
internal static EndpointCatalogEntry[] ResolveActiveEndpoints(
|
internal static EndpointCatalogEntry[] ResolveActiveEndpoints(
|
||||||
EndpointCatalog catalog,
|
EndpointCatalog catalog,
|
||||||
IEnumerable<EndpointCandidate> candidates,
|
IEnumerable<EndpointCandidate> candidates,
|
||||||
@@ -283,10 +310,12 @@ namespace BizTalkCheckmkPulse
|
|||||||
x.Enabled
|
x.Enabled
|
||||||
&& !string.Equals(x.ArtifactType, "Manual", StringComparison.OrdinalIgnoreCase)
|
&& !string.Equals(x.ArtifactType, "Manual", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !activeKeys.Contains(x.Key));
|
&& !activeKeys.Contains(x.Key));
|
||||||
return selected
|
var resolved = selected
|
||||||
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
|
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
|
||||||
.Select(x => x.First())
|
.Select(x => x.First())
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
state.BestEffortActive = resolved.Count(x => x.BestEffort);
|
||||||
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -355,6 +384,13 @@ namespace BizTalkCheckmkPulse
|
|||||||
}).ToArray();
|
}).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Berechnet die theoretische Obergrenze fuer vollstaendig timeoutende Zielgruppen.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="targetCount">Anzahl eindeutiger Socket-Ziele.</param>
|
||||||
|
/// <param name="concurrency">Maximale parallele Probes.</param>
|
||||||
|
/// <param name="timeoutMilliseconds">Timeout je Probe in Millisekunden.</param>
|
||||||
|
/// <returns>Theoretische Worst-Case-Dauer in Millisekunden.</returns>
|
||||||
internal static long CalculateWorstCaseProbeMilliseconds(int targetCount, int concurrency, int timeoutMilliseconds)
|
internal static long CalculateWorstCaseProbeMilliseconds(int targetCount, int concurrency, int timeoutMilliseconds)
|
||||||
{
|
{
|
||||||
if (targetCount <= 0)
|
if (targetCount <= 0)
|
||||||
@@ -464,6 +500,11 @@ namespace BizTalkCheckmkPulse
|
|||||||
: "SocketError=" + socket.SocketErrorCode + ": " + socket.Message;
|
: "SocketError=" + socket.SocketErrorCode + ": " + socket.Message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Formatiert ein geheimnisfreies Socket-Ziel fuer Checkmk und Dateilog.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="endpoint">Zu formatierender Katalogeintrag.</param>
|
||||||
|
/// <returns>Artefakt, Host, Port und Protokoll ohne URI-Geheimnisse.</returns>
|
||||||
internal static string Display(EndpointCatalogEntry endpoint)
|
internal static string Display(EndpointCatalogEntry endpoint)
|
||||||
{
|
{
|
||||||
var artifact = string.IsNullOrWhiteSpace(endpoint.ApplicationName)
|
var artifact = string.IsNullOrWhiteSpace(endpoint.ApplicationName)
|
||||||
|
|||||||
@@ -305,7 +305,20 @@ namespace BizTalkCheckmkPulse
|
|||||||
public int Port { get; set; }
|
public int Port { get; set; }
|
||||||
public bool Enabled { get; set; }
|
public bool Enabled { get; set; }
|
||||||
public bool AutoDiscovered { get; set; }
|
public bool AutoDiscovered { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Gibt an, dass ein automatisch abgeleiteter Adaptertest nur Diagnosewert besitzt
|
||||||
|
/// und ein Fehlschlag deshalb keinen Checkmk-Alarm ausloest.
|
||||||
|
/// </summary>
|
||||||
|
public bool BestEffort { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Erstellt den stabilen, adressunabhaengigen Schluessel eines BizTalk-Transports.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="artifactType">Typ des BizTalk-Artefakts.</param>
|
||||||
|
/// <param name="applicationName">Name der BizTalk-Anwendung.</param>
|
||||||
|
/// <param name="artifactName">Name des Send Ports oder der Receive Location.</param>
|
||||||
|
/// <param name="transportRole">Primaer-, Sekundaer- oder Inbound-Rolle.</param>
|
||||||
|
/// <returns>Normalisierter Katalogschluessel.</returns>
|
||||||
public static string BuildKey(string artifactType, string applicationName, string artifactName, string transportRole)
|
public static string BuildKey(string artifactType, string applicationName, string artifactName, string transportRole)
|
||||||
{
|
{
|
||||||
return NormalizeKeyPart(artifactType)
|
return NormalizeKeyPart(artifactType)
|
||||||
@@ -371,6 +384,14 @@ namespace BizTalkCheckmkPulse
|
|||||||
public int SkippedInactive { get; set; }
|
public int SkippedInactive { get; set; }
|
||||||
public int UnresolvedActive { get; set; }
|
public int UnresolvedActive { get; set; }
|
||||||
public int ExpectedNonProbeableActive { get; set; }
|
public int ExpectedNonProbeableActive { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Anzahl der automatisch abgeleiteten, nicht alarmierenden Adapterziele.
|
||||||
|
/// </summary>
|
||||||
|
public int BestEffortActive { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Anzahl fehlgeschlagener Best-Effort-Probes im aktuellen Lauf.
|
||||||
|
/// </summary>
|
||||||
|
public int BestEffortIgnoredFailures { get; set; }
|
||||||
public int ManualOverridesActive { get; set; }
|
public int ManualOverridesActive { get; set; }
|
||||||
public int UniqueTargets { get; set; }
|
public int UniqueTargets { get; set; }
|
||||||
public long ProbeDurationMilliseconds { get; set; }
|
public long ProbeDurationMilliseconds { get; set; }
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Configuration;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace BizTalkCheckmkPulse
|
namespace BizTalkCheckmkPulse
|
||||||
{
|
{
|
||||||
@@ -15,6 +16,7 @@ namespace BizTalkCheckmkPulse
|
|||||||
public string Server { get; set; }
|
public string Server { get; set; }
|
||||||
public string ServicePrefix { get; set; }
|
public string ServicePrefix { get; set; }
|
||||||
public string EnvironmentName { get; set; }
|
public string EnvironmentName { get; set; }
|
||||||
|
public bool IncludeEnvironmentInServiceName { get; set; }
|
||||||
public int QueryTimeoutSeconds { get; set; }
|
public int QueryTimeoutSeconds { get; set; }
|
||||||
public bool ProbeSqlConnectivity { get; set; }
|
public bool ProbeSqlConnectivity { get; set; }
|
||||||
public int SqlConnectionTimeoutSeconds { get; set; }
|
public int SqlConnectionTimeoutSeconds { get; set; }
|
||||||
@@ -48,6 +50,10 @@ namespace BizTalkCheckmkPulse
|
|||||||
public int EndpointMaxCount { get; set; }
|
public int EndpointMaxCount { get; set; }
|
||||||
public bool Collect { get; set; }
|
public bool Collect { get; set; }
|
||||||
public bool SelfTest { 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>
|
/// <summary>
|
||||||
/// Setzt konservative Standardwerte fuer BizTalk Server 2020.
|
/// Setzt konservative Standardwerte fuer BizTalk Server 2020.
|
||||||
@@ -57,6 +63,7 @@ namespace BizTalkCheckmkPulse
|
|||||||
Server = ".";
|
Server = ".";
|
||||||
ServicePrefix = "BizTalk";
|
ServicePrefix = "BizTalk";
|
||||||
EnvironmentName = string.Empty;
|
EnvironmentName = string.Empty;
|
||||||
|
IncludeEnvironmentInServiceName = false;
|
||||||
QueryTimeoutSeconds = 25;
|
QueryTimeoutSeconds = 25;
|
||||||
ProbeSqlConnectivity = true;
|
ProbeSqlConnectivity = true;
|
||||||
SqlConnectionTimeoutSeconds = 5;
|
SqlConnectionTimeoutSeconds = 5;
|
||||||
@@ -89,16 +96,18 @@ namespace BizTalkCheckmkPulse
|
|||||||
EndpointProbeTimeoutMilliseconds = 3000;
|
EndpointProbeTimeoutMilliseconds = 3000;
|
||||||
EndpointProbeMaxConcurrency = 16;
|
EndpointProbeMaxConcurrency = 16;
|
||||||
EndpointMaxCount = 100;
|
EndpointMaxCount = 100;
|
||||||
|
ExpectedRuntimeIdentity = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Erzeugt den stabilen Checkmk-Service-Namen mit optionalem Umgebungsnamen.
|
/// Erzeugt den stabilen Checkmk-Service-Namen. Der Umgebungsname ist nur explizit opt-in,
|
||||||
|
/// damit ein Installer-Update bestehende Checkmk-Services nicht unbemerkt umbenennt.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="suffix">Fachlicher Suffix des Services.</param>
|
/// <param name="suffix">Fachlicher Suffix des Services.</param>
|
||||||
/// <returns>Vollstaendiger Service-Name fuer Checkmk.</returns>
|
/// <returns>Vollstaendiger Service-Name fuer Checkmk.</returns>
|
||||||
public string ServiceName(string suffix)
|
public string ServiceName(string suffix)
|
||||||
{
|
{
|
||||||
var prefix = string.IsNullOrWhiteSpace(EnvironmentName)
|
var prefix = !IncludeEnvironmentInServiceName || string.IsNullOrWhiteSpace(EnvironmentName)
|
||||||
? ServicePrefix
|
? ServicePrefix
|
||||||
: ServicePrefix + " " + EnvironmentName.Trim();
|
: ServicePrefix + " " + EnvironmentName.Trim();
|
||||||
return prefix.Trim() + " " + suffix;
|
return prefix.Trim() + " " + suffix;
|
||||||
@@ -117,6 +126,10 @@ namespace BizTalkCheckmkPulse
|
|||||||
options.Server = ReadString(settings, "Server", options.Server);
|
options.Server = ReadString(settings, "Server", options.Server);
|
||||||
options.ServicePrefix = ReadString(settings, "ServicePrefix", options.ServicePrefix);
|
options.ServicePrefix = ReadString(settings, "ServicePrefix", options.ServicePrefix);
|
||||||
options.EnvironmentName = ReadString(settings, "EnvironmentName", options.EnvironmentName);
|
options.EnvironmentName = ReadString(settings, "EnvironmentName", options.EnvironmentName);
|
||||||
|
options.IncludeEnvironmentInServiceName = ReadBool(
|
||||||
|
settings,
|
||||||
|
"IncludeEnvironmentInServiceName",
|
||||||
|
options.IncludeEnvironmentInServiceName);
|
||||||
options.QueryTimeoutSeconds = ReadInt(settings, "QueryTimeoutSeconds", options.QueryTimeoutSeconds, 5, 120);
|
options.QueryTimeoutSeconds = ReadInt(settings, "QueryTimeoutSeconds", options.QueryTimeoutSeconds, 5, 120);
|
||||||
options.ProbeSqlConnectivity = ReadBool(settings, "ProbeSqlConnectivity", options.ProbeSqlConnectivity);
|
options.ProbeSqlConnectivity = ReadBool(settings, "ProbeSqlConnectivity", options.ProbeSqlConnectivity);
|
||||||
options.SqlConnectionTimeoutSeconds = ReadInt(settings, "SqlConnectionTimeoutSeconds", options.SqlConnectionTimeoutSeconds, 3, 60);
|
options.SqlConnectionTimeoutSeconds = ReadInt(settings, "SqlConnectionTimeoutSeconds", options.SqlConnectionTimeoutSeconds, 3, 60);
|
||||||
@@ -175,6 +188,33 @@ namespace BizTalkCheckmkPulse
|
|||||||
{
|
{
|
||||||
options.Collect = true;
|
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"))
|
else if (EqualsAny(arg, "--consume", "/consume"))
|
||||||
{
|
{
|
||||||
options.Collect = false;
|
options.Collect = false;
|
||||||
@@ -207,6 +247,18 @@ namespace BizTalkCheckmkPulse
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private static void Validate(MonitoringOptions options)
|
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))
|
if (string.IsNullOrWhiteSpace(options.SnapshotPath) || !Path.IsPathRooted(options.SnapshotPath))
|
||||||
{
|
{
|
||||||
throw new ConfigurationErrorsException("SnapshotPath must be an absolute path.");
|
throw new ConfigurationErrorsException("SnapshotPath must be an absolute path.");
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ namespace BizTalkCheckmkPulse
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.RuntimeValidation)
|
||||||
|
{
|
||||||
|
return RunRuntimeValidation(options, formatter);
|
||||||
|
}
|
||||||
|
|
||||||
return options.Collect
|
return options.Collect
|
||||||
? RunProvider(options, formatter)
|
? RunProvider(options, formatter)
|
||||||
: RunConsumer(options, formatter);
|
: RunConsumer(options, formatter);
|
||||||
@@ -51,7 +56,14 @@ namespace BizTalkCheckmkPulse
|
|||||||
Console.WriteLine(line);
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,10 +110,14 @@ namespace BizTalkCheckmkPulse
|
|||||||
+ result.SendPorts.Count
|
+ result.SendPorts.Count
|
||||||
+ " send_ports_not_started="
|
+ " send_ports_not_started="
|
||||||
+ result.SendPorts.Count(x => x.Status != 3)
|
+ result.SendPorts.Count(x => x.Status != 3)
|
||||||
|
+ " endpoint_candidates_active="
|
||||||
|
+ result.EndpointCandidates.Count(x => x.Active)
|
||||||
+ " endpoints_active="
|
+ " endpoints_active="
|
||||||
+ result.EndpointConnectivity.Active
|
+ result.EndpointConnectivity.Active
|
||||||
+ " endpoints_failed="
|
+ " endpoints_failed="
|
||||||
+ result.EndpointConnectivity.Results.Count(x => !x.Available)
|
+ result.EndpointConnectivity.Results.Count(x => !x.Available && !x.Endpoint.BestEffort)
|
||||||
|
+ " endpoints_best_effort_ignored="
|
||||||
|
+ result.EndpointConnectivity.BestEffortIgnoredFailures
|
||||||
+ " endpoints_unresolved="
|
+ " endpoints_unresolved="
|
||||||
+ result.EndpointConnectivity.UnresolvedActive
|
+ result.EndpointConnectivity.UnresolvedActive
|
||||||
+ " endpoints_expected_non_socket="
|
+ " endpoints_expected_non_socket="
|
||||||
@@ -131,6 +147,36 @@ 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
|
||||||
|
+ " acceptedUnknown=" + validation.AcceptedUnknownLines.Count);
|
||||||
|
foreach (var line in validation.AcceptedUnknownLines)
|
||||||
|
{
|
||||||
|
Console.WriteLine(
|
||||||
|
"RUNTIME_VALIDATION_WARNING: Installation technisch valide; Monitoringzustand bleibt sichtbar: "
|
||||||
|
+ line);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
private static void EnsureProviderIdentity()
|
private static void EnsureProviderIdentity()
|
||||||
{
|
{
|
||||||
using (var identity = WindowsIdentity.GetCurrent())
|
using (var identity = WindowsIdentity.GetCurrent())
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ using System.Reflection;
|
|||||||
[assembly: AssemblyDescription("Privileged BizTalk data provider and validated Checkmk snapshot consumer")]
|
[assembly: AssemblyDescription("Privileged BizTalk data provider and validated Checkmk snapshot consumer")]
|
||||||
[assembly: AssemblyCompany("BEW")]
|
[assembly: AssemblyCompany("BEW")]
|
||||||
[assembly: AssemblyProduct("BizTalk Checkmk Pulse")]
|
[assembly: AssemblyProduct("BizTalk Checkmk Pulse")]
|
||||||
[assembly: AssemblyVersion("2.2.1.0")]
|
[assembly: AssemblyVersion("2.2.7.0")]
|
||||||
[assembly: AssemblyFileVersion("2.2.1.0")]
|
[assembly: AssemblyFileVersion("2.2.7.0")]
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
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;
|
||||||
|
string[] unknownServiceLines;
|
||||||
|
var expectedServices = formatter.FormatSelfTest().Select(ExtractServiceName).ToArray();
|
||||||
|
if (!ContainsExactStableContract(
|
||||||
|
snapshot.Lines,
|
||||||
|
expectedServices,
|
||||||
|
out unknownServiceLines,
|
||||||
|
out serviceError))
|
||||||
|
{
|
||||||
|
return RuntimeValidationResult.Failed(serviceError);
|
||||||
|
}
|
||||||
|
|
||||||
|
var acceptedEndpointUnknownName = options.ServiceName("Endpoint Reachability");
|
||||||
|
var blockingUnknown = unknownServiceLines
|
||||||
|
.Where(x => !options.ProbeEndpointConnectivity
|
||||||
|
|| !string.Equals(
|
||||||
|
ExtractServiceName(x),
|
||||||
|
acceptedEndpointUnknownName,
|
||||||
|
StringComparison.Ordinal))
|
||||||
|
.Select(ExtractServiceName)
|
||||||
|
.OrderBy(x => x, StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
if (blockingUnknown.Length != 0)
|
||||||
|
{
|
||||||
|
return RuntimeValidationResult.Failed(
|
||||||
|
"Snapshot enthaelt blockierende UNKNOWN-Zustaende in Kernservices. UNKNOWN=["
|
||||||
|
+ string.Join(", ", blockingUnknown) + "].");
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
unknownServiceLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool ContainsExactStableContract(
|
||||||
|
IEnumerable<string> lines,
|
||||||
|
IEnumerable<string> expectedServices,
|
||||||
|
out string[] unknownServiceLines,
|
||||||
|
out string error)
|
||||||
|
{
|
||||||
|
unknownServiceLines = new string[0];
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
unknownServiceLines = (lines ?? Enumerable.Empty<string>())
|
||||||
|
.Where(x => x != null && x.StartsWith("3 ", StringComparison.Ordinal))
|
||||||
|
.Where(x => expected.Contains(ExtractServiceName(x), StringComparer.Ordinal))
|
||||||
|
.OrderBy(ExtractServiceName, StringComparer.Ordinal)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
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 IReadOnlyList<string> AcceptedUnknownLines { get; private set; }
|
||||||
|
|
||||||
|
public static RuntimeValidationResult Success(
|
||||||
|
DateTime generatedUtc,
|
||||||
|
DateTime? catalogUtc,
|
||||||
|
int serviceCount,
|
||||||
|
IEnumerable<string> acceptedUnknownLines)
|
||||||
|
{
|
||||||
|
return new RuntimeValidationResult
|
||||||
|
{
|
||||||
|
IsSuccess = true,
|
||||||
|
GeneratedUtc = generatedUtc,
|
||||||
|
CatalogSynchronizedUtc = catalogUtc,
|
||||||
|
StableServiceCount = serviceCount,
|
||||||
|
AcceptedUnknownLines = (acceptedUnknownLines ?? Enumerable.Empty<string>()).ToArray()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RuntimeValidationResult Failed(string error)
|
||||||
|
{
|
||||||
|
return new RuntimeValidationResult
|
||||||
|
{
|
||||||
|
Error = error,
|
||||||
|
AcceptedUnknownLines = new string[0]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -114,7 +114,7 @@ namespace BizTalkCheckmkPulse
|
|||||||
}
|
}
|
||||||
|
|
||||||
var expectedMachine = DecodeHeader(headerLines[2], "machineBase64=");
|
var expectedMachine = DecodeHeader(headerLines[2], "machineBase64=");
|
||||||
DecodeHeader(headerLines[3], "identityBase64=");
|
var identity = DecodeHeader(headerLines[3], "identityBase64=");
|
||||||
var expectedHash = ReadHeaderValue(headerLines[5], "payloadSha256=");
|
var expectedHash = ReadHeaderValue(headerLines[5], "payloadSha256=");
|
||||||
if (!string.Equals(expectedMachine, Environment.MachineName, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(expectedMachine, Environment.MachineName, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -159,7 +159,7 @@ namespace BizTalkCheckmkPulse
|
|||||||
+ "s.");
|
+ "s.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return SnapshotReadResult.Success(lines, generatedUtc);
|
return SnapshotReadResult.Success(lines, generatedUtc, identity);
|
||||||
}
|
}
|
||||||
catch (FileNotFoundException)
|
catch (FileNotFoundException)
|
||||||
{
|
{
|
||||||
@@ -330,14 +330,16 @@ namespace BizTalkCheckmkPulse
|
|||||||
public string Error { get; private set; }
|
public string Error { get; private set; }
|
||||||
public IReadOnlyList<string> Lines { get; private set; }
|
public IReadOnlyList<string> Lines { get; private set; }
|
||||||
public DateTime GeneratedUtc { 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
|
return new SnapshotReadResult
|
||||||
{
|
{
|
||||||
IsSuccess = true,
|
IsSuccess = true,
|
||||||
Lines = lines,
|
Lines = lines,
|
||||||
GeneratedUtc = generatedUtc
|
GeneratedUtc = generatedUtc,
|
||||||
|
Identity = identity
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,14 +30,37 @@ namespace BizTalkCheckmkPulse.Tests
|
|||||||
Run("ArtifactSummaryIsBounded", ArtifactSummaryIsBounded);
|
Run("ArtifactSummaryIsBounded", ArtifactSummaryIsBounded);
|
||||||
Run("EndpointAddressesResolveWithoutSecrets", EndpointAddressesResolveWithoutSecrets);
|
Run("EndpointAddressesResolveWithoutSecrets", EndpointAddressesResolveWithoutSecrets);
|
||||||
Run("AdapterSpecificEndpointAddressesResolve", AdapterSpecificEndpointAddressesResolve);
|
Run("AdapterSpecificEndpointAddressesResolve", AdapterSpecificEndpointAddressesResolve);
|
||||||
|
Run("SchedulerDatabaseAndSapEndpointsAreClassifiedSafely", SchedulerDatabaseAndSapEndpointsAreClassifiedSafely);
|
||||||
Run("EndpointClassificationSeparatesExpectedAndUnresolved", EndpointClassificationSeparatesExpectedAndUnresolved);
|
Run("EndpointClassificationSeparatesExpectedAndUnresolved", EndpointClassificationSeparatesExpectedAndUnresolved);
|
||||||
Run("EndpointCatalogPreservesManualOverrides", EndpointCatalogPreservesManualOverrides);
|
Run("EndpointCatalogPreservesManualOverrides", EndpointCatalogPreservesManualOverrides);
|
||||||
Run("EndpointRuntimeUsesCurrentAddressAndManualOverrides", EndpointRuntimeUsesCurrentAddressAndManualOverrides);
|
Run("EndpointRuntimeUsesCurrentAddressAndManualOverrides", EndpointRuntimeUsesCurrentAddressAndManualOverrides);
|
||||||
Run("EndpointCatalogRoundTrip", EndpointCatalogRoundTrip);
|
Run("EndpointCatalogRoundTrip", EndpointCatalogRoundTrip);
|
||||||
Run("EndpointOutputListsOnlyUnavailableTargets", EndpointOutputListsOnlyUnavailableTargets);
|
Run("EndpointOutputListsOnlyUnavailableTargets", EndpointOutputListsOnlyUnavailableTargets);
|
||||||
|
Run("BestEffortEndpointFailuresRemainNonAlerting", BestEffortEndpointFailuresRemainNonAlerting);
|
||||||
Run("EndpointOutputExplainsOnlyRealResolutionGaps", EndpointOutputExplainsOnlyRealResolutionGaps);
|
Run("EndpointOutputExplainsOnlyRealResolutionGaps", EndpointOutputExplainsOnlyRealResolutionGaps);
|
||||||
Run("EndpointProbeBudgetFitsMinuteInterval", EndpointProbeBudgetFitsMinuteInterval);
|
Run("EndpointProbeBudgetFitsMinuteInterval", EndpointProbeBudgetFitsMinuteInterval);
|
||||||
|
Run("ForcedEndpointRefreshRewritesFreshCatalog", ForcedEndpointRefreshRewritesFreshCatalog);
|
||||||
|
Run("EnvironmentLabelDoesNotRenameServicesByDefault", EnvironmentLabelDoesNotRenameServicesByDefault);
|
||||||
|
Run("DefaultServiceContractIsExact", DefaultServiceContractIsExact);
|
||||||
|
Run("InstallerRejectsUnconfirmedServiceRename", InstallerRejectsUnconfirmedServiceRename);
|
||||||
|
Run("InstallerAllowsConfirmedServiceRename", InstallerAllowsConfirmedServiceRename);
|
||||||
|
Run("InstallerAcceptsLegacyEightServiceSelfTest", InstallerAcceptsLegacyEightServiceSelfTest);
|
||||||
|
Run("InstallerRequiresNineServicesForCandidate", InstallerRequiresNineServicesForCandidate);
|
||||||
|
Run("InstallerAllowsAdditiveServiceContract", InstallerAllowsAdditiveServiceContract);
|
||||||
Run("InstallerUpdatePreservesExistingSettings", InstallerUpdatePreservesExistingSettings);
|
Run("InstallerUpdatePreservesExistingSettings", InstallerUpdatePreservesExistingSettings);
|
||||||
|
Run("RuntimeValidationAcceptsFreshConsistentArtifacts", RuntimeValidationAcceptsFreshConsistentArtifacts);
|
||||||
|
Run("RuntimeValidationRejectsPreInstallSnapshot", RuntimeValidationRejectsPreInstallSnapshot);
|
||||||
|
Run("RuntimeValidationRejectsWrongIdentity", RuntimeValidationRejectsWrongIdentity);
|
||||||
|
Run("RuntimeValidationRejectsStaleCatalog", RuntimeValidationRejectsStaleCatalog);
|
||||||
|
Run("RuntimeValidationRejectsChangedServiceContract", RuntimeValidationRejectsChangedServiceContract);
|
||||||
|
Run("RuntimeValidationRejectsUnknownStableService", RuntimeValidationRejectsUnknownStableService);
|
||||||
|
Run("RuntimeValidationAllowsEndpointUnknownAsWarning", RuntimeValidationAllowsEndpointUnknownAsWarning);
|
||||||
|
Run("RuntimeValidationRejectsEndpointUnknownWhenProbeDisabled", RuntimeValidationRejectsEndpointUnknownWhenProbeDisabled);
|
||||||
|
Run("RuntimeValidationAllowsDisabledEndpointProbeWithoutCatalog", RuntimeValidationAllowsDisabledEndpointProbeWithoutCatalog);
|
||||||
|
Run("ForceEndpointRefreshRequiresProviderMode", ForceEndpointRefreshRequiresProviderMode);
|
||||||
|
Run("RuntimeValidationArgumentsAreParsedStrictly", RuntimeValidationArgumentsAreParsedStrictly);
|
||||||
|
Run("TaskCompletionRequiresFreshSuccessfulRun", TaskCompletionRequiresFreshSuccessfulRun);
|
||||||
|
Run("TaskFailureIsDetectedImmediately", TaskFailureIsDetectedImmediately);
|
||||||
Run("SnapshotRoundTripPreservesLines", SnapshotRoundTripPreservesLines);
|
Run("SnapshotRoundTripPreservesLines", SnapshotRoundTripPreservesLines);
|
||||||
Run("SnapshotRejectsTampering", SnapshotRejectsTampering);
|
Run("SnapshotRejectsTampering", SnapshotRejectsTampering);
|
||||||
Run("SnapshotRejectsStaleData", SnapshotRejectsStaleData);
|
Run("SnapshotRejectsStaleData", SnapshotRejectsStaleData);
|
||||||
@@ -330,6 +353,7 @@ namespace BizTalkCheckmkPulse.Tests
|
|||||||
Assert(result.IsSuccess, "snapshot should be readable: " + result.Error);
|
Assert(result.IsSuccess, "snapshot should be readable: " + result.Error);
|
||||||
AssertEqual(lines.Length, result.Lines.Count, "snapshot line count");
|
AssertEqual(lines.Length, result.Lines.Count, "snapshot line count");
|
||||||
Assert(lines.SequenceEqual(result.Lines), "snapshot payload changed");
|
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();
|
var replacement = lines.Select(x => x.Replace("Self test OK", "Replacement OK")).ToArray();
|
||||||
store.Write(replacement, generated.AddSeconds(30), "DOMAIN\\collector$");
|
store.Write(replacement, generated.AddSeconds(30), "DOMAIN\\collector$");
|
||||||
@@ -448,6 +472,73 @@ namespace BizTalkCheckmkPulse.Tests
|
|||||||
AssertEqual(8080, endpoint.Port, "wildcard listener port");
|
AssertEqual(8080, endpoint.Port, "wildcard listener port");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void SchedulerDatabaseAndSapEndpointsAreClassifiedSafely()
|
||||||
|
{
|
||||||
|
var candidate = new EndpointCandidate
|
||||||
|
{
|
||||||
|
ArtifactType = "ReceiveLocation",
|
||||||
|
ApplicationName = "Integration",
|
||||||
|
ArtifactName = "RL Scheduler",
|
||||||
|
TransportRole = "Inbound",
|
||||||
|
AdapterName = "Schedule",
|
||||||
|
Address = "schedule://weekday/06:00",
|
||||||
|
Active = true
|
||||||
|
};
|
||||||
|
|
||||||
|
AssertEqual(
|
||||||
|
EndpointResolutionStatus.ExpectedNonProbeable,
|
||||||
|
EndpointAddressParser.Analyze(candidate).Status,
|
||||||
|
"scheduler must be excluded");
|
||||||
|
|
||||||
|
candidate.ArtifactType = "SendPort";
|
||||||
|
candidate.ArtifactName = "SP Oracle";
|
||||||
|
candidate.TransportRole = "Primary";
|
||||||
|
candidate.AdapterName = "DATABASE";
|
||||||
|
candidate.Address = "Database://oracle01/SAPUSER/Oracle";
|
||||||
|
var resolution = EndpointAddressParser.Analyze(candidate);
|
||||||
|
AssertEqual(EndpointResolutionStatus.Probeable, resolution.Status, "Oracle DATABASE classification");
|
||||||
|
AssertEqual("oracle01", resolution.Entry.Host, "Oracle DATABASE host");
|
||||||
|
AssertEqual(1521, resolution.Entry.Port, "Oracle DATABASE default port");
|
||||||
|
Assert(resolution.Entry.BestEffort, "Oracle DATABASE probe must be best effort");
|
||||||
|
|
||||||
|
candidate.Address = "Database://oracle01:1526/SAPUSER/Oracle";
|
||||||
|
resolution = EndpointAddressParser.Analyze(candidate);
|
||||||
|
AssertEqual(1526, resolution.Entry.Port, "Oracle DATABASE explicit port");
|
||||||
|
|
||||||
|
candidate.Address = "Database://sql01/Lookup/SQLServer";
|
||||||
|
resolution = EndpointAddressParser.Analyze(candidate);
|
||||||
|
AssertEqual(
|
||||||
|
EndpointResolutionStatus.ExpectedNonProbeable,
|
||||||
|
resolution.Status,
|
||||||
|
"non-Oracle DATABASE endpoint must not cause UNKNOWN");
|
||||||
|
|
||||||
|
candidate.ArtifactType = "ReceiveLocation";
|
||||||
|
candidate.ArtifactName = "RL SAP";
|
||||||
|
candidate.TransportRole = "Inbound";
|
||||||
|
// WMI kann einen WCF-SAP-Port lediglich als WCF-Custom ausweisen;
|
||||||
|
// das sap://-Schema muss deshalb fuer die Klassifizierung genuegen.
|
||||||
|
candidate.AdapterName = "WCF-Custom";
|
||||||
|
candidate.Address = "sap://Client=800;lang=DE@A/sap-app/00?ListenerGwHost=sap-gateway.example&ListenerGwServ=SAPGW00&ListenerProgramId=SECRET";
|
||||||
|
resolution = EndpointAddressParser.Analyze(candidate);
|
||||||
|
AssertEqual(EndpointResolutionStatus.Probeable, resolution.Status, "WCF-SAP listener classification");
|
||||||
|
AssertEqual("sap-gateway.example", resolution.Entry.Host, "WCF-SAP gateway host");
|
||||||
|
AssertEqual(3300, resolution.Entry.Port, "WCF-SAP sapgw00 port");
|
||||||
|
Assert(resolution.Entry.BestEffort, "WCF-SAP probe must be best effort");
|
||||||
|
Assert(resolution.Entry.Host.IndexOf("SECRET", StringComparison.OrdinalIgnoreCase) < 0, "SAP Program ID must not enter catalog");
|
||||||
|
|
||||||
|
candidate.Address = "sap://Client=800;lang=DE@A/sap-app/00?SAPROUTER=%2fH%2fsap-router.example%2fS%2f3400%2fH%2fsap-gateway.example&ListenerGwHost=sap-gateway.example&ListenerGwServ=sapgw00";
|
||||||
|
resolution = EndpointAddressParser.Analyze(candidate);
|
||||||
|
AssertEqual("sap-router.example", resolution.Entry.Host, "SAProuter first hop host");
|
||||||
|
AssertEqual(3400, resolution.Entry.Port, "SAProuter explicit port");
|
||||||
|
|
||||||
|
candidate.Address = "sap://Client=800;lang=DE@D/DESTINATION";
|
||||||
|
resolution = EndpointAddressParser.Analyze(candidate);
|
||||||
|
AssertEqual(
|
||||||
|
EndpointResolutionStatus.ExpectedNonProbeable,
|
||||||
|
resolution.Status,
|
||||||
|
"destination-based WCF-SAP endpoint must not cause UNKNOWN");
|
||||||
|
}
|
||||||
|
|
||||||
private static void EndpointClassificationSeparatesExpectedAndUnresolved()
|
private static void EndpointClassificationSeparatesExpectedAndUnresolved()
|
||||||
{
|
{
|
||||||
var candidate = new EndpointCandidate
|
var candidate = new EndpointCandidate
|
||||||
@@ -595,6 +686,33 @@ namespace BizTalkCheckmkPulse.Tests
|
|||||||
Assert(line.IndexOf("up.example.test", StringComparison.Ordinal) < 0, "reachable host must not clutter output");
|
Assert(line.IndexOf("up.example.test", StringComparison.Ordinal) < 0, "reachable host must not clutter output");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void BestEffortEndpointFailuresRemainNonAlerting()
|
||||||
|
{
|
||||||
|
var result = new ProbeResult();
|
||||||
|
result.EndpointConnectivity.RuntimeStateAvailable = true;
|
||||||
|
result.EndpointConnectivity.CatalogAvailable = true;
|
||||||
|
result.EndpointConnectivity.RefreshSucceeded = true;
|
||||||
|
result.EndpointConnectivity.Configured = 1;
|
||||||
|
result.EndpointConnectivity.Active = 1;
|
||||||
|
result.EndpointConnectivity.BestEffortActive = 1;
|
||||||
|
var endpoint = TestEndpoint("SAP Listener", "sap-gateway.example");
|
||||||
|
endpoint.AdapterName = "WCF-SAP";
|
||||||
|
endpoint.Port = 3300;
|
||||||
|
endpoint.BestEffort = true;
|
||||||
|
result.EndpointConnectivity.Results.Add(new EndpointProbeResult
|
||||||
|
{
|
||||||
|
Endpoint = endpoint,
|
||||||
|
Available = false,
|
||||||
|
Failure = "TCP timeout"
|
||||||
|
});
|
||||||
|
|
||||||
|
var line = FindServiceLine(result, "Endpoint Reachability");
|
||||||
|
Assert(line.StartsWith("0 \"BizTalk Endpoint Reachability\"", StringComparison.Ordinal), "best-effort failure must remain OK");
|
||||||
|
Assert(line.Contains("biztalk_endpoints_failed=0"), "best-effort failure must not increment failed metric");
|
||||||
|
Assert(line.Contains("biztalk_endpoints_best_effort_ignored=1"), "ignored best-effort metric missing");
|
||||||
|
Assert(line.IndexOf("unavailable=", StringComparison.OrdinalIgnoreCase) < 0, "best-effort target must not appear as unavailable");
|
||||||
|
}
|
||||||
|
|
||||||
private static void EndpointOutputExplainsOnlyRealResolutionGaps()
|
private static void EndpointOutputExplainsOnlyRealResolutionGaps()
|
||||||
{
|
{
|
||||||
var result = new ProbeResult();
|
var result = new ProbeResult();
|
||||||
@@ -630,7 +748,9 @@ namespace BizTalkCheckmkPulse.Tests
|
|||||||
ActiveCandidates = 150,
|
ActiveCandidates = 150,
|
||||||
UnresolvedCandidates = 7
|
UnresolvedCandidates = 7
|
||||||
};
|
};
|
||||||
catalog.Entries.Add(TestEndpoint("Orders", "api.example.test"));
|
var storedEndpoint = TestEndpoint("Orders", "api.example.test");
|
||||||
|
storedEndpoint.BestEffort = true;
|
||||||
|
catalog.Entries.Add(storedEndpoint);
|
||||||
var store = new EndpointCatalogStore(path, 1048576, 100);
|
var store = new EndpointCatalogStore(path, 1048576, 100);
|
||||||
store.Write(catalog);
|
store.Write(catalog);
|
||||||
var loaded = store.Read("ACC");
|
var loaded = store.Read("ACC");
|
||||||
@@ -638,8 +758,18 @@ namespace BizTalkCheckmkPulse.Tests
|
|||||||
AssertEqual(1, loaded.Entries.Count, "catalog entry count");
|
AssertEqual(1, loaded.Entries.Count, "catalog entry count");
|
||||||
AssertEqual("api.example.test", loaded.Entries[0].Host, "catalog host");
|
AssertEqual("api.example.test", loaded.Entries[0].Host, "catalog host");
|
||||||
AssertEqual(443, loaded.Entries[0].Port, "catalog port");
|
AssertEqual(443, loaded.Entries[0].Port, "catalog port");
|
||||||
|
Assert(loaded.Entries[0].BestEffort, "catalog best-effort marker");
|
||||||
AssertEqual(7, loaded.UnresolvedCandidates, "catalog unresolved candidate count");
|
AssertEqual(7, loaded.UnresolvedCandidates, "catalog unresolved candidate count");
|
||||||
Assert(File.ReadAllText(path).IndexOf("top-secret", StringComparison.OrdinalIgnoreCase) < 0, "catalog must not contain URI secrets");
|
Assert(File.ReadAllText(path).IndexOf("top-secret", StringComparison.OrdinalIgnoreCase) < 0, "catalog must not contain URI secrets");
|
||||||
|
|
||||||
|
// Vorhandene Kataloge der Formatversion 1 besitzen das neue optionale
|
||||||
|
// Attribut noch nicht und muessen beim Update weiterhin lesbar bleiben.
|
||||||
|
var legacyXml = File.ReadAllText(path)
|
||||||
|
.Replace(" bestEffort=\"true\"", string.Empty)
|
||||||
|
.Replace(" bestEffort=\"True\"", string.Empty);
|
||||||
|
File.WriteAllText(path, legacyXml, new UTF8Encoding(false));
|
||||||
|
loaded = store.Read("ACC");
|
||||||
|
Assert(!loaded.Entries[0].BestEffort, "legacy catalog best-effort default");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -683,6 +813,169 @@ namespace BizTalkCheckmkPulse.Tests
|
|||||||
Assert(configuredMaximum <= 30000, "configured endpoint worst-case must leave headroom in the minute interval");
|
Assert(configuredMaximum <= 30000, "configured endpoint worst-case must leave headroom in the minute interval");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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" };
|
||||||
|
var lines = new CheckmkLocalFormatter(options).FormatSelfTest().ToArray();
|
||||||
|
Assert(lines.Any(x => x.Contains("\"BizTalk Platform\"")), "stable service name without environment missing");
|
||||||
|
Assert(lines.All(x => x.IndexOf("\"BizTalk ACC ", StringComparison.Ordinal) < 0), "environment label must not rename services by default");
|
||||||
|
|
||||||
|
options.IncludeEnvironmentInServiceName = true;
|
||||||
|
lines = new CheckmkLocalFormatter(options).FormatSelfTest().ToArray();
|
||||||
|
Assert(lines.Any(x => x.Contains("\"BizTalk ACC Platform\"")), "explicit environment service-name opt-in missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DefaultServiceContractIsExact()
|
||||||
|
{
|
||||||
|
var actual = new CheckmkLocalFormatter(new MonitoringOptions())
|
||||||
|
.FormatSelfTest()
|
||||||
|
.Select(ExtractQuotedServiceName)
|
||||||
|
.ToArray();
|
||||||
|
var expected = new[]
|
||||||
|
{
|
||||||
|
"BizTalk Platform",
|
||||||
|
"BizTalk SQL Access",
|
||||||
|
"BizTalk Suspended Instances",
|
||||||
|
"BizTalk Host Instances",
|
||||||
|
"BizTalk Receive Locations",
|
||||||
|
"BizTalk Send Ports",
|
||||||
|
"BizTalk Endpoint Reachability",
|
||||||
|
"BizTalk Orchestrations",
|
||||||
|
"BizTalk Event Log"
|
||||||
|
};
|
||||||
|
AssertEqual(string.Join("|", expected), string.Join("|", actual), "exact default service contract");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void InstallerRejectsUnconfirmedServiceRename()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
BizTalkCheckmkPulse.Setup.InstallerEngine.EnsureServiceNameCompatibility(
|
||||||
|
new[] { "BizTalk Platform", "BizTalk Event Log" },
|
||||||
|
new[] { "BizTalk ACC Platform", "BizTalk ACC Event Log" },
|
||||||
|
false);
|
||||||
|
throw new InvalidOperationException("unconfirmed rename was accepted");
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
Assert(ex.Message.Contains("Sicherheitsstopp"), "rename rejection must explain the safety stop");
|
||||||
|
Assert(ex.Message.Contains("Service Discovery"), "rename rejection must require discovery");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void InstallerAllowsConfirmedServiceRename()
|
||||||
|
{
|
||||||
|
var unchanged = BizTalkCheckmkPulse.Setup.InstallerEngine.EnsureServiceNameCompatibility(
|
||||||
|
new[] { "BizTalk Platform", "BizTalk Event Log" },
|
||||||
|
new[] { "BizTalk Event Log", "BizTalk Platform" },
|
||||||
|
false);
|
||||||
|
AssertEqual(string.Empty, unchanged, "service ordering must not be treated as rename");
|
||||||
|
|
||||||
|
var change = BizTalkCheckmkPulse.Setup.InstallerEngine.EnsureServiceNameCompatibility(
|
||||||
|
new[] { "BizTalk ACC Platform" },
|
||||||
|
new[] { "BizTalk Platform" },
|
||||||
|
true);
|
||||||
|
Assert(change.Contains("BizTalk ACC Platform"), "confirmed rename must report removed service");
|
||||||
|
Assert(change.Contains("BizTalk Platform"), "confirmed rename must report new service");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void InstallerAcceptsLegacyEightServiceSelfTest()
|
||||||
|
{
|
||||||
|
var output = string.Join("\r\n", Enumerable.Range(1, 8)
|
||||||
|
.Select(x => "0 \"BizTalk Legacy " + x + "\" - Self test OK"));
|
||||||
|
var services = BizTalkCheckmkPulse.Setup.InstallerEngine.ParseSelfTestOutput(
|
||||||
|
output,
|
||||||
|
string.Empty,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
"Installierte Version");
|
||||||
|
AssertEqual(8, services.Count, "legacy self-test service count");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void InstallerRequiresNineServicesForCandidate()
|
||||||
|
{
|
||||||
|
var output = string.Join("\n", Enumerable.Range(1, 8)
|
||||||
|
.Select(x => "0 \"BizTalk Candidate " + x + "\" - Self test OK"));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
BizTalkCheckmkPulse.Setup.InstallerEngine.ParseSelfTestOutput(
|
||||||
|
output,
|
||||||
|
string.Empty,
|
||||||
|
0,
|
||||||
|
9,
|
||||||
|
"Staging");
|
||||||
|
throw new InvalidOperationException("candidate with eight services was accepted");
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
Assert(ex.Message.Contains("Zeilen=8"), "candidate failure must report actual line count");
|
||||||
|
Assert(ex.Message.Contains("Erwartet=9"), "candidate failure must report expected line count");
|
||||||
|
Assert(ex.Message.Contains("Stdout="), "candidate failure must include stdout");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void InstallerAllowsAdditiveServiceContract()
|
||||||
|
{
|
||||||
|
var change = BizTalkCheckmkPulse.Setup.InstallerEngine.EnsureServiceNameCompatibility(
|
||||||
|
new[] { "BizTalk Platform", "BizTalk Event Log" },
|
||||||
|
new[] { "BizTalk Platform", "BizTalk Endpoint Reachability", "BizTalk Event Log" },
|
||||||
|
false);
|
||||||
|
Assert(change.Contains("Entfernt=[]"), "additive contract must not remove a service");
|
||||||
|
Assert(change.Contains("BizTalk Endpoint Reachability"), "additive contract must report the new service");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ExtractQuotedServiceName(string line)
|
||||||
|
{
|
||||||
|
var start = line.IndexOf('"') + 1;
|
||||||
|
var end = line.IndexOf('"', start);
|
||||||
|
return line.Substring(start, end - start);
|
||||||
|
}
|
||||||
|
|
||||||
private static void InstallerUpdatePreservesExistingSettings()
|
private static void InstallerUpdatePreservesExistingSettings()
|
||||||
{
|
{
|
||||||
var directory = Path.Combine(Path.GetTempPath(), "BizTalkCheckmkPulse.InstallerTests." + Guid.NewGuid().ToString("N"));
|
var directory = Path.Combine(Path.GetTempPath(), "BizTalkCheckmkPulse.InstallerTests." + Guid.NewGuid().ToString("N"));
|
||||||
@@ -696,6 +989,7 @@ namespace BizTalkCheckmkPulse.Tests
|
|||||||
source,
|
source,
|
||||||
"<configuration><appSettings>"
|
"<configuration><appSettings>"
|
||||||
+ "<add key=\"EnvironmentName\" value=\"\" />"
|
+ "<add key=\"EnvironmentName\" value=\"\" />"
|
||||||
|
+ "<add key=\"IncludeEnvironmentInServiceName\" value=\"false\" />"
|
||||||
+ "<add key=\"EndpointProbeMaxConcurrency\" value=\"16\" />"
|
+ "<add key=\"EndpointProbeMaxConcurrency\" value=\"16\" />"
|
||||||
+ "<add key=\"EndpointMaxCount\" value=\"100\" />"
|
+ "<add key=\"EndpointMaxCount\" value=\"100\" />"
|
||||||
+ "<add key=\"EndpointCatalogMaxEntries\" value=\"1000\" />"
|
+ "<add key=\"EndpointCatalogMaxEntries\" value=\"1000\" />"
|
||||||
@@ -719,6 +1013,7 @@ namespace BizTalkCheckmkPulse.Tests
|
|||||||
string.Empty);
|
string.Empty);
|
||||||
var merged = File.ReadAllText(staged);
|
var merged = File.ReadAllText(staged);
|
||||||
AssertEqual("ACC", effectiveEnvironment, "preserved installer environment");
|
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=\"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=\"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");
|
Assert(merged.Contains("key=\"EndpointCatalogMaxEntries\" value=\"1000\""), "new catalog entry limit must be added");
|
||||||
@@ -738,6 +1033,274 @@ 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 core service must fail runtime acceptance");
|
||||||
|
Assert(result.Error.Contains("UNKNOWN=[BizTalk Platform]"), "UNKNOWN core service reason missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RuntimeValidationAllowsEndpointUnknownAsWarning()
|
||||||
|
{
|
||||||
|
var boundary = DateTime.UtcNow.AddMinutes(-1);
|
||||||
|
var lines = new CheckmkLocalFormatter(new MonitoringOptions()).FormatSelfTest().ToArray();
|
||||||
|
var endpointIndex = Array.FindIndex(
|
||||||
|
lines,
|
||||||
|
x => x.Contains("\"BizTalk Endpoint Reachability\""));
|
||||||
|
Assert(endpointIndex >= 0, "endpoint service missing from self-test contract");
|
||||||
|
lines[endpointIndex] = "3 \"BizTalk Endpoint Reachability\" "
|
||||||
|
+ "biztalk_endpoints_unresolved=1;;1;0 Endpoint reachability incomplete; unresolved=1.";
|
||||||
|
|
||||||
|
var result = ValidateRuntimeArtifacts(
|
||||||
|
boundary,
|
||||||
|
boundary.AddSeconds(10),
|
||||||
|
boundary.AddSeconds(5),
|
||||||
|
"BEW\\t231bizmon",
|
||||||
|
"BEW\\t231bizmon",
|
||||||
|
true,
|
||||||
|
lines);
|
||||||
|
|
||||||
|
Assert(result.IsSuccess, "endpoint UNKNOWN must be accepted as operational warning: " + result.Error);
|
||||||
|
AssertEqual(1, result.AcceptedUnknownLines.Count, "accepted endpoint UNKNOWN count");
|
||||||
|
Assert(result.AcceptedUnknownLines[0].Contains("unresolved=1"), "endpoint UNKNOWN detail must be retained");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RuntimeValidationRejectsEndpointUnknownWhenProbeDisabled()
|
||||||
|
{
|
||||||
|
var boundary = DateTime.UtcNow.AddMinutes(-1);
|
||||||
|
var lines = new CheckmkLocalFormatter(new MonitoringOptions()).FormatSelfTest().ToArray();
|
||||||
|
var endpointIndex = Array.FindIndex(
|
||||||
|
lines,
|
||||||
|
x => x.Contains("\"BizTalk Endpoint Reachability\""));
|
||||||
|
lines[endpointIndex] = "3 \"BizTalk Endpoint Reachability\" - Unexpected UNKNOWN while probe is disabled.";
|
||||||
|
|
||||||
|
var result = ValidateRuntimeArtifacts(
|
||||||
|
boundary,
|
||||||
|
boundary.AddSeconds(10),
|
||||||
|
null,
|
||||||
|
"BEW\\t231bizmon",
|
||||||
|
"BEW\\t231bizmon",
|
||||||
|
false,
|
||||||
|
lines);
|
||||||
|
|
||||||
|
Assert(!result.IsSuccess, "endpoint UNKNOWN must be rejected when endpoint probing is disabled");
|
||||||
|
Assert(result.Error.Contains("UNKNOWN=[BizTalk Endpoint Reachability]"), "disabled-probe UNKNOWN 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 void TaskFailureIsDetectedImmediately()
|
||||||
|
{
|
||||||
|
var boundary = DateTime.UtcNow;
|
||||||
|
Assert(BizTalkCheckmkPulse.Setup.TaskSchedulerService.IsFreshCompletedFailure(
|
||||||
|
3, 8, boundary.AddSeconds(1), boundary), "fresh failed task must be detected");
|
||||||
|
Assert(!BizTalkCheckmkPulse.Setup.TaskSchedulerService.IsFreshCompletedFailure(
|
||||||
|
4, 8, boundary.AddSeconds(1), boundary), "running task must not be treated as completed failure");
|
||||||
|
Assert(!BizTalkCheckmkPulse.Setup.TaskSchedulerService.IsFreshCompletedFailure(
|
||||||
|
3, 8, boundary.AddSeconds(-3), boundary), "old task failure must not fail a new validation run");
|
||||||
|
var description = BizTalkCheckmkPulse.Setup.TaskSchedulerService.DescribeResult(8);
|
||||||
|
Assert(description.Contains("0x00000008"), "task result must include hexadecimal code");
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
private static void SnapshotRejectsTampering()
|
||||||
{
|
{
|
||||||
WithTemporarySnapshot((path, store) =>
|
WithTemporarySnapshot((path, store) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user