Compare commits

..
11 Commits
37 changed files with 4455 additions and 609 deletions
+1 -2
View File
@@ -8,6 +8,5 @@ indent_style = space
indent_size = 4 indent_size = 4
[*.md] [*.md]
end_of_line = lf end_of_line = crlf
trim_trailing_whitespace = false trim_trailing_whitespace = false
+1
View File
@@ -0,0 +1 @@
* text=auto eol=crlf
+206 -138
View File
@@ -2,24 +2,24 @@
## 1. Ziel und Randbedingungen ## 1. Ziel und Randbedingungen
Die Anwendung ueberwacht BizTalk Server 2020 auf Windows Server 2019 mit Die Anwendung überwacht BizTalk Server 2020 auf Windows Server 2019 mit
Checkmk 2.4. Pro Umgebung (`ACC`, `DEV`, `TST`, `PRD`) wird lokal auf dem Checkmk 2.4. Pro Umgebung (`ACC`, `DEV`, `TST`, `PRD`) wird lokal auf dem
BizTalk-Server gesammelt. Das Checkmk-MSSQL-Plugin bleibt fuer die allgemeine BizTalk-Server gesammelt. Das Checkmk-MSSQL-Plugin bleibt für die allgemeine
SQL-Ueberwachung zustaendig; BizTalk Checkmk Pulse liefert die SQL-Überwachung zuständig; BizTalk Checkmk Pulse liefert die
BizTalk-spezifische Laufzeit- und Berechtigungssicht. BizTalk-spezifische Laufzeit- und Berechtigungssicht.
Verbindliche Randbedingungen: Verbindliche Randbedingungen:
- Der Checkmk Windows Agent bleibt `LocalSystem`. - Der Checkmk Windows Agent bleibt `LocalSystem`.
- `LocalSystem` erhaelt keine BizTalk-/SQL-Gruppenmitgliedschaft. - `LocalSystem` erhält keine BizTalk-/SQL-Gruppenmitgliedschaft.
- Ein normales dediziertes AD-Servicekonto sammelt minuetlich. - Ein normales dediziertes AD-Servicekonto sammelt minütlich.
- Der Agentpfad fuehrt niemals WMI- oder SQL-Abfragen aus. - Der Agentpfad führt niemals WMI- oder SQL-Abfragen aus.
- Fehler muessen als gueltige Checkmk-`UNKNOWN`-Services sichtbar werden. - Fehler müssen als gültige Checkmk-`UNKNOWN`-Services sichtbar werden.
- Schreiben und Lesen duerfen nie einen halben Snapshot exponieren. - Schreiben und Lesen dürfen nie einen halben Snapshot exponieren.
- Installation und Laufzeit benoetigen keine PowerShell und keine BizTalk-DLL-Referenz. - Installation und Laufzeit benötigen keine PowerShell und keine BizTalk-DLL-Referenz.
- Installation, ACL und Scheduled Task muessen reproduzierbar sein. - Installation, ACL und Scheduled Task müssen reproduzierbar sein.
## 2. Ausloeser der Architekturanderung ## 2. Auslöser der Architekturänderung
Der ACC-Agent-Dump vom 29.07.2026 bewies: Der ACC-Agent-Dump vom 29.07.2026 bewies:
@@ -31,13 +31,13 @@ Innerer Fehler: Login failed for user 'BEW\AV23AGPWBIO1$'
``` ```
Lokales `root\MicrosoftBizTalkServer` war erreichbar. Die Lokales `root\MicrosoftBizTalkServer` war erreichbar. Die
datenbankgestuetzten BizTalk-WMI-Klassen scheiterten erst beim Zugriff des datenbankgestützten BizTalk-WMI-Klassen scheiterten erst beim Zugriff des
Providers auf den getrennten SQL Server. DCOM-, Firewall- oder pauschale Providers auf den getrennten SQL Server. DCOM-, Firewall- oder pauschale
WMI-ACL-Aenderungen beheben diesen Befund nicht. WMI-ACL-Änderungen beheben diesen Befund nicht.
Eine Berechtigung des Maschinenkontos haette die Netzwerkrolle allen Eine Berechtigung des Maschinenkontos hätte die Netzwerkrolle allen
`LocalSystem`-Diensten des BizTalk-Servers vermittelt. Die jetzt implementierte `LocalSystem`-Diensten des BizTalk-Servers vermittelt. Die jetzt implementierte
Producer/Consumer-Trennung beschraenkt sie stattdessen auf ein dediziertes Producer/Consumer-Trennung beschränkt sie stattdessen auf ein dediziertes
Konto. Konto.
## 3. Architektur ## 3. Architektur
@@ -67,12 +67,12 @@ Konto.
│ └─ local\biztalk_checkmk_pulse.cmd │ │ └─ local\biztalk_checkmk_pulse.cmd │
│ └─ BizTalkCheckmkPulse.exe --consume │ │ └─ BizTalkCheckmkPulse.exe --consume │
│ ├─ Format/Alter/Maschine/SHA-256 validieren │ │ ├─ Format/Alter/Maschine/SHA-256 validieren │
│ └─ Payload unveraendert nach STDOUT │ │ └─ Payload unverändert nach STDOUT
└──────────────────────────────────────────────────────────────┘ └──────────────────────────────────────────────────────────────┘
``` ```
Eine gemeinsame EXE implementiert beide Modi. Das reduziert Eine gemeinsame EXE implementiert beide Modi. Das reduziert
Versionsabweichungen. Die zentrale Config unter `%ProgramFiles%` gilt fuer Versionsabweichungen. Die zentrale Config unter `%ProgramFiles%` gilt für
Provider und Consumer. Provider und Consumer.
### 3.1 Provider-Modus ### 3.1 Provider-Modus
@@ -86,13 +86,13 @@ Aufruf:
Ablauf: Ablauf:
1. Config laden und absolute Runtimepfade validieren. 1. Config laden und absolute Runtimepfade validieren.
2. Tageslogs gemaess `LogRetentionDays` bereinigen. 2. Tageslogs gemäß `LogRetentionDays` bereinigen.
3. exklusives Handle auf `<SnapshotPath>.provider.lock` halten. 3. exklusives Handle auf `<SnapshotPath>.provider.lock` halten.
4. WMI-, SQL- und Event-Log-Probes ausfuehren. 4. WMI-, SQL- und Event-Log-Probes ausführen.
5. neun stabile und optionale dynamische Checkmk-Zeilen formatieren. 5. neun stabile und optionale dynamische Checkmk-Zeilen formatieren.
6. Snapshot in einer eindeutigen Temporaerdatei desselben Verzeichnisses 6. Snapshot in einer eindeutigen Temporärdatei desselben Verzeichnisses
schreiben. schreiben.
7. `Flush(true)` ausfuehren und Temporaerdatei atomar publizieren. 7. `Flush(true)` ausführen und Temporärdatei atomar publizieren.
8. Laufzeit, Zeilen- und Diagnosezahl loggen. 8. Laufzeit, Zeilen- und Diagnosezahl loggen.
Exitcodes: Exitcodes:
@@ -104,7 +104,7 @@ Exitcodes:
| `2` | Parallelstart oder Snapshot-I/O-Fehler. | | `2` | Parallelstart oder Snapshot-I/O-Fehler. |
Fachliche WMI-/SQL-Fehler werden strukturiert in den Snapshot geschrieben und Fachliche WMI-/SQL-Fehler werden strukturiert in den Snapshot geschrieben und
muessen nicht zwingend einen Prozessfehler verursachen. So bleibt die genaue müssen nicht zwingend einen Prozessfehler verursachen. So bleibt die genaue
Fehlerkategorie in Checkmk sichtbar. Fehlerkategorie in Checkmk sichtbar.
### 3.2 Consumer-Modus ### 3.2 Consumer-Modus
@@ -120,19 +120,19 @@ setzt es trotzdem explizit.
Der Consumer: Der Consumer:
- oeffnet nur den Snapshot, - öffnet nur den Snapshot,
- begrenzt die Dateigroesse vor dem Einlesen, - begrenzt die Dateigröße vor dem Einlesen,
- validiert striktes UTF-8, - validiert striktes UTF-8,
- validiert Formatversion und Header, - validiert Formatversion und Header,
- fordert die lokale Maschinenkennung, - fordert die lokale Maschinenkennung,
- berechnet SHA-256 ueber den exakten Payload, - berechnet SHA-256 über den exakten Payload,
- prueft Zeilenanzahl und Checkmk-Zeilenform, - prüft Zeilenanzahl und Checkmk-Zeilenform,
- verwirft Zeitstempel mehr als fuenf Minuten in der Zukunft, - verwirft Zeitstempel mehr als fünf Minuten in der Zukunft,
- verwirft Snapshots nach `SnapshotMaxAgeSeconds`, - verwirft Snapshots nach `SnapshotMaxAgeSeconds`,
- gibt bei Erfolg ausschliesslich den validierten Payload aus. - gibt bei Erfolg ausschließlich den validierten Payload aus.
Er gibt immer Exitcode `0` zurueck, damit ein Fehler die komplette Er gibt immer Exitcode `0` zurück, damit ein Fehler die komplette
Checkmk-Agentsektion nicht zerstoert. Jede Ablehnung erzeugt neun Checkmk-Agentsektion nicht zerstört. Jede Ablehnung erzeugt neun
`UNKNOWN`-Zeilen und einen Eintrag im Consumer-Log. `UNKNOWN`-Zeilen und einen Eintrag im Consumer-Log.
## 4. Snapshot-Vertrag ## 4. Snapshot-Vertrag
@@ -144,33 +144,33 @@ BIZTALK_CHECKMK_PULSE_SNAPSHOT_V2
generatedUtc=2026-07-30T12:34:56.1234567Z generatedUtc=2026-07-30T12:34:56.1234567Z
machineBase64=QVYyM0FHUFdCSU8x machineBase64=QVYyM0FHUFdCSU8x
identityBase64=QkVXXHN2Y19iaXp0YWxrX2NtayQ= identityBase64=QkVXXHN2Y19iaXp0YWxrX2NtayQ=
payloadLines=8 payloadLines=9
payloadSha256=<64 hex characters> payloadSha256=<64 hex characters>
0 "BizTalk Platform" - ... 0 "BizTalk Platform" - ...
... ...
``` ```
Maschine und Identitaet sind Base64-kodiert, um Zeilenumbrueche und Maschine und Identität sind Base64-kodiert, um Zeilenumbrüche und
Trennzeichen im Header auszuschliessen. Base64 ist hier keine Verschluesselung. Trennzeichen im Header auszuschließen. Base64 ist hier keine Verschlüsselung.
Die SHA-256-Pruefung schuetzt vor unbemerkter Teil-/Fehlschreibung, nicht vor Die SHA-256-Prüfung schützt vor unbemerkter Teil-/Fehlschreibung, nicht vor
einem lokalen Administrator. einem lokalen Administrator.
### 4.1 Atomare Publikation ### 4.1 Atomare Publikation
Die Temporaerdatei liegt zwingend im Snapshotverzeichnis. Dadurch findet der Die Temporärdatei liegt zwingend im Snapshotverzeichnis. Dadurch findet der
Ersatz auf demselben Volume statt. Existiert bereits ein Snapshot, wird Ersatz auf demselben Volume statt. Existiert bereits ein Snapshot, wird
`File.Replace` verwendet; beim ersten Lauf `File.Move`. Bis zum erfolgreichen `File.Replace` verwendet; beim ersten Lauf `File.Move`. Bis zum erfolgreichen
Ersatz bleibt der letzte vollstaendige Snapshot erhalten. Seine Alterspruefung Ersatz bleibt der letzte vollständige Snapshot erhalten. Seine Altersprüfung
verhindert unbegrenztes Weiterreichen alter Daten. verhindert unbegrenztes Weiterreichen alter Daten.
### 4.2 Keine direkte Checkmk-Spooldatei ### 4.2 Keine direkte Checkmk-Spooldatei
Checkmk kann zwar Dateien aus seinem Spoolverzeichnis direkt anhaengen, prueft Checkmk kann zwar Dateien aus seinem Spoolverzeichnis direkt anhängen, prüft
deren Inhalt aber laut eigener Dokumentation nicht. Ein fehlerhafter Spool kann deren Inhalt aber laut eigener Dokumentation nicht. Ein fehlerhafter Spool kann
nachfolgende Agentsektionen beeinflussen. Deshalb liegt der Provider-Snapshot nachfolgende Agentsektionen beeinflussen. Deshalb liegt der Provider-Snapshot
in einem eigenen Verzeichnis und wird erst durch den validierenden Consumer in in einem eigenen Verzeichnis und wird erst durch den validierenden Consumer in
die Local-Check-Ausgabe uebernommen. die Local-Check-Ausgabe übernommen.
## 5. Datenquellen ## 5. Datenquellen
@@ -187,11 +187,11 @@ Pflichtklassen:
| Klasse | Zweck | | Klasse | Zweck |
| --- | --- | | --- | --- |
| `MSBTS_GroupSetting` | Gruppe, Read-Only-/Operator-Gruppe, Management- und Master-MessageBox-Ziel. | | `MSBTS_GroupSetting` | Gruppe, Read-Only-/Operator-Gruppe, Management- und Master-MessageBox-Ziel. |
| `MSBTS_HostInstance` | lokale Host-Instance-Zustaende. | | `MSBTS_HostInstance` | lokale Host-Instance-Zustände. |
| `MSBTS_ServiceInstance` | suspendierte Instanzen. | | `MSBTS_ServiceInstance` | suspendierte Instanzen. |
| `MSBTS_ReceiveLocation` | Receive-Location-Zustaende. | | `MSBTS_ReceiveLocation` | Receive-Location-Zustände. |
| `MSBTS_SendPort` | Send-Port-Zustaende. | | `MSBTS_SendPort` | Send-Port-Zustände. |
| `MSBTS_Orchestration` | Orchestration-Zustaende. | | `MSBTS_Orchestration` | Orchestration-Zustände. |
`MSBTS_GroupSetting` wird mit den dokumentierten Properties `MSBTS_GroupSetting` wird mit den dokumentierten Properties
`MgmtDbServerName`, `MgmtDbName`, `SubscriptionDBServerName`, `MgmtDbServerName`, `MgmtDbName`, `SubscriptionDBServerName`,
@@ -202,10 +202,10 @@ Pflichtklassen:
Hostnamen werden clientseitig verglichen. Sonderzeichen und FQDN-/Kurzname Hostnamen werden clientseitig verglichen. Sonderzeichen und FQDN-/Kurzname
gelangen nicht in dynamisch erzeugte WQL-Filter. gelangen nicht in dynamisch erzeugte WQL-Filter.
`MSBTS_ServiceInstance` wird fuer `ServiceStatus=4` (resumable), `MSBTS_ServiceInstance` wird für `ServiceStatus=4` (resumable),
`ServiceStatus=32` (non-resumable) und `ServiceClass=64` abgefragt. `ServiceStatus=32` (non-resumable) und `ServiceClass=64` abgefragt.
`ServiceClass=64` kennzeichnet Routing Failure Reports. Sie werden als eigene `ServiceClass=64` kennzeichnet Routing Failure Reports. Sie werden als eigene
Metrik ausgewiesen und zugleich in `suspended_nonresumable` mitgezaehlt. Metrik ausgewiesen und zugleich in `suspended_nonresumable` mitgezählt.
### 5.2 Kompaktes Checkmk-Servicebild ### 5.2 Kompaktes Checkmk-Servicebild
@@ -222,62 +222,70 @@ Services ersetzt. Insgesamt entstehen neun stabile Services:
8. `BizTalk Orchestrations` 8. `BizTalk Orchestrations`
9. `BizTalk Event Log` 9. `BizTalk Event Log`
Receive Locations und Send Ports alarmieren nur fuer unerwartete Receive Locations und Send Ports alarmieren nur für unerwartete
Aus-Zustaende. Fachlich bewusst deaktivierte Namen werden exakt, ohne Aus-Zustände. Fachlich bewusst deaktivierte Namen werden exakt, ohne
Wildcards, in `ExpectedDisabledReceiveLocations` beziehungsweise Wildcards, in `ExpectedDisabledReceiveLocations` beziehungsweise
`ExpectedInactiveSendPorts` hinterlegt. Ein Eintrag ist entweder nur der Name `ExpectedInactiveSendPorts` hinterlegt. Ein Eintrag ist entweder nur der Name
oder `Anwendung\Name`. oder `Anwendung\Name`.
Pro Summary werden maximal `MaxSummaryItems` Treffer angezeigt. Weitere Pro Summary werden maximal `MaxSummaryItems` Treffer angezeigt. Weitere
Artefakte werden als `(+n more)` zusammengefasst. `MaxDetailCharacters` Artefakte werden als `(+n more)` zusammengefasst. `MaxDetailCharacters`
begrenzt jede Detailausgabe, damit Checkmk-Ansichten kompakt bleiben, waehrend begrenzt jede Detailausgabe, damit Checkmk-Ansichten kompakt bleiben, während
alle numerischen Metriken vollstaendig erhalten bleiben. alle numerischen Metriken vollständig erhalten bleiben.
### 5.3 Endpoint-Discovery und Netzwerkprobe ### 5.3 Endpoint-Discovery und Netzwerkprobe
Die Endpoint-Funktion erweitert keine WMI-Klassenmenge. Aus den ohnehin Die Endpoint-Funktion erweitert keine WMI-Klassenmenge. Aus den ohnehin
minuetlich abgefragten Instanzen werden nur dokumentierte Properties gelesen: minütlich abgefragten Instanzen werden nur dokumentierte Properties gelesen:
| WMI-Klasse | Properties | | WMI-Klasse | Properties |
| --- | --- | | --- | --- |
| `MSBTS_SendPort` | `Status`, `IsDynamic`, `PTAddress`, `STAddress`, `PTTransportType`, `STTransportType` | | `MSBTS_SendPort` | `Status`, `IsDynamic`, `PTAddress`, `STAddress`, `PTTransportType`, `STTransportType` |
| `MSBTS_ReceiveLocation` | `IsDisabled`, `InboundTransportURL`, `AdapterName` | | `MSBTS_ReceiveLocation` | `IsDisabled`, `InboundTransportURL`, `AdapterName` |
Nur `Status=Started` beziehungsweise `IsDisabled=false` wird geprueft. Nur `Status=Started` beziehungsweise `IsDisabled=false` wird geprüft.
Inaktive Artefakte bleiben aus der Probe ausgeschlossen, auch wenn sie noch im Inaktive Artefakte bleiben aus der Probe ausgeschlossen, auch wenn sie noch im
Katalog stehen. Dynamische Send Ports, lokale Dateipfade, SMTP-Empfaengerlisten Katalog stehen. Dynamische Send Ports, lokale Dateipfade, SMTP-Empfängerlisten
und Named Pipes werden nicht als externe Host/Port-Ziele behandelt. und Named Pipes sowie relative HTTP-/WCF-Receive-Listener werden als erwartbar
nicht socket-prüfbar behandelt und erzeugen kein `UNKNOWN`. WCF-SQL-Adressen
im Format `mssql://server[:port]/instanz/datenbank` werden auf TCP 1433 oder
den expliziten Port reduziert. Schemafreie `host:port`-Angaben werden vor der
allgemeinen URI-Auswertung erkannt. Bei FTP und SFTP werden zusätzlich Angaben
der Form `[benutzer@]host[:port]/pfad` auf Host und expliziten beziehungsweise
adaptertypischen Standardport reduziert. Echte Auflösungslücken bleiben
sichtbar und nennen Artefakt, Adapter und Grund, aber niemals die vollständige
URI.
Der lokale Katalog Der lokale Katalog
`%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml` wird beim ersten `%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml` wird beim ersten
erfolgreichen Lauf erzeugt und danach standardmaessig alle 168 Stunden atomar erfolgreichen Lauf erzeugt und danach standardmäßig alle 168 Stunden atomar
abgeglichen. Er enthaelt keine vollstaendigen URIs, Querystrings, abgeglichen. Er enthält keine vollständigen URIs, Querystrings,
Benutzernamen oder Kennwoerter, sondern nur Artefaktbezug, Adapter, Host, Port Benutzernamen oder Kennwörter, sondern nur Artefaktbezug, Adapter, Host, Port
und `TCP`/`UDP`. Automatisch erzeugte Eintraege tragen und `TCP`/`UDP`. Automatisch erzeugte Einträge tragen
`autoDiscovered="true"`. Ein bewusst manueller Override wird mit `autoDiscovered="true"`. Ein bewusst manueller Override wird mit
`autoDiscovered="false"` markiert und beim Wochenabgleich erhalten; sein `autoDiscovered="false"` markiert und beim Wochenabgleich erhalten; sein
`key` muss dem Artefakteintrag entsprechen. Eigenstaendige manuelle Ziele `key` muss dem Artefakteintrag entsprechen. Eigenständige manuelle Ziele
verwenden `artifactType="Manual"`. verwenden `artifactType="Manual"`.
HTTP(S), SFTP, FTP, UNC/SMB, WCF und `net.tcp` werden ausschliesslich per HTTP(S), SFTP, FTP, UNC/SMB, WCF und `net.tcp` werden ausschließlich per
TCP-Connect geprueft. Es findet kein fachlicher Request und keine Anmeldung TCP-Connect geprüft. Es findet kein fachlicher Request und keine Anmeldung
statt. Explizite `udp://host:port`-Eintraege senden ein einzelnes Datagramm; statt. Explizite `udp://host:port`-Einträge senden ein einzelnes Datagramm;
das bestaetigt DNS, Route und lokalen Versand, kann ohne das bestätigt DNS, Route und lokalen Versand, kann ohne
applikationsspezifisches Antwortprotokoll aber keinen entfernten Dienstzustand applikationsspezifisches Antwortprotokoll aber keinen entfernten Dienstzustand
beweisen. Gemeinsame Host/Port-Ziele werden dedupliziert und mit begrenzter beweisen. Gemeinsame Host/Port-Ziele werden dedupliziert und mit begrenzter
Parallelitaet getestet. Parallelität getestet.
Der Checkmk-Service nennt im OK-Fall nur die Gesamtzahl. Bei Ausfaellen werden Der Checkmk-Service nennt im OK-Fall nur die Gesamtzahl. Bei Ausfällen werden
ausschliesslich die nicht erreichbaren Artefakte in `unavailable=` angezeigt; ausschließlich die nicht erreichbaren Artefakte in `unavailable=` angezeigt;
`MaxSummaryItems` und `MaxDetailCharacters` begrenzen die Darstellung. Alle `MaxSummaryItems` und `MaxDetailCharacters` begrenzen die Darstellung. Alle
Einzelfehler bleiben vollstaendig im Provider-Log. Einzelfehler bleiben vollständig im Provider-Log.
### 5.4 SQL-Zugriffsprobe ### 5.4 SQL-Zugriffsprobe
Der Provider oeffnet fuer Management- und Master-MessageBox-Datenbank eine Der Provider öffnet für Management- und Master-MessageBox-Datenbank eine
kurze `System.Data.SqlClient`-Verbindung mit integrierter kurze `System.Data.SqlClient`-Verbindung mit integrierter
Windows-Authentifizierung, fuehrt `SELECT 1` aus und schliesst sie. Keine Windows-Authentifizierung, führt `SELECT 1` aus und schließt sie. Keine
BizTalk-Tabelle wird direkt gelesen oder veraendert. BizTalk-Tabelle wird direkt gelesen oder verändert.
Die Ausgabe `execution_identity=` zeigt deshalb das Provider-Konto, nicht mehr Die Ausgabe `execution_identity=` zeigt deshalb das Provider-Konto, nicht mehr
`NT AUTHORITY\SYSTEM`. Der Check beweist, dass genau das Scheduled-Task-Konto `NT AUTHORITY\SYSTEM`. Der Check beweist, dass genau das Scheduled-Task-Konto
@@ -293,10 +301,10 @@ und filtert Quellen wie `BizTalk Server`, `XLANG/s`, `ENTSSO`,
### 6.1 Bevorzugte BizTalk-Rolle ### 6.1 Bevorzugte BizTalk-Rolle
BizTalk Server 2020 fuehrte die `BizTalk Server Read Only Users`-Gruppe ein. BizTalk Server 2020 führte die `BizTalk Server Read Only Users`-Gruppe ein.
Sie darf Artefakte, Servicezustand, Message Flow und Trackinginformationen Sie darf Artefakte, Servicezustand, Message Flow und Trackinginformationen
lesen, aber keine administrativen Operationen ausfuehren. SQL-seitig wird sie lesen, aber keine administrativen Operationen ausführen. SQL-seitig wird sie
ueber `BTS_READONLY_USERS` vermittelt. über `BTS_READONLY_USERS` vermittelt.
Soll: Soll:
@@ -311,16 +319,16 @@ Die genaue Gruppe ist aus
`MSBTS_GroupSetting.BizTalkReadOnlyUserGroup` beziehungsweise der `MSBTS_GroupSetting.BizTalkReadOnlyUserGroup` beziehungsweise der
BizTalk-Administration zu lesen. Nicht vom Standardnamen ausgehen. BizTalk-Administration zu lesen. Nicht vom Standardnamen ausgehen.
Die Operator-Gruppe (`BTS_OPERATORS`) wird erst geprueft, wenn eine konkret Die Operator-Gruppe (`BTS_OPERATORS`) wird erst geprüft, wenn eine konkret
benannte erforderliche WMI-Klasse trotz nachgewiesener Read-Only-Konfiguration benannte erforderliche WMI-Klasse trotz nachgewiesener Read-Only-Konfiguration
und neuem Anmeldetoken abgewiesen wird. Die Erweiterung muss mit Security, und neuem Anmeldetoken abgewiesen wird. Die Erweiterung muss mit Security,
BizTalk- und SQL-Administration begruendet werden. BizTalk- und SQL-Administration begründet werden.
Unzulaessig: Unzulässig:
- Checkmk-Dienstkonto aendern, - Checkmk-Dienstkonto ändern,
- Maschinenkonto pauschal berechtigen, - Maschinenkonto pauschal berechtigen,
- individueller SQL-Login fuer das Provider-Konto, - individueller SQL-Login für das Provider-Konto,
- direkte ad-hoc DB-Rollen, - direkte ad-hoc DB-Rollen,
- `db_owner` oder `sysadmin`, - `db_owner` oder `sysadmin`,
- WMI-/DCOM-Vollzugriff als Reaktion auf einen SQL-Loginfehler. - WMI-/DCOM-Vollzugriff als Reaktion auf einen SQL-Loginfehler.
@@ -328,7 +336,7 @@ Unzulaessig:
### 6.2 Dateisystem-ACL ### 6.2 Dateisystem-ACL
Der Installer entfernt die Vererbung an den Runtimeverzeichnissen und setzt Der Installer entfernt die Vererbung an den Runtimeverzeichnissen und setzt
explizite Eintraege mit sprachneutralen Well-known-SIDs: explizite Einträge mit sprachneutralen Well-known-SIDs:
| Ziel | Collector | SYSTEM | lokale Administratoren | | Ziel | Collector | SYSTEM | lokale Administratoren |
| --- | --- | --- | --- | | --- | --- | --- | --- |
@@ -337,7 +345,7 @@ explizite Eintraege mit sprachneutralen Well-known-SIDs:
| `logs` | Modify | Modify | F | | `logs` | Modify | Modify | F |
`LocalSystem` kann deshalb den Snapshot lesen, aber im Datenverzeichnis nicht `LocalSystem` kann deshalb den Snapshot lesen, aber im Datenverzeichnis nicht
schreiben. Schreibrecht im Logverzeichnis wird benoetigt, um abgelehnte schreiben. Schreibrecht im Logverzeichnis wird benötigt, um abgelehnte
Snapshots aus dem Consumer-Kontext zu protokollieren. Snapshots aus dem Consumer-Kontext zu protokollieren.
## 7. Scheduled Task ## 7. Scheduled Task
@@ -363,10 +371,10 @@ Sollkonfiguration:
| StartWhenAvailable | aktiv | | StartWhenAvailable | aktiv |
| Restart | zweimal im Minutenabstand | | Restart | zweimal im Minutenabstand |
Der Task wird mit gespeichertem Dienstkontokennwort ausgefuehrt, also Der Task wird mit gespeichertem Dienstkontokennwort ausgeführt, also
unabhaengig von einer interaktiven Anmeldung. Bei Kennwortwechsel oder unabhängig von einer interaktiven Anmeldung. Bei Kennwortwechsel oder
-ablauf muss das Task-Kennwort aktualisiert werden. Ein gMSA ist weiterhin -ablauf muss das Task-Kennwort aktualisiert werden. Ein gMSA ist weiterhin
optional unterstuetzt, aber nicht die produktive Standardannahme. optional unterstützt, aber nicht die produktive Standardannahme.
## 8. Installation ## 8. Installation
@@ -380,13 +388,11 @@ scripts\package-release.cmd
Der Release-Build der Visual-Studio-2019-Solution baut Anwendung, grafischen Der Release-Build der Visual-Studio-2019-Solution baut Anwendung, grafischen
Installer und Packaging-Werkzeug. Danach liegen der Paketordner Installer und Packaging-Werkzeug. Danach liegen der Paketordner
`artifacts\BizTalkCheckmkPulse-Setup` und das direkt transportierbare `artifacts\BizTalkCheckmkPulse-Setup` und das direkt transportierbare
`artifacts\BizTalkCheckmkPulse-Setup.zip` bereit. Zusaetzlich entstehen `artifacts\BizTalkCheckmkPulse-Setup.zip` bereit.
`BizTalkCheckmkPulse-Setup.zip.b64.txt` fuer `certutil -decode` und eine
SHA-256-Textdatei.
### 8.2 Normales Servicekonto ### 8.2 Normales Servicekonto
Das ZIP wird auf dem Zielserver vollstaendig entpackt. Danach `Setup.exe` als Das ZIP wird auf dem Zielserver vollständig entpackt. Danach `Setup.exe` als
Administrator starten, Konto im Format `DOMAIN\Benutzer` (zum Beispiel Administrator starten, Konto im Format `DOMAIN\Benutzer` (zum Beispiel
`BEW\t231bizmon`), Kennwort und Umgebung eintragen und die Installation `BEW\t231bizmon`), Kennwort und Umgebung eintragen und die Installation
starten. starten.
@@ -396,16 +402,64 @@ AD-/BizTalk-/SQL-Rechte; diese bleiben getrennte administrative Freigaben.
Beim Update wird zuerst die neue Paketversion im Staging-Verzeichnis mit neun Beim Update wird zuerst die neue Paketversion im Staging-Verzeichnis mit neun
Self-Test-Zeilen validiert. Bestehende AppSettings werden in die neue Config Self-Test-Zeilen validiert. Bestehende AppSettings werden in die neue Config
uebernommen; neue Keys stammen aus der neuen Standardkonfiguration. Die übernommen; neue Keys stammen aus der neuen Standardkonfiguration. Die
Runtime-Daten unter `%ProgramData%` bleiben bestehen. Erst danach wird der Runtime-Daten unter `%ProgramData%` bleiben bestehen. Erst danach wird der
Task gestoppt und die Programmversion umgeschaltet. Scheitert ein Folgeschritt, 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 zurueckzusetzen. auf den vorherigen Stand zurückzusetzen.
Der installierte Task selbst laeuft mit `RunLevel Limited`. Der Installer Ab Version 2.2.4 ist die Umschaltung erst nach einer transaktionalen
uebergibt das Kennwort direkt an die Windows-Task-Scheduler-COM-Schnittstelle; 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
ü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
geschrieben. Das Konto benoetigt `Log on as a batch job`. geschrieben. Das Konto benötigt `Log on as a batch job`.
### 8.3 Optionales gMSA ### 8.3 Optionales gMSA
@@ -420,15 +474,15 @@ Der installierte Wrapper liegt unter:
%ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd %ProgramData%\checkmk\agent\local\biztalk_checkmk_pulse.cmd
``` ```
Der Agent darf den Consumer synchron ausfuehren. Er liest nur maximal 1 MiB Der Agent darf den Consumer synchron ausführen. Er liest nur maximal 1 MiB
lokale Daten; ein Checkmk-Async-Cache ist daher nicht notwendig. Falls alte lokale Daten; ein Checkmk-Async-Cache ist daher nicht notwendig. Falls alte
`check_mk.user.yml`-Regeln fuer den bisherigen 300-Sekunden-Cache existieren, `check_mk.user.yml`-Regeln für den bisherigen 300-Sekunden-Cache existieren,
sollten sie entfernt werden, damit Checkmk den minuetlich aktualisierten sollten sie entfernt werden, damit Checkmk den minütlich aktualisierten
Snapshot nicht zusaetzlich verzoegert. Snapshot nicht zusätzlich verzögert.
Nach dem Agent-Dump: Nach dem Agent-Dump:
1. Service Discovery fuer den BizTalk-Host ausfuehren. 1. Service Discovery für den BizTalk-Host ausführen.
2. neun stabile Services aufnehmen; den alten Service 2. neun stabile Services aufnehmen; den alten Service
`BizTalk Runtime Artifacts` nach erfolgreicher Discovery entfernen. `BizTalk Runtime Artifacts` nach erfolgreicher Discovery entfernen.
3. Changes aktivieren. 3. Changes aktivieren.
@@ -443,24 +497,24 @@ Pfad:
biztalk-checkmk-pulse-YYYYMMDD.log biztalk-checkmk-pulse-YYYYMMDD.log
``` ```
Jede Zeile enthaelt UTC-Zeit, Level, Komponente, PID, Windows-Identitaet und Jede Zeile enthält UTC-Zeit, Level, Komponente, PID, Windows-Identität und
einzeilige Nachricht. Provider-Erfolge werden pro Lauf geloggt; der Consumer einzeilige Nachricht. Provider-Erfolge werden pro Lauf geloggt; der Consumer
loggt nur abgelehnte Snapshots. Exceptions werden mit Typ, Nachricht, loggt nur abgelehnte Snapshots. Exceptions werden mit Typ, Nachricht,
HRESULT-/Providerdetails und Stacktrace einzeilig gespeichert. HRESULT-/Providerdetails und Stacktrace einzeilig gespeichert.
Der erfolgreiche Abschluss nennt Suspensions, Routing Failure Reports, Der erfolgreiche Abschluss nennt Suspensions, Routing Failure Reports,
Receive-Location-/Send-Port-Zahlen und Laufzeit. Jede strukturierte Receive-Location-/Send-Port-Zahlen und Laufzeit. Jede strukturierte
WMI-/SQL-/Event-Log-Diagnose wird zusaetzlich als eigene `WARN`-Zeile WMI-/SQL-/Event-Log-Diagnose wird zusätzlich als eigene `WARN`-Zeile
protokolliert und bleibt damit auch ausserhalb der gekuerzten Checkmk-Summary protokolliert und bleibt damit auch außerhalb der gekürzten Checkmk-Summary
vollstaendig nachvollziehbar. vollständig nachvollziehbar.
Logging ist best effort: Ein blockiertes Log darf Checkmk-Ausgabe oder Logging ist best effort: Ein blockiertes Log darf Checkmk-Ausgabe oder
Snapshot-Publikation nicht zerstoeren. Der Provider entfernt beim Start Dateien Snapshot-Publikation nicht zerstören. Der Provider entfernt beim Start Dateien
aelter als `LogRetentionDays`. älter als `LogRetentionDays`.
## 10. Diagnose und Runbook ## 10. Diagnose und Runbook
### 10.1 Grundpruefung ### 10.1 Grundprüfung
```cmd ```cmd
schtasks.exe /query /tn "BizTalk Checkmk Pulse Provider" /v /fo list schtasks.exe /query /tn "BizTalk Checkmk Pulse Provider" /v /fo list
@@ -472,42 +526,42 @@ type "%ProgramData%\BizTalkCheckmkPulse\logs\biztalk-checkmk-pulse-*.log"
### 10.2 Kein Snapshot ### 10.2 Kein Snapshot
Pruefen: Prüfen:
1. Task existiert und ist aktiviert. 1. Task existiert und ist aktiviert.
2. `LastTaskResult` und Provider-Log. 2. `LastTaskResult` und Provider-Log.
3. Dienstkonto kann sich als Batch anmelden. 3. Dienstkonto kann sich als Batch anmelden.
4. gespeichertes Task-Kennwort ist nach Rotation/Ablauf noch gueltig. 4. gespeichertes Task-Kennwort ist nach Rotation/Ablauf noch gültig.
5. Provider besitzt Modify auf `data` und `logs`. 5. Provider besitzt Modify auf `data` und `logs`.
6. EXE/Config sind ausfuehrbar. 6. EXE/Config sind ausführbar.
### 10.3 Staler Snapshot ### 10.3 Staler Snapshot
Der Consumer nennt Alter und Grenzwert. Ursachen: Der Consumer nennt Alter und Grenzwert. Ursachen:
- Task deaktiviert oder Konto/Kennwort ungueltig, - Task deaktiviert oder Konto/Kennwort ungültig,
- vorheriger Lauf haengt, - vorheriger Lauf hängt,
- WMI-/SQL-Timeouts, - WMI-/SQL-Timeouts,
- Task Scheduler lief waehrend Wartung nicht, - Task Scheduler lief während Wartung nicht,
- Snapshot-Publikation wird durch AV/EDR oder ACL blockiert. - Snapshot-Publikation wird durch AV/EDR oder ACL blockiert.
Nicht einfach `SnapshotMaxAgeSeconds` stark erhoehen. Zuerst den minuetlichen Nicht einfach `SnapshotMaxAgeSeconds` stark erhöhen. Zuerst den minütlichen
Lauf wiederherstellen. Bei geplanten Wartungen darf Checkmk bewusst `UNKNOWN` Lauf wiederherstellen. Bei geplanten Wartungen darf Checkmk bewusst `UNKNOWN`
anzeigen. anzeigen.
### 10.4 SHA-256-/Formatfehler ### 10.4 SHA-256-/Formatfehler
Snapshot nicht reparieren oder direkt an Checkmk kopieren. Task neu starten, Snapshot nicht reparieren oder direkt an Checkmk kopieren. Task neu starten,
Datentraeger und AV/EDR pruefen. Wiederholt sich der Fehler, Temporaerdateien, Datenträger und AV/EDR prüfen. Wiederholt sich der Fehler, Temporärdateien,
NTFS-Ereignisse und Schreibrechte untersuchen. NTFS-Ereignisse und Schreibrechte untersuchen.
### 10.5 BizTalk-/SQL-Permission ### 10.5 BizTalk-/SQL-Permission
1. Log zeigt `execution_identity` des Providers. 1. Log zeigt `execution_identity` des Providers.
2. `BizTalkReadOnlyUserGroup` gegen BizTalk-Konfiguration abgleichen. 2. `BizTalkReadOnlyUserGroup` gegen BizTalk-Konfiguration abgleichen.
3. AD-Gruppenmitgliedschaft des Provider-Kontos pruefen. 3. AD-Gruppenmitgliedschaft des Provider-Kontos prüfen.
4. neuen Logontoken durch neuen Tasklauf sicherstellen. 4. neuen Logontoken durch neuen Tasklauf sicherstellen.
5. SQL-seitig Gruppenlogin und `BTS_READONLY_USERS` read-only pruefen. 5. SQL-seitig Gruppenlogin und `BTS_READONLY_USERS` read-only prüfen.
6. keine Einzelrechte erteilen. 6. keine Einzelrechte erteilen.
7. Nur wenn eine spezifische Klasse weiter abgewiesen wird, Read-Only-Umfang 7. Nur wenn eine spezifische Klasse weiter abgewiesen wird, Read-Only-Umfang
mit Microsoft-Dokumentation und BizTalk-Betrieb abgleichen; Operator-Rolle mit Microsoft-Dokumentation und BizTalk-Betrieb abgleichen; Operator-Rolle
@@ -516,8 +570,8 @@ NTFS-Ereignisse und Schreibrechte untersuchen.
### 10.6 WMI-Schema ### 10.6 WMI-Schema
`InvalidClass` oder `InvalidQuery` wird als `Wmi/Schema` klassifiziert. `InvalidClass` oder `InvalidQuery` wird als `Wmi/Schema` klassifiziert.
Berechtigungserhoehung kann eine nicht vorhandene Klasse nicht erzeugen. Berechtigungserhöhung kann eine nicht vorhandene Klasse nicht erzeugen.
Installierte BizTalk-Version, WQL und Properties pruefen. Installierte BizTalk-Version, WQL und Properties prüfen.
## 11. Konfiguration ## 11. Konfiguration
@@ -544,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` |
@@ -553,7 +609,8 @@ BizTalk-Probes:
| `EndpointDiscoveryIntervalHours` | `168` | | `EndpointDiscoveryIntervalHours` | `168` |
| `EndpointProbeTimeoutMilliseconds` | `3000` | | `EndpointProbeTimeoutMilliseconds` | `3000` |
| `EndpointProbeMaxConcurrency` | `16` | | `EndpointProbeMaxConcurrency` | `16` |
| `EndpointMaxCount` | `100` | | `EndpointCatalogMaxEntries` | `1000` |
| `EndpointMaxCount` | `100` eindeutige Netzwerkziele |
Alarmierung: Alarmierung:
@@ -583,57 +640,66 @@ Automatisiert:
- WMI-Queryvertrag nutzt dokumentierte Properties, - WMI-Queryvertrag nutzt dokumentierte Properties,
- eingebetteter SQL-Loginfehler wird `Permission`, - eingebetteter SQL-Loginfehler wird `Permission`,
- abgewiesener Principal wird extrahiert, - abgewiesener Principal wird extrahiert,
- Routing Failure Reports werden separat und als non-resumable gezaehlt, - Routing Failure Reports werden separat und als non-resumable gezählt,
- Receive-/Send-Allowlisten trennen bewusste von unerwarteten Aus-Zustaenden, - Receive-/Send-Allowlisten trennen bewusste von unerwarteten Aus-Zuständen,
- ein fehlender `IsDisabled`-Wert wird `UNKNOWN` statt still als enabled, - ein fehlender `IsDisabled`-Wert wird `UNKNOWN` statt still als enabled,
- betroffene Namen und Gesamtlaenge der Summary bleiben begrenzt, - betroffene Namen und Gesamtlänge der Summary bleiben begrenzt,
- Endpoint-Adressen werden ohne URI-Secrets auf Host/Port reduziert, - Endpoint-Adressen werden ohne URI-Secrets auf Host/Port reduziert,
- manuelle Endpoint-Overrides ueberleben den automatischen Wochenabgleich, - manuelle Endpoint-Overrides überleben den automatischen Wochenabgleich,
- nur nicht erreichbare Endpoints erscheinen im Checkmk-Detail, - nur nicht erreichbare oder wirklich unaufgelöste Endpoints erscheinen im
- 70 vollstaendig timeoutende eindeutige Ziele bleiben mit den Defaults in Checkmk-Detail,
- 70 vollständig timeoutende eindeutige Ziele bleiben mit den Defaults in
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 erhaelt bestehende Werte und fuegt 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,
- Consumer-Fallback enthaelt neun `UNKNOWN`-Services. - Consumer-Fallback enthält neun `UNKNOWN`-Services.
Windows-/ACC-Abnahme: Windows-/ACC-Abnahme:
1. `LastTaskResult=0`. 1. `LastTaskResult=0`.
2. Snapshot wird mindestens dreimal minuetlich aktualisiert. 2. Snapshot wird mindestens dreimal minütlich aktualisiert.
3. Provider-Log nennt das dedizierte Konto. 3. Provider-Log nennt das dedizierte Konto.
4. `BizTalk Platform` zeigt `read_only_group=`. 4. `BizTalk Platform` zeigt `read_only_group=`.
5. `BizTalk SQL Access`: `targets=2`, `available=2`. 5. `BizTalk SQL Access`: `targets=2`, `available=2`.
6. keine Permission-`UNKNOWN`s. 6. keine Permission-`UNKNOWN`s.
7. Suspensionsmetriken mit der BizTalk Group Hub Page plausibilisieren, 7. Suspensionsmetriken mit der BizTalk Group Hub Page plausibilisieren,
einschliesslich Routing Failure Reports. einschließlich Routing Failure Reports.
8. Eine bewusst deaktivierte Receive Location und einen inaktiven Send Port 8. Eine bewusst deaktivierte Receive Location und einen inaktiven Send Port
ueber die exakten Allowlisten als expected bestaetigen. über die exakten Allowlisten als expected bestätigen.
9. Einen Testnamen aus der Allowlist entfernen und den erwarteten CRIT mit 9. Einen Testnamen aus der Allowlist entfernen und den erwarteten CRIT mit
kurzem `affected=`-Detail pruefen. kurzem `affected=`-Detail prüfen.
10. ACL-Test: `LocalSystem` kann Snapshot lesen, nicht schreiben. 10. ACL-Test: `LocalSystem` kann Snapshot lesen, nicht schreiben.
11. `endpoints.xml` wurde erzeugt; sie enthaelt keine vollstaendigen URIs oder 11. `endpoints.xml` wurde erzeugt; sie enthält keine vollständigen URIs oder
Zugangsdaten und nur Started-/Enabled-Artefakte. Zugangsdaten und nur Started-/Enabled-Artefakte.
12. Einen erreichbaren Testport sperren: nur `Endpoint Reachability` wird CRIT 12. `Endpoint Reachability` zeigt `unresolved=0`; erwartbar lokale oder
und nennt ausschliesslich das nicht erreichbare Ziel. adapterinterne Adressen stehen nur unter `expected_non_socket` und bleiben OK.
13. Ein Artefakt stoppen/deaktivieren: es wird im naechsten Minutenlauf nicht 13. Einen erreichbaren Testport sperren: nur `Endpoint Reachability` wird CRIT
mehr geprueft. und nennt ausschließlich das nicht erreichbare Ziel.
14. Katalogzeit auf aelter als 168 Stunden setzen beziehungsweise Intervall 14. Ein Artefakt stoppen/deaktivieren: es wird im nächsten Minutenlauf nicht
mehr geprüft.
15. Katalogzeit auf älter als 168 Stunden setzen beziehungsweise Intervall
testweise reduzieren: erfolgreicher atomarer Umgebungsabgleich. testweise reduzieren: erfolgreicher atomarer Umgebungsabgleich.
15. Task deaktivieren: nach 180 Sekunden neun stale-`UNKNOWN`s. 16. Task deaktivieren: nach 180 Sekunden neun stale-`UNKNOWN`s.
16. Task wieder aktivieren: naechster Snapshot stellt Echtzustand her. 17. Task wieder aktivieren: nächster Snapshot stellt Echtzustand her.
17. Agent-Dump und Checkmk Service Discovery erfolgreich. 18. Agent-Dump und Checkmk Service Discovery erfolgreich.
Erst nach ACC-Abnahme erfolgt der gestufte Rollout nach DEV/TST/PRD. Erst nach ACC-Abnahme erfolgt der gestufte Rollout nach DEV/TST/PRD.
## 13. Deinstallation und Rollback ## 13. Deinstallation und Rollback
`Setup.exe` aus dem entpackten Paket erneut starten und **Deinstallieren** `Setup.exe` aus dem entpackten Paket erneut starten und **Deinstallieren**
waehlen. Der Installer entfernt Task, Wrapper und Programmdateien, behaelt wählen. Der Installer entfernt Task, Wrapper und Programmdateien, behält
Logs und Snapshot aber absichtlich bei. Die AD-Gruppenmitgliedschaft wird Logs und Snapshot aber absichtlich bei. Die AD-Gruppenmitgliedschaft wird
nicht geaendert und separat nach Change-Verfahren entzogen. nicht geändert und separat nach Change-Verfahren entzogen.
Rollback auf die alte direkte `LocalSystem`-Architektur ist nicht empfohlen. Rollback auf die alte direkte `LocalSystem`-Architektur ist nicht empfohlen.
Falls kurzfristig notwendig, muss Security die Maschinenkonto-Berechtigung Falls kurzfristig notwendig, muss Security die Maschinenkonto-Berechtigung
@@ -650,6 +716,8 @@ Collector-Konto umgestellt werden.
- https://learn.microsoft.com/en-us/biztalk/core/types-of-message-failures - https://learn.microsoft.com/en-us/biztalk/core/types-of-message-failures
- https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-isdisabled-property-wmi - https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-isdisabled-property-wmi
- https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-status-property-wmi - https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-status-property-wmi
- https://learn.microsoft.com/en-us/biztalk/adapters-and-accelerators/adapter-sql/create-the-sql-server-connection-uri
- https://learn.microsoft.com/en-us/biztalk/adapters-and-accelerators/adapter-oracle-database/create-the-oracle-database-connection-uri
- https://docs.checkmk.com/latest/en/agent_windows.html - https://docs.checkmk.com/latest/en/agent_windows.html
- https://docs.checkmk.com/latest/en/localchecks.html - https://docs.checkmk.com/latest/en/localchecks.html
- https://docs.checkmk.com/latest/en/spool_directory.html - https://docs.checkmk.com/latest/en/spool_directory.html
+206 -132
View File
@@ -1,34 +1,58 @@
# BizTalk Checkmk Pulse # BizTalk Checkmk Pulse
`BizTalk Checkmk Pulse` ist ein robuster Checkmk-2.4-Local-Check fuer Microsoft `BizTalk Checkmk Pulse` ist ein robuster Checkmk-2.4-Local-Check für Microsoft
BizTalk Server 2020 auf Windows Server 2019. Die Anwendung trennt den BizTalk Server 2020 auf Windows Server 2019. Die Anwendung trennt den
berechtigten BizTalk-Datenzugriff vollstaendig vom Checkmk-Agenten: berechtigten BizTalk-Datenzugriff vollständig vom Checkmk-Agenten:
```text ![Architektur von BizTalk Checkmk Pulse mit Provider, Endpoint-Probes, Snapshot und Checkmk-Consumer](docs/architecture.svg)
Scheduled Task (normales dediziertes Servicekonto)
|
| jede Minute: BizTalkCheckmkPulse.exe --collect
v
lokales BizTalk-WMI + BizTalk-SQL + Application Event Log
|
| atomarer, versionierter, SHA-256-geschuetzter Snapshot
v
%ProgramData%\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot
^
| nur lesen: BizTalkCheckmkPulse.exe --consume
|
Checkmk Windows Agent (LocalSystem)
|
v
neun kompakte Checkmk Local Checks
```
Damit bekommt `LocalSystem` keine BizTalk- oder SQL-Berechtigung. Nur das Damit bekommt `LocalSystem` keine BizTalk- oder SQL-Berechtigung. Nur das
dedizierte Provider-Konto wird in die konfigurierte BizTalk Server Read Only dedizierte Provider-Konto wird in die konfigurierte BizTalk Server Read Only
Users-Gruppe aufgenommen. Der Checkmk-Consumer liest keine BizTalk-Datenbank, Users-Gruppe aufgenommen. Der Checkmk-Consumer liest keine BizTalk-Datenbank,
verwendet kein WMI und nimmt keine Identitaetswechsel vor. verwendet kein WMI und nimmt keine Identitätswechsel vor.
## Warum die Architektur geaendert wurde ## Endpoint-Erreichbarkeitsprüfung
Der Provider prüft die netzwerktechnische Erreichbarkeit der externen Ziele
aktiver BizTalk Send Ports und Receive Locations. Die Funktion ist im eigenen
Checkmk-Service `BizTalk Endpoint Reachability` zusammengefasst:
- Berücksichtigt werden nur Send Ports mit `Status=Started` und Receive
Locations mit `IsDisabled=false`. Dynamische Send Ports werden nicht als
statisches Ziel interpretiert.
- Fehlt der lokale Endpoint-Katalog, wird er aus den bereits gelesenen
`MSBTS_SendPort`- und `MSBTS_ReceiveLocation`-Daten aufgebaut. Alle
168 Stunden erfolgt ein vollständiger Abgleich mit der BizTalk-Umgebung.
- HTTP(S), SFTP, FTP, WCF/`net.tcp` und UNC/SMB werden durch einen reinen
TCP-Verbindungsaufbau zu Host und Port geprüft. Explizite
`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
BizTalk-Nachrichten gesendet. Der Check validiert DNS, Netzwerkpfad,
Firewall und bei TCP einen annehmenden Zielport, nicht die fachliche
Funktion des entfernten Dienstes.
- Im OK-Fall meldet Checkmk nur, dass alle aktiven Endpunkte erreichbar sind.
Bei einem Fehler werden ausschließlich die nicht erreichbaren Endpunkte
ausgegeben; lange Listen sind begrenzt, die Metriken bleiben vollständig.
- Doppelte Protokoll-/Host-/Port-Ziele werden nur einmal verbunden. Mit den Defaults
benötigen 70 vollständig timeoutende eindeutige Ziele theoretisch etwa
15 Sekunden; die harte Standardgrenze von 100 Zielen liegt bei etwa
21 Sekunden.
Der Checkmk-Agent führt diese Netzwerkprobes nicht selbst aus. Er liest als
`LocalSystem` weiterhin ausschließlich den validierten lokalen Snapshot.
Details zum Katalog und zu manuellen Overrides stehen in
[docs/EndpointCatalog.md](docs/EndpointCatalog.md).
## Warum die Architektur geändert wurde
Der ACC-Test vom 29.07.2026 zeigte: Der ACC-Test vom 29.07.2026 zeigte:
@@ -37,44 +61,48 @@ Login failed for user 'BEW\AV23AGPWBIO1$'
``` ```
Der Checkmk-Agent lief korrekt als `NT AUTHORITY\SYSTEM` und erreichte lokales Der Checkmk-Agent lief korrekt als `NT AUTHORITY\SYSTEM` und erreichte lokales
BizTalk-WMI. Datenbankgestuetzte WMI-Abfragen wurden jedoch am SQL Server als BizTalk-WMI. Datenbankgestützte WMI-Abfragen wurden jedoch am SQL Server als
Maschinenkonto `BEW\AV23AGPWBIO1$` abgewiesen. Eine Mitgliedschaft des Maschinenkonto `BEW\AV23AGPWBIO1$` abgewiesen. Eine Mitgliedschaft des
Computerkontos wuerde die BizTalk-Netzwerkberechtigung allen als `LocalSystem` Computerkontos würde die BizTalk-Netzwerkberechtigung allen als `LocalSystem`
laufenden Diensten des Servers geben. laufenden Diensten des Servers geben.
Die neue Trennung reduziert diesen Berechtigungsumfang. Fuer BizTalk Server Die neue Trennung reduziert diesen Berechtigungsumfang. Für BizTalk Server
2020 ist die konfigurierte `BizTalk Server Read Only Users`-Gruppe mit der 2020 ist die konfigurierte `BizTalk Server Read Only Users`-Gruppe mit der
SQL-Datenbankrolle `BTS_READONLY_USERS` die bevorzugte Grenze. Die SQL-Datenbankrolle `BTS_READONLY_USERS` die bevorzugte Grenze. Die
`BizTalk Server Operators`-Gruppe ist nur ein dokumentierter Fallback, wenn `BizTalk Server Operators`-Gruppe ist nur ein dokumentierter Fallback, wenn
eine konkret benoetigte WMI-Klasse trotz bestaetigter Read-Only-Zuordnung eine konkret benötigte WMI-Klasse trotz bestätigter Read-Only-Zuordnung
abgewiesen wird. Direkte SQL-Logins, manuelle Datenbankrollen und `sysadmin` abgewiesen wird. Direkte SQL-Logins, manuelle Datenbankrollen und `sysadmin`
sind keine Loesung. sind keine Lösung.
## Robustheit ## Robustheit
Der Datenaustausch ist bewusst defensiv: Der Datenaustausch ist bewusst defensiv:
- Der Provider schreibt zuerst eine eindeutige Temporaerdatei im Zielordner, - Der Provider schreibt zuerst eine eindeutige Temporärdatei im Zielordner,
leert Betriebssystempuffer und ersetzt danach den Snapshot atomar. leert Betriebssystempuffer und ersetzt danach den Snapshot atomar.
- Der Snapshot enthaelt Formatversion, UTC-Zeit, Quellmaschine, - Der Snapshot enthält Formatversion, UTC-Zeit, Quellmaschine,
Provider-Identitaet, Zeilenanzahl und SHA-256 des Payloads. Provider-Identität, Zeilenanzahl und SHA-256 des Payloads.
- Der Consumer akzeptiert nur denselben Rechner, gueltiges UTF-8, intakte - Der Consumer akzeptiert nur denselben Rechner, gültiges UTF-8, intakte
Checkmk-Zeilen, korrekte SHA-256-Pruefsumme und ein maximales Alter von Checkmk-Zeilen, korrekte SHA-256-Prüfsumme und ein maximales Alter von
standardmaessig 180 Sekunden. standardmäßig 180 Sekunden.
- Fehlende, veraltete, abgeschnittene, manipulierte oder unlesbare Dateien - Fehlende, veraltete, abgeschnittene, manipulierte oder unlesbare Dateien
ergeben neun gueltige `UNKNOWN`-Services statt einer kaputten Agent-Ausgabe. ergeben neun gültige `UNKNOWN`-Services statt einer kaputten Agent-Ausgabe.
- Ein exklusives Lock und die Task-Einstellung `IgnoreNew` verhindern - Ein exklusives Lock und die Task-Einstellung `IgnoreNew` verhindern
ueberlappende Providerlaeufe. überlappende Providerläufe.
- Ein unerwarteter Providerfehler erzeugt nach Moeglichkeit einen aktuellen - Ein unerwarteter Providerfehler erzeugt nach Möglichkeit einen aktuellen
`UNKNOWN`-Snapshot und einen ungleich null lautenden Task-Exitcode. `UNKNOWN`-Snapshot und einen ungleich null lautenden Task-Exitcode.
- Provider und Consumer protokollieren in taegliche Dateien; die Aufbewahrung - Provider und Consumer protokollieren in tägliche Dateien; die Aufbewahrung
ist standardmaessig 30 Tage. ist standardmäßig 30 Tage.
- Snapshotgroesse, WMI-/SQL-Timeouts, Log-Retention und Stale-Grenze sind - 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
begrenzt und konfigurierbar. begrenzt und konfigurierbar.
## Erzeugte Services ## Erzeugte Services
Standardmaessig entstehen: Standardmäßig entstehen:
- `BizTalk Platform` - `BizTalk Platform`
- `BizTalk SQL Access` - `BizTalk SQL Access`
@@ -93,30 +121,46 @@ Zustand und Graphen direkt erkennbar, ohne eine lange Sammelzeile zu lesen:
- Receive Locations: total, enabled, unerwartet/bewusst disabled und unbekannt - Receive Locations: total, enabled, unerwartet/bewusst disabled und unbekannt
- Send Ports: total, started, stopped, bound, unbekannt sowie bewusst inactive - Send Ports: total, started, stopped, bound, unbekannt sowie bewusst inactive
- Endpoint Reachability: nur aktive Send-/Receive-Artefakte; im OK-Fall nur - Endpoint Reachability: nur aktive Send-/Receive-Artefakte; im OK-Fall nur
eine Gesamtaussage, im Fehlerfall ausschliesslich nicht erreichbare Ziele eine Gesamtaussage, im Fehlerfall ausschließlich nicht erreichbare oder
wirklich unaufgelöste Ziele
- Orchestrations: total, started, stopped, bound, unbound und unbekannt - Orchestrations: total, started, stopped, bound, unbound und unbekannt
Der Provider nutzt fuer die Endpoint-Discovery keine neue WMI-Klasse. Er liest Der Provider nutzt für die Endpoint-Discovery keine neue WMI-Klasse. Er liest
`PTAddress`, `STAddress`, `PTTransportType`, `STTransportType`, `PTAddress`, `STAddress`, `PTTransportType`, `STTransportType`,
`InboundTransportURL` und `AdapterName` aus den bereits vorhandenen `InboundTransportURL` und `AdapterName` aus den bereits vorhandenen
`MSBTS_SendPort`-/`MSBTS_ReceiveLocation`-Abfragen. Der Check selbst ist ein `MSBTS_SendPort`-/`MSBTS_ReceiveLocation`-Abfragen. Der Check selbst ist ein
reiner Host/Port-Test: HTTP(S), SFTP, FTP, WCF/net.tcp und UNC werden per TCP reiner Host/Port-Test: HTTP(S), SFTP, FTP, WCF/net.tcp, WCF-SQL/mssql und UNC
geprueft; explizite `udp://`-Ziele per UDP-Datagramm. Es werden keine werden per TCP geprüft; explizite `udp://`-Ziele per UDP-Datagramm. Es werden
HTTP-Requests, Anmeldungen oder fachlichen Nachrichten gesendet. keine HTTP-Requests, Anmeldungen oder fachlichen Nachrichten gesendet.
Auch schemafreie `host:port`-Angaben sowie FTP-/SFTP-Adressen der Form
`[benutzer@]host[:port]/pfad` werden erkannt; Benutzer und Pfad werden dabei
nicht in den Endpoint-Katalog übernommen.
Nicht jede aktive BizTalk-Adresse ist selbst ein Socket-Ziel. Dynamische Send
Ports, lokale Pfade, SMTP-Empfänger, Named Pipes, relative HTTP-/WCF-
Receive-Listener, Scheduling-Komponenten sowie nicht sicher ableitbare
DATABASE-/WCF-SAP-Adressen werden deshalb als erwartbar nicht socket-prüfbar
behandelt und erzeugen kein `UNKNOWN`. Nur eine andere aktive statische
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
naechsten erfolgreichen Providerlauf erstellt. Alle 168 Stunden wird er gegen nächsten erfolgreichen Providerlauf erstellt. Alle 168 Stunden wird er gegen
die Umgebung abgeglichen. Automatisch verwaltete Eintraege fuer inzwischen die Umgebung abgeglichen. Automatisch verwaltete Einträge für inzwischen
inaktive Artefakte verschwinden beim Abgleich; bei jedem Minutenlauf werden inaktive Artefakte verschwinden beim Abgleich; bei jedem Minutenlauf werden
sie zusaetzlich gegen den aktuellen Started-/Enabled-Zustand gefiltert. sie zusätzlich gegen den aktuellen Started-/Enabled-Zustand gefiltert.
Pro Service werden standardmaessig maximal fuenf betroffene Namen gezeigt. Pro Service werden standardmäßig maximal fünf betroffene Namen gezeigt.
Weitere Treffer erscheinen nur als `(+n more)`; Details sind zusaetzlich auf Weitere Treffer erscheinen nur als `(+n more)`; Details sind zusätzlich auf
1600 Zeichen begrenzt. Metriken bleiben trotzdem vollstaendig. 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)
@@ -125,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:
@@ -140,21 +188,21 @@ BizTalk-Server:
`root\MicrosoftBizTalkServer` `root\MicrosoftBizTalkServer`
- .NET Framework 4.7.2 - .NET Framework 4.7.2
- Checkmk Windows Agent - Checkmk Windows Agent
- administrativer Zugriff fuer die einmalige Installation - administrativer Zugriff für die einmalige Installation
- normales dediziertes AD-Servicekonto fuer den Provider - normales dediziertes AD-Servicekonto für den Provider
Das Provider-Konto benoetigt: Das Provider-Konto benötigt:
- lokales Recht zur Ausfuehrung als Scheduled Task - lokales Recht zur Ausführung als Scheduled Task
- lokalen Lese-/Ausfuehrungszugriff auf die installierte EXE - lokalen Lese-/Ausführungszugriff auf die installierte EXE
- Schreibzugriff nur auf Snapshot- und Logverzeichnis - Schreibzugriff nur auf Snapshot- und Logverzeichnis
- Mitgliedschaft in der exakt konfigurierten BizTalk Server Read Only - Mitgliedschaft in der exakt konfigurierten BizTalk Server Read Only
Users-Gruppe Users-Gruppe
Es soll weder lokaler Administrator noch SQL-`sysadmin` sein. Der Scheduled Es soll weder lokaler Administrator noch SQL-`sysadmin` sein. Der Scheduled
Task laeuft mit `RunLevel Limited`. Das Servicekonto braucht ein gespeichertes Task läuft mit `RunLevel Limited`. Das Servicekonto braucht ein gespeichertes
Task-Kennwort und das Recht `Log on as a batch job`. Ein gMSA bleibt optional, Task-Kennwort und das Recht `Log on as a batch job`. Ein gMSA bleibt optional,
ist aber fuer diese Installation nicht vorausgesetzt. ist aber für diese Installation nicht vorausgesetzt.
## Build und Tests ## Build und Tests
@@ -170,8 +218,6 @@ und das transportierbare ZIP:
```text ```text
artifacts\ artifacts\
BizTalkCheckmkPulse-Setup.zip BizTalkCheckmkPulse-Setup.zip
BizTalkCheckmkPulse-Setup.zip.b64.txt
BizTalkCheckmkPulse-Setup.zip.sha256.txt
BizTalkCheckmkPulse-Setup\ BizTalkCheckmkPulse-Setup\
Setup.exe Setup.exe
INSTALLATION.md INSTALLATION.md
@@ -182,16 +228,8 @@ artifacts\
``` ```
Das Packaging-Projekt ist Teil von `BizTalkCheckmkPulse.sln`. Ein normaler Das Packaging-Projekt ist Teil von `BizTalkCheckmkPulse.sln`. Ein normaler
`Release`-Build in Visual Studio 2019 genuegt daher; ein separates `Release`-Build in Visual Studio 2019 genügt daher; ein separates
PowerShell-/WiX-/MSI-Tooling wird nicht benoetigt. PowerShell-/WiX-/MSI-Tooling wird nicht benötigt.
Die Base64-TXT kann auf dem Windows-Zielsystem ohne Zusatzwerkzeug dekodiert
werden:
```cmd
certutil -decode BizTalkCheckmkPulse-Setup.zip.b64.txt BizTalkCheckmkPulse-Setup.zip
certutil -hashfile BizTalkCheckmkPulse-Setup.zip SHA256
```
Format-Self-Test ohne WMI, SQL oder Event Log: Format-Self-Test ohne WMI, SQL oder Event Log:
@@ -199,10 +237,12 @@ Format-Self-Test ohne WMI, SQL oder Event Log:
artifacts\BizTalkCheckmkPulse-Setup\application\BizTalkCheckmkPulse.exe --self-test artifacts\BizTalkCheckmkPulse-Setup\application\BizTalkCheckmkPulse.exe --self-test
``` ```
Erwartet werden exakt neun `OK`-Zeilen. Die Regressionstests pruefen Erwartet werden exakt neun `OK`-Zeilen. Die Regressionstests prüfen
zusaetzlich 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
Ein Mono-Build ist eine hilfreiche Quellcodepruefung, ersetzt aber nicht die Katalogaktualisierung sowie erfolgreiche und abgewiesene
Post-Install-Runtime-Abnahmen.
Ein Mono-Build ist eine hilfreiche Quellcodeprüfung, ersetzt aber nicht die
Windows-/BizTalk-Laufzeitvalidierung. Windows-/BizTalk-Laufzeitvalidierung.
## Berechtigung vorbereiten ## Berechtigung vorbereiten
@@ -218,48 +258,66 @@ Die BizTalk-Konfiguration muss die Domain-Gruppe bereits als Windows-Login und
in `BizTalkMgmtDb`, `BizTalkMsgBoxDb`, `BizTalkDTADb`, in `BizTalkMgmtDb`, `BizTalkMsgBoxDb`, `BizTalkDTADb`,
`BizTalkRuleEngineDb` sowie gegebenenfalls `BAMPrimaryImport` mit `BizTalkRuleEngineDb` sowie gegebenenfalls `BAMPrimaryImport` mit
`BTS_READONLY_USERS` abbilden. Eine fehlende Abbildung wird durch BizTalk- und `BTS_READONLY_USERS` abbilden. Eine fehlende Abbildung wird durch BizTalk- und
SQL-Administration fuer die Gruppe repariert, nicht als Einzelberechtigung SQL-Administration für die Gruppe repariert, nicht als Einzelberechtigung
fuer das Provider-Konto. für das Provider-Konto.
## Installation mit normalem Servicekonto ## Installation mit normalem Servicekonto
1. `BizTalkCheckmkPulse-Setup.zip` auf dem Zielserver vollstaendig entpacken. 1. `BizTalkCheckmkPulse-Setup.zip` auf dem Zielserver vollständig entpacken.
2. `Setup.exe` starten und die UAC-Abfrage bestaetigen. 2. `Setup.exe` starten und die UAC-Abfrage bestätigen.
3. Collector-Konto, zum Beispiel `BEW\t231bizmon`, Kennwort und Umgebung 3. Collector-Konto, zum Beispiel `BEW\t231bizmon`, Kennwort und Umgebung
eingeben. eingeben.
4. **Installieren / aktualisieren** waehlen. 4. **Installieren / aktualisieren** wählen.
Der Installer uebergibt das Kennwort direkt ueber die Windows-Task-Scheduler- Der Installer übergibt das Kennwort direkt über die Windows-Task-Scheduler-
COM-Schnittstelle. Es steht weder in einer Prozesskommandozeile noch in der COM-Schnittstelle. Es steht weder in einer Prozesskommandozeile noch in der
Konfigurationsdatei oder in Logs. Der Backslash in `DOMAIN\Benutzer` wird als Konfigurationsdatei oder in Logs. Der Backslash in `DOMAIN\Benutzer` wird als
normaler Bestandteil des Kontonamens verarbeitet. normaler Bestandteil des Kontonamens verarbeitet.
Wenn das Kennwort rotiert oder ablaeuft, muss es im Scheduled Task aktualisiert Wenn das Kennwort rotiert oder abläuft, muss es im Scheduled Task aktualisiert
werden. Dazu `Setup.exe` mit dem neuen Kennwort erneut ausfuehren. Bis dahin werden. Dazu `Setup.exe` mit dem neuen Kennwort erneut ausführen. Bis dahin
wird der Snapshot nach 180 Sekunden stale und Checkmk zeigt alle neun Services wird der Snapshot nach 180 Sekunden stale und Checkmk zeigt alle neun Services
als `UNKNOWN`. als `UNKNOWN`.
Ein gMSA kann ueber 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. prueft 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
2. baut die neue Version in einem Staging-Verzeichnis auf und uebernimmt Legacy-Version darf weniger gültige Zeilen enthalten,
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
unveraenderten 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. behaelt `%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 fuer 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` minuetlich mit 10. registriert `BizTalk Checkmk Pulse Provider` minütlich mit `IgnoreNew`,
`IgnoreNew`, fuenf Minuten Laufzeitlimit und zwei Wiederholungen, nf Minuten Laufzeitlimit und zwei Wiederholungen.
9. fuehrt 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.
@@ -276,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
@@ -291,8 +361,8 @@ Verbindlicher Test im echten `LocalSystem`-Kontext:
"C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump "C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump
``` ```
Danach in Checkmk eine Service Discovery ausfuehren, die neun Services Danach in Checkmk eine Service Discovery ausführen, die neun Services
aufnehmen und Changes aktivieren. Ein zusaetzlicher Checkmk-Async-Cache ist aufnehmen und Changes aktivieren. Ein zusätzlicher Checkmk-Async-Cache ist
nicht erforderlich: Der Consumer liest nur eine kleine lokale Datei und der nicht erforderlich: Der Consumer liest nur eine kleine lokale Datei und der
Provider besitzt bereits seinen eigenen Minutentakt. Provider besitzt bereits seinen eigenen Minutentakt.
@@ -319,13 +389,13 @@ ACL-Soll:
| Pfad | Provider | LocalSystem | Administratoren | | Pfad | Provider | LocalSystem | Administratoren |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| Programm | Lesen/Ausfuehren | Lesen/Ausfuehren | Vollzugriff | | Programm | Lesen/Ausführen | Lesen/Ausführen | Vollzugriff |
| `data` | Aendern | Lesen/Ausfuehren | Vollzugriff | | `data` | Ändern | Lesen/Ausführen | Vollzugriff |
| `logs` | Aendern | Aendern | Vollzugriff | | `logs` | Ändern | Ändern | Vollzugriff |
Der Snapshot enthaelt Monitoringzustand und kompakte Fehlerdetails, aber keine Der Snapshot enthält Monitoringzustand und kompakte Fehlerdetails, aber keine
Passwoerter oder Nachrichteninhalte. Der Consumer validiert die Datei trotzdem Passwörter oder Nachrichteninhalte. Der Consumer validiert die Datei trotzdem
vollstaendig, bevor er sie an Checkmk weitergibt. vollständig, bevor er sie an Checkmk weitergibt.
## Konfiguration ## Konfiguration
@@ -340,17 +410,19 @@ 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 Eingabegroesse fuer den Consumer. | | `SnapshotMaxBytes` | `1048576` | Harte Eingabegröße für den Consumer. |
| `LogDirectory` | `%ProgramData%\BizTalkCheckmkPulse\logs` | Tageslogs. | | `LogDirectory` | `%ProgramData%\BizTalkCheckmkPulse\logs` | Tageslogs. |
| `LogRetentionDays` | `30` | Provider bereinigt aeltere Logs. | | `LogRetentionDays` | `30` | Provider bereinigt ältere Logs. |
| `ProbeEndpointConnectivity` | `true` | Aktiviert den aggregierten TCP-/UDP-Netzwerkcheck. | | `ProbeEndpointConnectivity` | `true` | Aktiviert den aggregierten TCP-/UDP-Netzwerkcheck. |
| `EndpointCatalogPath` | `%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml` | Lokal gepflegte Endpoint-Konfiguration ohne vollstaendige URIs/Secrets. | | `EndpointCatalogPath` | `%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml` | Lokal gepflegte Endpoint-Konfiguration ohne vollständige URIs/Secrets. |
| `EndpointDiscoveryIntervalHours` | `168` | Intervall fuer den vollstaendigen Umgebungsabgleich. | | `EndpointCatalogMaxEntries` | `1000` | Harte Größenbegrenzung für Artefakte im Katalog. |
| `EndpointDiscoveryIntervalHours` | `168` | Intervall für den vollständigen Umgebungsabgleich. |
| `EndpointProbeTimeoutMilliseconds` | `3000` | Timeout je dedupliziertem Host/Port-Ziel. | | `EndpointProbeTimeoutMilliseconds` | `3000` | Timeout je dedupliziertem Host/Port-Ziel. |
| `EndpointProbeMaxConcurrency` | `16` | Begrenzte parallele Socket-Probes. | | `EndpointProbeMaxConcurrency` | `16` | Begrenzte parallele Socket-Probes. |
| `EndpointMaxCount` | `100` | Harte Obergrenze und Laufzeitbudget; bei 3 s Timeout theoretisch maximal etwa 21 s. | | `EndpointMaxCount` | `100` | Obergrenze eindeutiger Host/Port/Protokoll-Ziele und damit Laufzeitbudget; bei 3 s Timeout theoretisch maximal etwa 21 s. |
| `QueryTimeoutSeconds` | `25` | WMI-Timeout je Query. | | `QueryTimeoutSeconds` | `25` | WMI-Timeout je Query. |
| `SqlConnectionTimeoutSeconds` | `5` | SQL-Timeout je Ziel. | | `SqlConnectionTimeoutSeconds` | `5` | SQL-Timeout je Ziel. |
| `WarnResumableThreshold` | `1` | WARN ab n resumable Suspensions. | | `WarnResumableThreshold` | `1` | WARN ab n resumable Suspensions. |
@@ -359,40 +431,40 @@ Wichtige Werte:
| `AlertOnArtifactRuntimeIssues` | `true` | Unerwartet deaktivierte Receive Locations bzw. inaktive Send Ports werden CRIT. | | `AlertOnArtifactRuntimeIssues` | `true` | Unerwartet deaktivierte Receive Locations bzw. inaktive Send Ports werden CRIT. |
| `ExpectedDisabledReceiveLocations` | leer | Pipe-getrennte exakte Allowlist: `Name` oder `Anwendung\Name`. | | `ExpectedDisabledReceiveLocations` | leer | Pipe-getrennte exakte Allowlist: `Name` oder `Anwendung\Name`. |
| `ExpectedInactiveSendPorts` | leer | Pipe-getrennte exakte Allowlist: `Name` oder `Anwendung\Name`. | | `ExpectedInactiveSendPorts` | leer | Pipe-getrennte exakte Allowlist: `Name` oder `Anwendung\Name`. |
| `AlertOnInactiveOrchestrations` | `false` | Optional WARN fuer stopped/bound/unbound Orchestrations. | | `AlertOnInactiveOrchestrations` | `false` | Optional WARN für stopped/bound/unbound Orchestrations. |
| `MaxSummaryItems` | `5` | Maximal angezeigte betroffene Artefakte je Service. | | `MaxSummaryItems` | `5` | Maximal angezeigte betroffene Artefakte je Service. |
| `MaxDetailCharacters` | `1600` | Harte Obergrenze fuer Checkmk-Summary. | | `MaxDetailCharacters` | `1600` | Harte Obergrenze für Checkmk-Summary. |
| `EmitPerApplicationSuspensionServices` | `false` | Zusaetzliche Anwendungsservices. | | `EmitPerApplicationSuspensionServices` | `false` | Zusätzliche Anwendungsservices. |
| `EventLogLookbackMinutes` | `60` | Event-Log-Zeitfenster des Providers. | | `EventLogLookbackMinutes` | `60` | Event-Log-Zeitfenster des Providers. |
Nach einer Config-Aenderung den Scheduled Task manuell starten. Der Consumer Nach einer Config-Änderung den Scheduled Task manuell starten. Der Consumer
liest den naechsten atomar publizierten Snapshot. liest den nächsten atomar publizierten Snapshot.
## Fehlerbilder ## Fehlerbilder
| Beobachtung | Ursache / Massnahme | | Beobachtung | Ursache / Maßnahme |
| --- | --- | | --- | --- |
| Alle neun Services melden fehlenden Snapshot | Task, Provider-Log, Task-Konto/Kennwort und ACL pruefen. | | Alle neun Services melden fehlenden Snapshot | Task, Provider-Log, Task-Konto/Kennwort und ACL prüfen. |
| Snapshot ist `stale` | `LastTaskResult`, Laufzeit, WMI-/SQL-Timeout und Log pruefen. | | Snapshot ist `stale` | `LastTaskResult`, Laufzeit, WMI-/SQL-Timeout und Log prüfen. |
| SHA-256 oder Format ungueltig | Datei nicht manuell bearbeiten; Datentraeger/AV und Schreibpfad pruefen, Task neu starten. | | SHA-256 oder Format ungültig | Datei nicht manuell bearbeiten; Datenträger/AV und Schreibpfad prüfen, Task neu starten. |
| Provider meldet `Login failed` | Provider-Konto und exakt konfigurierte Read-Only-Gruppe sowie `BTS_READONLY_USERS` pruefen. | | 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=` pruefen; 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=` pruefen; 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 | WMI-Vollstaendigkeit, `endpoints.xml`, woechentlichen Refresh und nicht automatisch aufloesbare externe Adapteradresse pruefen. | | 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 pruefen; 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 pruefen. | | 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 pruefen. | | Task-Result `2` | Parallelstart oder Snapshot-I/O; Log und Lock/ACL prüfen. |
Ausfuehrliche Betriebs- und Architekturdetails stehen in Ausführliche Betriebs- und Architekturdetails stehen in
[Dokumentation.md](Dokumentation.md). [Dokumentation.md](Dokumentation.md).
## Deinstallation ## Deinstallation
`Setup.exe` erneut starten und **Deinstallieren** waehlen. Snapshot und Logs `Setup.exe` erneut starten und **Deinstallieren** wählen. Snapshot und Logs
bleiben dabei unter `%ProgramData%\BizTalkCheckmkPulse` erhalten. bleiben dabei unter `%ProgramData%\BizTalkCheckmkPulse` erhalten.
Die AD-Gruppenmitgliedschaft des Provider-Kontos wird bewusst nicht automatisch Die AD-Gruppenmitgliedschaft des Provider-Kontos wird bewusst nicht automatisch
geaendert und muss separat durch AD-/BizTalk-Administration entfernt werden. geändert und muss separat durch AD-/BizTalk-Administration entfernt werden.
## Quellen ## Quellen
@@ -401,6 +473,8 @@ geaendert und muss separat durch AD-/BizTalk-Administration entfernt werden.
- Microsoft: Managing BizTalk Server Security - Microsoft: Managing BizTalk Server Security
- Microsoft: [`MSBTS_SendPort` (WMI)](https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-wmi) - Microsoft: [`MSBTS_SendPort` (WMI)](https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-wmi)
- Microsoft: [`MSBTS_ReceiveLocation` (WMI)](https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-wmi) - Microsoft: [`MSBTS_ReceiveLocation` (WMI)](https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-wmi)
- Microsoft: [SQL-Adapter-Verbindungs-URI](https://learn.microsoft.com/en-us/biztalk/adapters-and-accelerators/adapter-sql/create-the-sql-server-connection-uri)
- Microsoft: [Oracle-Database-Adapter-Verbindungs-URI](https://learn.microsoft.com/en-us/biztalk/adapters-and-accelerators/adapter-oracle-database/create-the-oracle-database-connection-uri)
- Checkmk: Windows Agent und Local Checks - Checkmk: Windows Agent und Local Checks
Die genauen Links stehen in [Dokumentation.md](Dokumentation.md). Die genauen Links stehen in [Dokumentation.md](Dokumentation.md).
+97 -22
View File
@@ -1,47 +1,122 @@
# Installation auf dem BizTalk-Server # Installation auf dem BizTalk-Server
Falls nur die TXT-Datei uebertragen wurde: 1. `BizTalkCheckmkPulse-Setup.zip` vollständig in ein lokales Verzeichnis entpacken.
2. `Setup.exe` als lokaler Administrator starten und die UAC-Abfrage bestätigen.
```cmd
certutil -decode BizTalkCheckmkPulse-Setup.zip.b64.txt BizTalkCheckmkPulse-Setup.zip
certutil -hashfile BizTalkCheckmkPulse-Setup.zip SHA256
```
1. `BizTalkCheckmkPulse-Setup.zip` vollstaendig in ein lokales Verzeichnis entpacken.
2. `Setup.exe` als lokaler Administrator starten und die UAC-Abfrage bestaetigen.
3. Das Collector-Konto im Format `DOMAIN\Benutzer` eingeben, zum Beispiel 3. Das Collector-Konto im Format `DOMAIN\Benutzer` eingeben, zum Beispiel
`BEW\t231bizmon`. `BEW\t231bizmon`.
4. Kennwort und Umgebung eingeben und **Installieren / aktualisieren** waehlen. 4. Kennwort und Umgebung eingeben und **Installieren / aktualisieren** wählen.
5. Im Aufgabenplaner den Task `BizTalk Checkmk Pulse Provider` und danach den 5. Im Aufgabenplaner den Task `BizTalk Checkmk Pulse Provider` und danach den
Checkmk-Agent-Dump kontrollieren. Checkmk-Agent-Dump kontrollieren.
Bei einer bereits vorhandenen ACC-Installation ist dies ein Update. Der Bei einer bereits vorhandenen ACC-Installation ist dies ein Update. Der
Installer erkennt die bestehende Umgebung, behaelt Endpoint-Katalog, Snapshot Installer erkennt die bestehende Umgebung, behält Endpoint-Katalog, Snapshot
und Logs sowie vorhandene AppSettings bei und ergaenzt neue Config-Keys aus dem und Logs sowie vorhandene AppSettings bei und ergänzt neue Config-Keys aus dem
Paket. Das BEW-Konto und sein aktuelles Kennwort muessen erneut eingegeben Paket. Das BEW-Konto und sein aktuelles Kennwort müssen erneut eingegeben
werden, weil der Scheduled Task mit den bestaetigten Zugangsdaten neu werden, weil der Scheduled Task mit den bestätigten Zugangsdaten neu
registriert wird. registriert wird.
Die frueheren unveraenderten Endpoint-Defaults `12` parallele Probes und Die früheren unveränderten Endpoint-Defaults `12` parallele Probes und
`EndpointMaxCount=500` werden beim Update auf `16` beziehungsweise `100` `EndpointMaxCount=500` werden beim Update auf `16` beziehungsweise `100`
migriert. Abweichende, bewusst konfigurierte Werte bleiben erhalten. migriert. Abweichende, bewusst konfigurierte Werte bleiben erhalten.
`EndpointMaxCount` begrenzt nun eindeutige Socket-Ziele statt Artefakte; der
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
Bei normalen Servicekonten prueft der Installer ausserdem Kennwort und `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
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
Sekunden abgewartet, damit keine laufende Provider-EXE ueberschrieben wird. Sekunden abgewartet, damit keine laufende Provider-EXE überschrieben wird.
Bei einem Fehler nach der Umschaltung versucht der Installer, vorherige Bei einem Fehler nach der Umschaltung versucht der Installer, vorherige
Programmdateien, Wrapper und Task wiederherzustellen. Programmdateien, Wrapper und Task wiederherzustellen.
PowerShell wird fuer Installation, Update, Deinstallation und Laufzeit nicht PowerShell wird für Installation, Update, Deinstallation und Laufzeit nicht
benoetigt. Das Kennwort wird direkt an die Windows-Aufgabenplanung uebergeben benötigt. Das Kennwort wird direkt an die Windows-Aufgabenplanung übergeben
und weder in einer Datei noch in einer Prozesskommandozeile abgelegt. und weder in einer Datei noch in einer Prozesskommandozeile abgelegt.
Der Installer vergibt keine AD-, BizTalk- oder SQL-Berechtigungen. Das Konto Der Installer vergibt keine AD-, BizTalk- oder SQL-Berechtigungen. Das Konto
muss separat Mitglied der fuer die BizTalk-Gruppe konfigurierten Read-Only- muss separat Mitglied der für die BizTalk-Gruppe konfigurierten Read-Only-
Gruppe sein und lokal das Recht `Log on as a batch job` besitzen. Gruppe sein und lokal das Recht `Log on as a batch job` besitzen.
Bei einer Deinstallation bleiben Snapshot und Logs absichtlich unter Bei einer Deinstallation bleiben Snapshot und Logs absichtlich unter
`%ProgramData%\BizTalkCheckmkPulse` erhalten. Sie koennen nach der `%ProgramData%\BizTalkCheckmkPulse` erhalten. Sie können nach der
Betriebsfreigabe manuell entfernt werden. Betriebsfreigabe manuell entfernt werden.
@@ -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.
+17 -17
View File
@@ -5,34 +5,34 @@ BEFUND
====== ======
Host: AV23AGPWBIO1 Host: AV23AGPWBIO1
Checkmk-Identitaet: NT AUTHORITY\SYSTEM Checkmk-Identität: NT AUTHORITY\SYSTEM
Alte Netzwerkidentitaet: BEW\AV23AGPWBIO1$ Alte Netzwerkidentität: BEW\AV23AGPWBIO1$
Fehler: COMException HRESULT=0x80131904 Fehler: COMException HRESULT=0x80131904
Innerer Fehler: Login failed for user 'BEW\AV23AGPWBIO1$' Innerer Fehler: Login failed for user 'BEW\AV23AGPWBIO1$'
Der lokale BizTalk-WMI-Namespace war erreichbar. Datenbankgestuetzte Der lokale BizTalk-WMI-Namespace war erreichbar. Datenbankgestützte
BizTalk-WMI-Abfragen scheiterten am SQL-Login des Maschinenkontos. BizTalk-WMI-Abfragen scheiterten am SQL-Login des Maschinenkontos.
NEUE VERBINDLICHE ARCHITEKTUR NEUE VERBINDLICHE ARCHITEKTUR
============================= =============================
Das Maschinenkonto erhaelt keine BizTalk-Berechtigung. Das Maschinenkonto erhält keine BizTalk-Berechtigung.
1. Ein normales dediziertes Servicekonto laeuft minuetlich als 1. Ein normales dediziertes Servicekonto läuft minütlich als
Scheduled Task "BizTalk Checkmk Pulse Provider". Scheduled Task "BizTalk Checkmk Pulse Provider".
2. Dieses Konto fuehrt BizTalkCheckmkPulse.exe --collect aus. 2. Dieses Konto führt BizTalkCheckmkPulse.exe --collect aus.
3. Es schreibt atomar einen SHA-256-geschuetzten Snapshot nach: 3. Es schreibt atomar einen SHA-256-geschützten Snapshot nach:
%ProgramData%\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot %ProgramData%\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot
4. Der Checkmk-Agent bleibt LocalSystem und startet nur den Consumer. 4. Der Checkmk-Agent bleibt LocalSystem und startet nur den Consumer.
5. Der Consumer liest und validiert den Snapshot. Er fuehrt kein WMI und 5. Der Consumer liest und validiert den Snapshot. Er führt kein WMI und
keinen SQL-Zugriff aus. keinen SQL-Zugriff aus.
BERECHTIGUNG DES PROVIDER-KONTOS BERECHTIGUNG DES PROVIDER-KONTOS
================================ ================================
Bevorzugte Rolle fuer BizTalk Server 2020: Bevorzugte Rolle für BizTalk Server 2020:
BizTalk Server Read Only Users BizTalk Server Read Only Users
SQL-Datenbankrolle: BTS_READONLY_USERS SQL-Datenbankrolle: BTS_READONLY_USERS
@@ -42,27 +42,27 @@ BizTalk Administration Console ablesen. Das Provider-Konto durch
AD-Administration in BizTalkReadOnlyUserGroup aufnehmen. Nicht vom AD-Administration in BizTalkReadOnlyUserGroup aufnehmen. Nicht vom
Standardnamen ausgehen. Standardnamen ausgehen.
Die Operator-Gruppe ist nur eine begruendungspflichtige Eskalation, falls eine Die Operator-Gruppe ist nur eine begründungspflichtige Eskalation, falls eine
konkret benoetigte WMI-Klasse trotz bestaetigter Read-Only-Zuordnung und neuem konkret benötigte WMI-Klasse trotz bestätigter Read-Only-Zuordnung und neuem
Anmeldetoken verweigert wird. Anmeldetoken verweigert wird.
NICHT DURCHFUEHREN NICHT DURCHFÜHREN
================== ==================
- keinen SQL-Einzellogin fuer Provider- oder Maschinenkonto - keinen SQL-Einzellogin für Provider- oder Maschinenkonto
- keine ad-hoc BizTalk-Datenbankrollen - keine ad-hoc BizTalk-Datenbankrollen
- kein db_owner oder sysadmin - kein db_owner oder sysadmin
- keine pauschalen DCOM-/WMI-ACL-Erweiterungen - keine pauschalen DCOM-/WMI-ACL-Erweiterungen
- Checkmk-Agentdienst nicht auf das Provider-Konto umstellen - Checkmk-Agentdienst nicht auf das Provider-Konto umstellen
- Maschinenkonto BEW\AV23AGPWBIO1$ nicht fuer diese Loesung berechtigen - Maschinenkonto BEW\AV23AGPWBIO1$ nicht für diese Lösung berechtigen
INSTALLATION INSTALLATION
============ ============
BizTalkCheckmkPulse-Setup.zip vollstaendig entpacken und Setup.exe als BizTalkCheckmkPulse-Setup.zip vollständig entpacken und Setup.exe als
Administrator starten. Konto im Format DOMAIN\Benutzer (zum Beispiel Administrator starten. Konto im Format DOMAIN\Benutzer (zum Beispiel
BEW\t231bizmon), Kennwort und Umgebung eingeben. PowerShell wird nicht BEW\t231bizmon), Kennwort und Umgebung eingeben. PowerShell wird nicht
benoetigt. benötigt.
VERIFIKATION VERIFIKATION
============ ============
@@ -77,7 +77,7 @@ ERWARTET
======== ========
- LastTaskResult = 0 - LastTaskResult = 0
- Snapshot wird minuetlich aktualisiert - Snapshot wird minütlich aktualisiert
- Provider-Log nennt das dedizierte Konto - Provider-Log nennt das dedizierte Konto
- BizTalk Platform zeigt read_only_group=<exakte Gruppe> - BizTalk Platform zeigt read_only_group=<exakte Gruppe>
- BizTalk SQL Access zeigt targets=2 und available=2 - BizTalk SQL Access zeigt targets=2 und available=2
+59 -45
View File
@@ -8,23 +8,23 @@ Alle Services werden als Checkmk Local Checks ausgegeben. Das Format ist:
Die Ausgabe ist so gebaut, dass sie auch mit Checkmk 2.4.0p4 kompatibel bleibt: zwischen den vier Feldern steht genau ein normales Leerzeichen. Die Ausgabe ist so gebaut, dass sie auch mit Checkmk 2.4.0p4 kompatibel bleibt: zwischen den vier Feldern steht genau ein normales Leerzeichen.
Die fachlichen Daten werden minuetlich vom privilegierten Scheduled-Task- Die fachlichen Daten werden minütlich vom privilegierten Scheduled-Task-
Provider erzeugt. Der Checkmk-Agent liest als `LocalSystem` nur den Provider erzeugt. Der Checkmk-Agent liest als `LocalSystem` nur den
integritaets- und altersgeprueften Snapshot. Ist dieser Transport nicht integritäts- und altersgeprüften Snapshot. Ist dieser Transport nicht
verfuegbar, liefern alle neun stabilen Services `UNKNOWN`. verfügbar, liefern alle neun stabilen Services `UNKNOWN`.
## BizTalk Platform ## BizTalk Platform
Zweck: Zweck:
- Prueft, ob der BizTalk-WMI-Namespace erreichbar ist. - Prüft, ob der BizTalk-WMI-Namespace erreichbar ist.
- Zeigt Server, Gruppe, konfigurierte BizTalk-Read-Only-/Operator-Gruppe, Management-DB und Master-MessageBox-DB aus `MSBTS_GroupSetting`. - Zeigt Server, Gruppe, konfigurierte BizTalk-Read-Only-/Operator-Gruppe, Management-DB und Master-MessageBox-DB aus `MSBTS_GroupSetting`.
Status: Status:
| Status | Bedingung | | Status | Bedingung |
| --- | --- | | --- | --- |
| `OK` | WMI-Verbindung erfolgreich und `MSBTS_GroupSetting` liefert beide vollstaendigen Datenbankziele. | | `OK` | WMI-Verbindung erfolgreich und `MSBTS_GroupSetting` liefert beide vollständigen Datenbankziele. |
| `UNKNOWN` | Namespace, `MSBTS_GroupSetting`, ein erforderliches Datenbankziel oder der Programmstart fehlgeschlagen. | | `UNKNOWN` | Namespace, `MSBTS_GroupSetting`, ein erforderliches Datenbankziel oder der Programmstart fehlgeschlagen. |
Metriken: Metriken:
@@ -35,33 +35,33 @@ Metriken:
Zweck: Zweck:
- Ermittelt Management- und Master-MessageBox-Datenbank ueber `MSBTS_GroupSetting`. - Ermittelt Management- und Master-MessageBox-Datenbank über `MSBTS_GroupSetting`.
- Oeffnet mit der Windows-Identitaet des privilegierten Provider-Tasks eine integrierte SQL-Verbindung. - Öffnet mit der Windows-Identität des privilegierten Provider-Tasks eine integrierte SQL-Verbindung.
- Fuehrt die harmlose Abfrage `SELECT 1` aus und schliesst die Verbindung sofort wieder. - Führt die harmlose Abfrage `SELECT 1` aus und schließt die Verbindung sofort wieder.
- Zeigt lokale Ausfuehrungsidentitaet und erwartete Netzwerkidentitaet an. - Zeigt lokale Ausführungsidentität und erwartete Netzwerkidentität an.
Der Service ersetzt nicht das Checkmk-MSSQL-Plugin. Er prueft den fuer das Der Service ersetzt nicht das Checkmk-MSSQL-Plugin. Er prüft den für das
BizTalk-Monitoring relevanten Zugriff des dedizierten Provider-Kontos. BizTalk-Monitoring relevanten Zugriff des dedizierten Provider-Kontos.
Status: Status:
| Status | Bedingung | | Status | Bedingung |
| --- | --- | | --- | --- |
| `OK` | WMI-Zielermittlung vollstaendig und alle ermittelten Datenbanken erreichbar. | | `OK` | WMI-Zielermittlung vollständig und alle ermittelten Datenbanken erreichbar. |
| `UNKNOWN` | Zielermittlung unvollstaendig, Anmeldung verweigert, SQL nicht erreichbar, Timeout oder Providerfehler. | | `UNKNOWN` | Zielermittlung unvollständig, Anmeldung verweigert, SQL nicht erreichbar, Timeout oder Providerfehler. |
| `OK` mit Hinweis | Nur wenn `ProbeSqlConnectivity=false`; der Test ist dann bewusst deaktiviert. | | `OK` mit Hinweis | Nur wenn `ProbeSqlConnectivity=false`; der Test ist dann bewusst deaktiviert. |
Fehlerkategorien: Fehlerkategorien:
| Kategorie | Typische Bedeutung | | Kategorie | Typische Bedeutung |
| --- | --- | | --- | --- |
| `Sql/Permission` | Login abgelehnt, Datenbank nicht oeffenbar oder fehlende Berechtigung. | | `Sql/Permission` | Login abgelehnt, Datenbank nicht öffnbar oder fehlende Berechtigung. |
| `Sql/Connectivity` | DNS, SQL-Dienst, Instanz, TCP-Port oder Firewall nicht erreichbar. | | `Sql/Connectivity` | DNS, SQL-Dienst, Instanz, TCP-Port oder Firewall nicht erreichbar. |
| `Sql/Timeout` | Verbindungsaufbau oder Testabfrage ueberschreitet das konfigurierte Zeitlimit. | | `Sql/Timeout` | Verbindungsaufbau oder Testabfrage überschreitet das konfigurierte Zeitlimit. |
| `Sql/Provider` | Sonstiger von SQL Server oder SqlClient gemeldeter Providerfehler. | | `Sql/Provider` | Sonstiger von SQL Server oder SqlClient gemeldeter Providerfehler. |
| `Sql/Configuration` | WMI-Ziele aus einem Nicht-Berechtigungsgrund unvollstaendig oder TLS-, Zertifikats-, SPN-/SSPI-Konfiguration fehlerhaft. | | `Sql/Configuration` | WMI-Ziele aus einem Nicht-Berechtigungsgrund unvollständig oder TLS-, Zertifikats-, SPN-/SSPI-Konfiguration fehlerhaft. |
Ist die Zielermittlung wegen eines eingebetteten SQL-Loginfehlers des BizTalk-WMI-Providers unvollstaendig, wird die Folgediagnose als `Sql/Permission` statt als `Sql/Configuration` ausgegeben. Ist die Zielermittlung wegen eines eingebetteten SQL-Loginfehlers des BizTalk-WMI-Providers unvollständig, wird die Folgediagnose als `Sql/Permission` statt als `Sql/Configuration` ausgegeben.
Metriken: Metriken:
@@ -77,7 +77,7 @@ Zweck:
- Ermittelt suspendierte BizTalk-Service-Instanzen. - Ermittelt suspendierte BizTalk-Service-Instanzen.
- Unterscheidet resumable und non-resumable. - Unterscheidet resumable und non-resumable.
- Zaehlt Routing Failure Reports (`ServiceClass=64`) separat. - Zählt Routing Failure Reports (`ServiceClass=64`) separat.
- Zeigt betroffene Anwendungen und Beispiele in der Summary. - Zeigt betroffene Anwendungen und Beispiele in der Summary.
Status: Status:
@@ -88,7 +88,7 @@ Status:
| `WARN` | `biztalk_suspended_resumable >= WarnResumableThreshold`. | | `WARN` | `biztalk_suspended_resumable >= WarnResumableThreshold`. |
| `CRIT` | `biztalk_suspended_nonresumable >= CritNonResumableThreshold`. | | `CRIT` | `biztalk_suspended_nonresumable >= CritNonResumableThreshold`. |
| `CRIT` | `biztalk_routing_failure_reports >= CritRoutingFailureThreshold`. | | `CRIT` | `biztalk_routing_failure_reports >= CritRoutingFailureThreshold`. |
| `UNKNOWN` | `MSBTS_ServiceInstance` nicht verlaesslich lesbar oder Programmfehler. | | `UNKNOWN` | `MSBTS_ServiceInstance` nicht verlässlich lesbar oder Programmfehler. |
Metriken: Metriken:
@@ -103,7 +103,7 @@ Metriken:
Zweck: Zweck:
- Ueberwacht BizTalk Host Instances auf dem BizTalk-Server. - Überwacht BizTalk Host Instances auf dem BizTalk-Server.
- Filtert die gruppenweite WMI-Auflistung clientseitig auf den konfigurierten BizTalk-Server; Namen mit Sonderzeichen gelangen nicht in WQL. - Filtert die gruppenweite WMI-Auflistung clientseitig auf den konfigurierten BizTalk-Server; Namen mit Sonderzeichen gelangen nicht in WQL.
- Nicht gestartete Host Instances sind normalerweise betriebsrelevant. - Nicht gestartete Host Instances sind normalerweise betriebsrelevant.
@@ -130,7 +130,7 @@ Metriken:
Zweck: Zweck:
- Zaehlt alle konfigurierten Receive Locations und ihren `IsDisabled`-Zustand. - Zählt alle konfigurierten Receive Locations und ihren `IsDisabled`-Zustand.
- Trennt bewusst deaktivierte von unerwartet deaktivierten Namen. - Trennt bewusst deaktivierte von unerwartet deaktivierten Namen.
Status: Status:
@@ -139,7 +139,7 @@ Status:
| --- | --- | | --- | --- |
| `OK` | Daten lesbar und keine unerwartet deaktivierte Receive Location. | | `OK` | Daten lesbar und keine unerwartet deaktivierte Receive Location. |
| `CRIT` | Mindestens eine unerwartet deaktivierte Receive Location bei `AlertOnArtifactRuntimeIssues=true`. | | `CRIT` | Mindestens eine unerwartet deaktivierte Receive Location bei `AlertOnArtifactRuntimeIssues=true`. |
| `UNKNOWN` | `MSBTS_ReceiveLocation` nicht verlaesslich lesbar oder `IsDisabled` eines Objekts nicht interpretierbar. | | `UNKNOWN` | `MSBTS_ReceiveLocation` nicht verlässlich lesbar oder `IsDisabled` eines Objekts nicht interpretierbar. |
Metriken: Metriken:
@@ -160,7 +160,7 @@ Status:
| --- | --- | | --- | --- |
| `OK` | Alle nicht allowlisted Send Ports sind started. | | `OK` | Alle nicht allowlisted Send Ports sind started. |
| `CRIT` | Mindestens ein unerwartet stopped/bound Port oder unbekannter Status. | | `CRIT` | Mindestens ein unerwartet stopped/bound Port oder unbekannter Status. |
| `UNKNOWN` | `MSBTS_SendPort` nicht verlaesslich lesbar. | | `UNKNOWN` | `MSBTS_SendPort` nicht verlässlich lesbar. |
Metriken: Metriken:
@@ -178,12 +178,22 @@ Metriken:
Zweck: Zweck:
- Prueft nur Receive Locations mit `IsDisabled=false` und Send Ports mit - Prüft nur Receive Locations mit `IsDisabled=false` und Send Ports mit
`Status=Started`. `Status=Started`.
- Reduziert die Adresse auf Host/Port und fuehrt keinen fachlichen - Reduziert die Adresse auf Host/Port und führt keinen fachlichen
HTTP-, FTP-, SFTP-, WCF- oder BizTalk-Aufruf aus. HTTP-, FTP-, SFTP-, WCF- oder BizTalk-Aufruf aus.
- Dedupliziert gemeinsame Host/Port-Ziele und prueft sie mit begrenzter - Erkennt unter anderem Standard-URIs, UNC, MSMQ, `net.tcp`, WCF-SQL-
Parallelitaet; das Ergebnis wird danach wieder den Artefakten zugeordnet. `mssql`-URIs, schemafreie `host:port`-Angaben, FTP-/SFTP-Ziele mit optionalen
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
Pfade, SMTP-Empfänger, Named Pipes und relative Receive-Listener) als
erwartbar ausgenommen und nicht als Abdeckungsfehler.
- Dedupliziert gemeinsame Host/Port-Ziele und prüft sie mit begrenzter
Parallelität; das Ergebnis wird danach wieder den Artefakten zugeordnet.
- Zeigt im gesunden Zustand nur `Alle n ... erreichbar`. Nur bei Fehlern - Zeigt im gesunden Zustand nur `Alle n ... erreichbar`. Nur bei Fehlern
erscheint die begrenzte Liste `unavailable=`. erscheint die begrenzte Liste `unavailable=`.
@@ -191,29 +201,33 @@ Status:
| Status | Bedingung | | Status | Bedingung |
| --- | --- | | --- | --- |
| `OK` | Katalog und Runtimezustand vollstaendig; alle aktiven pruefbaren 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 ungueltig, Wochenabgleich scheitert, Runtimezustand ist unvollstaendig 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. |
Metriken: Metriken:
| Metrik | Bedeutung | | Metrik | Bedeutung |
| --- | --- | | --- | --- |
| `biztalk_endpoints_configured` | Eintraege in `endpoints.xml`, einschliesslich derzeit inaktiver. | | `biztalk_endpoints_configured` | Einträge in `endpoints.xml`, einschließlich derzeit inaktiver. |
| `biztalk_endpoints_active` | Im aktuellen Lauf Started/Enabled und fuer die Probe ausgewaehlt. | | `biztalk_endpoints_active` | Im aktuellen Lauf Started/Enabled und für die Probe ausgewählt. |
| `biztalk_endpoints_unique_targets` | Nach Host/Port/Protokoll-Deduplizierung tatsaechlich verbundene Ziele. | | `biztalk_endpoints_unique_targets` | Nach Host/Port/Protokoll-Deduplizierung tatsächlich verbundene Ziele. |
| `biztalk_endpoints_tested` | Vollstaendig abgeschlossene Artefaktprobes. | | `biztalk_endpoints_tested` | Vollständig abgeschlossene Artefaktprobes. |
| `biztalk_endpoints_available` | Erreichbare aktive Artefaktziele. | | `biztalk_endpoints_available` | Erreichbare aktive Artefaktziele. |
| `biztalk_endpoints_failed` | Nicht erreichbare aktive Artefaktziele; CRIT ab 1. | | `biztalk_endpoints_failed` | Nicht erreichbare aktive Artefaktziele; CRIT ab 1. |
| `biztalk_endpoints_unsupported` | Aktive externe Adressen ohne sicher bestimmbaren Host/Port. | | `biztalk_endpoints_unresolved` | Aktive statische Netzwerkadressen ohne sicher bestimmbaren Host/Port; `UNKNOWN` ab 1, sofern kein manueller Override existiert. |
| `biztalk_endpoints_inactive_skipped` | Konfigurierte Eintraege, deren BizTalk-Artefakt aktuell nicht aktiv ist. | | `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_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. |
Bei expliziten `udp://host:port`-Eintraegen kann ein generischer Check ohne Bei expliziten `udp://host:port`-Einträgen kann ein generischer Check ohne
applikationsspezifisches Protokoll nur DNS, Route und erfolgreichen lokalen applikationsspezifisches Protokoll nur DNS, Route und erfolgreichen lokalen
Datagrammversand bestaetigen. Er beweist keine Antwort des entfernten Datagrammversand bestätigen. Er beweist keine Antwort des entfernten
UDP-Dienstes. Fuer alle ueblichen streambasierten Adapter wird deshalb TCP UDP-Dienstes. Für alle üblichen streambasierten Adapter wird deshalb TCP
verwendet. verwendet.
## BizTalk Orchestrations ## BizTalk Orchestrations
@@ -222,10 +236,10 @@ Status:
| Status | Bedingung | | Status | Bedingung |
| --- | --- | | --- | --- |
| `OK` | Daten lesbar und kein unbekannter Status; inaktive Orchestrations sind standardmaessig nur Information. | | `OK` | Daten lesbar und kein unbekannter Status; inaktive Orchestrations sind standardmäßig nur Information. |
| `WARN` | stopped/bound/unbound bei `AlertOnInactiveOrchestrations=true`. | | `WARN` | stopped/bound/unbound bei `AlertOnInactiveOrchestrations=true`. |
| `CRIT` | Mindestens ein unbekannter Statuswert. | | `CRIT` | Mindestens ein unbekannter Statuswert. |
| `UNKNOWN` | `MSBTS_Orchestration` nicht verlaesslich lesbar. | | `UNKNOWN` | `MSBTS_Orchestration` nicht verlässlich lesbar. |
Metriken: Metriken:
@@ -238,21 +252,21 @@ Metriken:
| `biztalk_orchestrations_unbound` | Unbound Orchestrations. | | `biztalk_orchestrations_unbound` | Unbound Orchestrations. |
| `biztalk_orchestrations_unknown` | Nicht interpretierbarer Orchestration-Status. | | `biztalk_orchestrations_unknown` | Nicht interpretierbarer Orchestration-Status. |
Allowlist-Syntax fuer Receive Locations und Send Ports: Allowlist-Syntax für Receive Locations und Send Ports:
```text ```text
Name|Anwendung\Name|Weiterer Name Name|Anwendung\Name|Weiterer Name
``` ```
Die Auswertung ist case-insensitive und exakt. Wildcards werden bewusst nicht Die Auswertung ist case-insensitive und exakt. Wildcards werden bewusst nicht
unterstuetzt, damit eine breite Regel keine neuen Artefaktfehler verdeckt. unterstützt, damit eine breite Regel keine neuen Artefaktfehler verdeckt.
## BizTalk Event Log ## BizTalk Event Log
Zweck: Zweck:
- Erfasst BizTalk-bezogene Errors und Warnings aus dem Windows Application Log. - Erfasst BizTalk-bezogene Errors und Warnings aus dem Windows Application Log.
- Ergaenzt WMI-Zustandsdaten um aktuelle Laufzeitereignisse. - Ergänzt WMI-Zustandsdaten um aktuelle Laufzeitereignisse.
Status: Status:
@@ -272,10 +286,10 @@ Metriken:
## Optionale per-Application-Services ## Optionale per-Application-Services
Wenn `EmitPerApplicationSuspensionServices=true` gesetzt ist, werden zusaetzliche Services fuer Anwendungen erzeugt, die suspendierte Instanzen haben und deren Anwendung per WMI tatsaechlich zugeordnet werden konnte: Wenn `EmitPerApplicationSuspensionServices=true` gesetzt ist, werden zusätzliche Services für Anwendungen erzeugt, die suspendierte Instanzen haben und deren Anwendung per WMI tatsächlich zugeordnet werden konnte:
```text ```text
BizTalk Suspended <ApplicationName> BizTalk Suspended <ApplicationName>
``` ```
Nicht zuordenbare Instanzen bleiben im stabilen Aggregat-Service enthalten; ein irrefuehrender per-Application-Service namens `(unknown)` wird nicht erzeugt. Die Option kann die Service-Anzahl erhoehen und bleibt fuer den Basisausbau deaktiviert. Nicht zuordenbare Instanzen bleiben im stabilen Aggregat-Service enthalten; ein irreführender per-Application-Service namens `(unknown)` wird nicht erzeugt. Die Option kann die Service-Anzahl erhöhen und bleibt für den Basisausbau deaktiviert.
+75 -34
View File
@@ -10,59 +10,88 @@ Transportadressen. Fehlt die Datei
%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml %ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml
``` ```
wird sie im ersten erfolgreichen Lauf erzeugt. Danach erfolgt der vollstaendige wird sie im ersten erfolgreichen Lauf erzeugt. Danach erfolgt der vollständige
Abgleich alle `EndpointDiscoveryIntervalHours` (Default: 168 Stunden). Ein Abgleich alle `EndpointDiscoveryIntervalHours` (Default: 168 Stunden). Ein
Fehler beim Abgleich ersetzt keinen letzten gueltigen Katalog. Checkmk meldet Fehler beim Abgleich ersetzt keinen letzten gültigen Katalog. Checkmk meldet
den ueberfaelligen/fehlgeschlagenen Abgleich trotzdem als `UNKNOWN`. den überfälligen/fehlgeschlagenen Abgleich trotzdem als `UNKNOWN`.
Unabhaengig vom Wochenabgleich wird bei jedem Minutenlauf gegen den aktuellen 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
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
geprueft. Neue Artefakte werden spaetestens beim naechsten Wochenabgleich geprüft. Prüffähige aktuelle WMI-Adressen werden bei jedem Minutenlauf direkt
aufgenommen. verwendet; der Wochenabgleich pflegt nur den persistenten, geheimnisfreien
Katalog. Dadurch wird weder ein neues Ziel verzögert noch eine bis zu sieben
Tage alte Auto-Discovery-Adresse weiter geprüft. Manuelle Overrides gewinnen
weiterhin anhand ihres stabilen Artefaktschlüssels.
## Laufzeitbudget ## Laufzeitbudget
Die Socket-Probes werden nach Protokoll, Host und Port dedupliziert und mit Die Socket-Probes werden nach Protokoll, Host und Port dedupliziert und mit
maximal `EndpointProbeMaxConcurrency=16` parallelen Verbindungen ausgefuehrt. maximal `EndpointProbeMaxConcurrency=16` parallelen Verbindungen ausgeführt.
Bei `EndpointProbeTimeoutMilliseconds=3000` benoetigen 70 vollstaendig Bei `EndpointProbeTimeoutMilliseconds=3000` benötigen 70 vollständig
timeoutende eindeutige Ziele theoretisch etwa 15 Sekunden. Die Standardgrenze timeoutende eindeutige Ziele theoretisch etwa 15 Sekunden. Die Standardgrenze
`EndpointMaxCount=100` begrenzt diesen Anteil eines Providerlaufs auf etwa `EndpointMaxCount=100` begrenzt diesen Anteil eines Providerlaufs auf etwa
21 Sekunden. Nicht pruefbare aktive BizTalk-Artefakte zaehlen nicht gegen diese 21 Sekunden. Die Grenze gilt für deduplizierte Host/Port/Protokoll-Ziele, nicht
Grenze; sie werden separat als `unsupported` gemeldet. für die Anzahl der darauf abgebildeten BizTalk-Artefakte. Der Katalog ist
separat über `EndpointCatalogMaxEntries=1000` begrenzt. Erwartbar nicht als
Socket prüfbare Artefakte zählen nicht gegen das Laufzeitbudget; echte
Auflösungslücken werden als `unresolved` mit einem geheimnisfreien Grund
gemeldet.
Checkmk selbst wartet nicht auf diese Verbindungen, sondern liest nur den Checkmk selbst wartet nicht auf diese Verbindungen, sondern liest nur den
zuletzt atomar geschriebenen Snapshot. Die reale ACC-Laufzeit ist nach dem zuletzt atomar geschriebenen Snapshot. Die reale ACC-Laufzeit ist nach dem
Update an `biztalk_endpoint_probe_duration_ms` sowie an `endpoint_probe_ms` und Update an `biztalk_endpoint_probe_duration_ms` sowie an `endpoint_probe_ms` und
`elapsed_ms` im Provider-Log zu kontrollieren. Der Scheduled Task verhindert `elapsed_ms` im Provider-Log zu kontrollieren. Der Scheduled Task verhindert
mit `IgnoreNew` ueberlappende Providerlaeufe. mit `IgnoreNew` überlappende Providerläufe.
## Sicherheitsmodell ## Sicherheitsmodell
Der Katalog speichert nicht die vollstaendige BizTalk-Adresse. Persistiert Der Katalog speichert nicht die vollständige BizTalk-Adresse. Persistiert
werden nur: werden nur:
- Artefakttyp, Anwendung, Name und Transportrolle - Artefakttyp, Anwendung, Name und Transportrolle
- 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, Kennwoerter, Pfade und Querystrings werden nicht URI-Benutzerinformationen, Kennwörter, Pfade und Querystrings werden nicht
geschrieben. Der Katalog liegt im bereits ACL-geschuetzten `data`-Verzeichnis; geschrieben. Der Katalog liegt im bereits ACL-geschützten `data`-Verzeichnis;
nur Provider und Administratoren duerfen ihn aendern. `LocalSystem` liest nur nur Provider und Administratoren dürfen ihn ändern. `LocalSystem` liest nur
den separaten, validierten Checkmk-Snapshot. den separaten, validierten Checkmk-Snapshot.
## Manuelle Korrektur eines automatisch nicht aufloesbaren Endpoints Die automatische Auflösung akzeptiert neben absoluten URIs auch `host:port`
und bei FTP/SFTP die schemafreie Form `[benutzer@]host[:port]/pfad`. Die
Reihenfolge ist absichtlich eindeutig: `host:port` wird vor der allgemeinen
.NET-URI-Auswertung erkannt, damit der Host nicht fälschlich als URI-Schema
gedeutet wird. Benutzerinformationen und Pfad bleiben ausschließlich im
flüchtigen WMI-Kandidaten und werden nicht persistiert. Eckige URI-Klammern
um IPv6-Adressen werden vor der Socket-Probe entfernt.
Vor einer manuellen Aenderung den Scheduled Task kurz stoppen und die Datei ## Manuelle Korrektur eines automatisch nicht auflösbaren Endpoints
sichern. Einen vorhandenen Eintrag auf den gewuenschten Host/Port korrigieren
und `autoDiscovered="false"` setzen. Der vorhandene `key` bleibt unveraendert: Vor einer manuellen Änderung den Scheduled Task kurz stoppen und die Datei
sichern. Einen vorhandenen Eintrag auf den gewünschten Host/Port korrigieren
und `autoDiscovered="false"` setzen. Der vorhandene `key` bleibt unverändert:
```xml ```xml
<Endpoint <Endpoint
@@ -79,13 +108,13 @@ und `autoDiscovered="false"` setzen. Der vorhandene `key` bleibt unveraendert:
autoDiscovered="false" /> autoDiscovered="false" />
``` ```
Manuelle Eintraege werden beim Wochenabgleich erhalten. Stimmen Artefakttyp Manuelle Einträge werden beim Wochenabgleich erhalten. Stimmen Artefakttyp
und `key` mit einem Send-/Receive-Artefakt ueberein, gilt weiterhin dessen und `key` mit einem Send-/Receive-Artefakt überein, gilt weiterhin dessen
Started-/Enabled-Filter. Ein zusaetzliches, nicht an BizTalk gebundenes Ziel Started-/Enabled-Filter. Ein zusätzliches, nicht an BizTalk gebundenes Ziel
kann `artifactType="Manual"` verwenden; es wird bei `enabled="true"` immer kann `artifactType="Manual"` verwenden; es wird bei `enabled="true"` immer
geprueft. geprüft.
Nach dem Speichern Task wieder starten und pruefen: Nach dem Speichern Task wieder starten und prüfen:
```powershell ```powershell
Start-ScheduledTask -TaskName 'BizTalk Checkmk Pulse Provider' Start-ScheduledTask -TaskName 'BizTalk Checkmk Pulse Provider'
@@ -93,22 +122,34 @@ Get-Content "$env:ProgramData\BizTalkCheckmkPulse\logs\*.log" -Tail 100
& "$env:ProgramFiles\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume & "$env:ProgramFiles\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume
``` ```
Ungueltiges XML, doppelte Keys, falsche Maschine/Umgebung, unbekannte Ungültiges XML, doppelte Keys, falsche Maschine/Umgebung, unbekannte
Protokolle, ungueltige Ports und uebergrosse Dateien werden verworfen. Der Protokolle, ungültige Ports und übergroße Dateien werden verworfen. Der
Provider kann bei vollstaendig verfuegbarem BizTalk-Runtimezustand einen Provider kann bei vollständig verfügbarem BizTalk-Runtimezustand einen
ungueltigen rein automatisch erzeugten Katalog neu aufbauen. Deshalb muss ein ungültigen rein automatisch erzeugten Katalog neu aufbauen. Deshalb muss ein
manueller Eintrag vor dem produktiven Einsatz gesichert und im Provider-Log manueller Eintrag vor dem produktiven Einsatz gesichert und im Provider-Log
verifiziert werden. verifiziert werden.
## Protokollgrenzen ## Protokollgrenzen
TCP-Connect ist fuer HTTP(S), SFTP, FTP, SMB/UNC, WCF und `net.tcp` der TCP-Connect ist für HTTP(S), SFTP, FTP, SMB/UNC, WCF und `net.tcp` der
passende nebenwirkungsfreie Netzwerktest. Er bestaetigt DNS, Routing, 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.
UDP ist verbindungslos. Bei `udp://host:port` bestaetigt der Check nur DNS, Oracle-`DATABASE`-Adressen mit explizitem Anbietersegment `/Oracle` verwenden
Route und erfolgreichen lokalen Datagrammversand. Fuer einen belastbaren den angegebenen Port oder den Oracle-Standard TCP 1521. WCF-SAP-Adressen
UDP-Dienstcheck waere ein adapter-/protokollspezifisches Request/Response- 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,
Route und erfolgreichen lokalen Datagrammversand. Für einen belastbaren
UDP-Dienstcheck wäre ein adapter-/protokollspezifisches Request/Response-
Payload erforderlich; ein solches fachliches Paket wird bewusst nicht Payload erforderlich; ein solches fachliches Paket wird bewusst nicht
erfunden. erfunden.
+6 -3
View File
@@ -53,7 +53,7 @@ werden nicht doppelt zum Total addiert.
2 "BizTalk Receive Locations" ... affected=App\RL1, App\RL2, App\RL3, App\RL4, App\RL5 (+17 more) 2 "BizTalk Receive Locations" ... affected=App\RL1, App\RL2, App\RL3, App\RL4, App\RL5 (+17 more)
``` ```
Die Zaehler und Performance-Daten enthalten weiterhin alle 22 betroffenen Die Zähler und Performance-Daten enthalten weiterhin alle 22 betroffenen
Artefakte. Nur die Textliste ist begrenzt. Artefakte. Nur die Textliste ist begrenzt.
## Endpoint-Erreichbarkeit ## Endpoint-Erreichbarkeit
@@ -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_unsupported=0;;1;0|biztalk_endpoints_inactive_skipped=2;;;0|biztalk_endpoint_probe_duration_ms=842;;;0 Alle 61 aktiven, pruefbaren Send-/Receive-Endpunkte sind erreichbar (unique_targets=44, probe_ms=842); catalog_utc=2026-08-04T10:00:00.0000000Z 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, unsupported_external=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.
+49 -23
View File
@@ -1,24 +1,24 @@
# Checkmk-Integration und Betriebsuebergabe # Checkmk-Integration und Betriebsübergabe
## Verantwortungsgrenzen ## Verantwortungsgrenzen
| Team | Aufgabe | | Team | Aufgabe |
| --- | --- | | --- | --- |
| AD/Security | normales dediziertes Servicekonto; Aufnahme in exakt konfigurierte BizTalk-Read-Only-Gruppe | | AD/Security | normales dediziertes Servicekonto; Aufnahme in exakt konfigurierte BizTalk-Read-Only-Gruppe |
| BizTalk/SQL | Gruppenabbildung und `BTS_READONLY_USERS` bestaetigen; keine Einzelrechte | | BizTalk/SQL | Gruppenabbildung und `BTS_READONLY_USERS` bestätigen; keine Einzelrechte |
| Windows | Paket installieren, ACL und Scheduled Task pruefen | | Windows | Paket installieren, ACL und Scheduled Task prüfen |
| Checkmk | Wrapper verteilen beziehungsweise Installation koordinieren, Discovery und Alarmierung | | Checkmk | Wrapper verteilen beziehungsweise Installation koordinieren, Discovery und Alarmierung |
Der Checkmk-Agent bleibt `LocalSystem`. Er greift weder auf BizTalk-WMI noch Der Checkmk-Agent bleibt `LocalSystem`. Er greift weder auf BizTalk-WMI noch
auf SQL zu. Der minuetliche Scheduled Task sammelt unter dem privilegierten auf SQL zu. Der minütliche Scheduled Task sammelt unter dem privilegierten
Provider-Konto und publiziert einen validierbaren Snapshot. Provider-Konto und publiziert einen validierbaren Snapshot.
## Installation ## Installation
`BizTalkCheckmkPulse-Setup.zip` vollstaendig entpacken, `Setup.exe` als `BizTalkCheckmkPulse-Setup.zip` vollständig entpacken, `Setup.exe` als
Administrator starten und Konto, Kennwort sowie Umgebung eingeben. Konten wie Administrator starten und Konto, Kennwort sowie Umgebung eingeben. Konten wie
`BEW\t231bizmon` werden direkt im Windows-Format `DOMAIN\Benutzer` verarbeitet. `BEW\t231bizmon` werden direkt im Windows-Format `DOMAIN\Benutzer` verarbeitet.
Der Installer benoetigt keine PowerShell. Der Installer benötigt keine PowerShell.
Der Installer legt den Local Check hier ab: Der Installer legt den Local Check hier ab:
@@ -36,10 +36,10 @@ Die EXE liegt zentral hier:
Der Wrapper darf synchron laufen, da er nur einen kleinen lokalen Snapshot Der Wrapper darf synchron laufen, da er nur einen kleinen lokalen Snapshot
liest. Eine alte Async-Regel mit `cache_age: 300` aus der direkten liest. Eine alte Async-Regel mit `cache_age: 300` aus der direkten
WMI-Architektur soll entfernt werden; andernfalls addiert sie eine unnoetige WMI-Architektur soll entfernt werden; andernfalls addiert sie eine unnötige
Verzoegerung zum minuetlichen Provider. Verzögerung zum minütlichen Provider.
Aktive Agentkonfiguration pruefen: Aktive Agentkonfiguration prüfen:
```cmd ```cmd
"C:\Program Files (x86)\checkmk\service\check_mk_agent.exe" showconfig local "C:\Program Files (x86)\checkmk\service\check_mk_agent.exe" showconfig local
@@ -53,10 +53,33 @@ 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 pruefen. 2. `LastTaskResult=0`, frischen Snapshot und Log prüfen.
3. Agent-Dump im `LocalSystem`-Kontext pruefen. 3. Agent-Dump im `LocalSystem`-Kontext prüfen.
4. Service Discovery fuer den BizTalk-Host ausfuehren. 4. Service Discovery für den BizTalk-Host ausführen.
5. neun stabile Services aufnehmen und den alten 5. neun stabile Services aufnehmen und den alten
`BizTalk Runtime Artifacts`-Service entfernen. `BizTalk Runtime Artifacts`-Service entfernen.
6. Changes aktivieren. 6. Changes aktivieren.
@@ -85,7 +108,7 @@ Die komplette Installation umfasst mehr als eine Dateiablage:
- Local-Check-Wrapper. - Local-Check-Wrapper.
Der Wrapper allein kann per Agent Bakery verteilt werden, ersetzt aber nicht Der Wrapper allein kann per Agent Bakery verteilt werden, ersetzt aber nicht
die lokale Providerinstallation und die Kontofreigabe. Fuer den ersten Rollout die lokale Providerinstallation und die Kontofreigabe. Für den ersten Rollout
ist das signierte/abgenommene Deployment-Paket mit administrativer ist das signierte/abgenommene Deployment-Paket mit administrativer
Installationsautomation die klarere Variante. Installationsautomation die klarere Variante.
@@ -100,27 +123,30 @@ type "%ProgramData%\BizTalkCheckmkPulse\logs\biztalk-checkmk-pulse-*.log"
Soll: Soll:
- Task laeuft jede Minute und endet mit `0`. - Task läuft jede Minute und endet mit `0`.
- Snapshot ist kleiner als `SnapshotMaxBytes` und juenger als 180 Sekunden. - Snapshot ist kleiner als `SnapshotMaxBytes` und jünger als 180 Sekunden.
- Log nennt das dedizierte Providerkonto. - Log nennt das dedizierte Providerkonto.
- Platform zeigt `read_only_group=...`. - Platform zeigt `read_only_group=...`.
- SQL Access zeigt `targets=2`, `available=2`. - SQL Access zeigt `targets=2`, `available=2`.
- Suspended Instances zeigt total/resumable/non-resumable/routing failures. - Suspended Instances zeigt total/resumable/non-resumable/routing failures.
- 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
ausschliesslich nicht erreichbare 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
Ein Ausfall des Providers wird ueber alle neun Services als `UNKNOWN` Ein Ausfall des Providers wird über alle neun Services als `UNKNOWN`
sichtbar. Die Summary nennt fehlenden, unlesbaren, ungueltigen oder stale sichtbar. Die Summary nennt fehlenden, unlesbaren, ungültigen oder stale
Snapshot. Als Betriebsregel sollte `UNKNOWN` dieser Services genauso Snapshot. Als Betriebsregel sollte `UNKNOWN` dieser Services genauso
eskaliert werden wie ein technischer Monitoringausfall. eskaliert werden wie ein technischer Monitoringausfall.
Optional kann Windows Task Scheduler zusaetzlich durch vorhandene Checkmk Optional kann Windows Task Scheduler zusätzlich durch vorhandene Checkmk
Task-/Event-Log-Regeln ueberwacht werden. Das ist eine Ergaenzung, kein Ersatz Task-/Event-Log-Regeln überwacht werden. Das ist eine Ergänzung, kein Ersatz
fuer die eingebaute Stale-Pruefung. für die eingebaute Stale-Prüfung.
## Rollout ## Rollout
@@ -128,7 +154,7 @@ Empfohlene Reihenfolge:
1. ACC: Berechtigung, Task, Snapshot, Stale-Test und Discovery abnehmen. 1. ACC: Berechtigung, Task, Snapshot, Stale-Test und Discovery abnehmen.
2. DEV/TST: gleiche Automatisierung und umgebungsspezifische Config. 2. DEV/TST: gleiche Automatisierung und umgebungsspezifische Config.
3. PRD: Change, Wartungsfenster, Healthcheck und fachliche Plausibilitaet. 3. PRD: Change, Wartungsfenster, Healthcheck und fachliche Plausibilität.
Ausfuehrliche Architektur, ACL, Fehlerbilder und Abnahmekriterien: Ausführliche Architektur, ACL, Fehlerbilder und Abnahmekriterien:
[Dokumentation.md](../Dokumentation.md). [Dokumentation.md](../Dokumentation.md).
@@ -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

+103
View File
@@ -0,0 +1,103 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="720" viewBox="0 0 1200 720" role="img" aria-labelledby="title description">
<title id="title">BizTalk Checkmk Pulse Architektur</title>
<desc id="description">Ein privilegierter Scheduled Task liest BizTalk-Daten und prüft aktive Endpunkte. Er schreibt einen validierten Snapshot, den der Checkmk-Agent als LocalSystem nur konsumiert.</desc>
<defs>
<marker id="arrow-blue" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#2563eb"/>
</marker>
<marker id="arrow-green" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#059669"/>
</marker>
<filter id="shadow" x="-10%" y="-10%" width="120%" height="130%">
<feDropShadow dx="0" dy="3" stdDeviation="4" flood-color="#0f172a" flood-opacity="0.12"/>
</filter>
<style>
.title { font: 700 28px Arial, sans-serif; fill: #0f172a; }
.lane-title { font: 700 17px Arial, sans-serif; fill: #334155; }
.box-title { font: 700 17px Arial, sans-serif; fill: #0f172a; }
.box-text { font: 14px Arial, sans-serif; fill: #334155; }
.small { font: 13px Arial, sans-serif; fill: #475569; }
.label { font: 700 13px Arial, sans-serif; fill: #1e40af; }
.provider-box { fill: #ffffff; stroke: #2563eb; stroke-width: 2; }
.endpoint-box { fill: #ecfdf5; stroke: #059669; stroke-width: 2; }
.snapshot-box { fill: #fff7ed; stroke: #ea580c; stroke-width: 2; }
.consumer-box { fill: #ffffff; stroke: #64748b; stroke-width: 2; }
.blue-arrow { fill: none; stroke: #2563eb; stroke-width: 3; marker-end: url(#arrow-blue); }
.green-arrow { fill: none; stroke: #059669; stroke-width: 3; marker-end: url(#arrow-green); }
</style>
</defs>
<rect width="1200" height="720" rx="18" fill="#f8fafc"/>
<text x="60" y="52" class="title">BizTalk Checkmk Pulse</text>
<text x="60" y="78" class="small">Privilegierte Datenerfassung und unprivilegierter Checkmk-Consumer</text>
<rect x="35" y="105" width="1130" height="355" rx="18" fill="#eff6ff" stroke="#bfdbfe" stroke-width="2"/>
<text x="60" y="138" class="lane-title">Provider-Zone: dediziertes AD-Servicekonto</text>
<g filter="url(#shadow)">
<rect x="70" y="175" width="245" height="105" rx="14" class="provider-box"/>
<text x="192" y="207" text-anchor="middle" class="box-title">Scheduled Task</text>
<text x="192" y="232" text-anchor="middle" class="box-text">jede Minute, RunLevel Limited</text>
<text x="192" y="254" text-anchor="middle" class="box-text">IgnoreNew verhindert Überlappung</text>
</g>
<g filter="url(#shadow)">
<rect x="395" y="165" width="310" height="125" rx="14" class="provider-box"/>
<text x="550" y="198" text-anchor="middle" class="box-title">BizTalkCheckmkPulse --collect</text>
<text x="550" y="225" text-anchor="middle" class="box-text">BizTalk-WMI + BizTalk-SQL</text>
<text x="550" y="247" text-anchor="middle" class="box-text">Application Event Log</text>
<text x="550" y="269" text-anchor="middle" class="box-text">aktive Send-/Receive-Artefakte</text>
</g>
<g filter="url(#shadow)">
<rect x="795" y="165" width="330" height="125" rx="14" class="endpoint-box"/>
<text x="960" y="198" text-anchor="middle" class="box-title">Externe BizTalk-Endpunkte</text>
<text x="960" y="225" text-anchor="middle" class="box-text">TCP: HTTP(S), SFTP/FTP, WCF/SQL, SMB</text>
<text x="960" y="247" text-anchor="middle" class="box-text">UDP: explizite udp://-Ziele</text>
<text x="960" y="269" text-anchor="middle" class="box-text">nur Started / Enabled</text>
</g>
<path d="M315 227 H385" class="blue-arrow"/>
<path d="M705 227 H785" class="green-arrow"/>
<text x="745" y="211" text-anchor="middle" class="label" fill="#047857">TCP / UDP</text>
<g filter="url(#shadow)">
<rect x="395" y="335" width="310" height="90" rx="14" class="snapshot-box"/>
<text x="550" y="367" text-anchor="middle" class="box-title">Validierter lokaler Snapshot</text>
<text x="550" y="392" text-anchor="middle" class="box-text">atomar + versioniert + SHA-256</text>
<text x="550" y="413" text-anchor="middle" class="small">Endpoint-Katalog und kompakte Checkmk-Daten</text>
</g>
<path d="M550 290 V325" class="blue-arrow"/>
<text x="565" y="316" class="label">schreiben</text>
<line x1="35" y1="485" x2="1165" y2="485" stroke="#94a3b8" stroke-width="2" stroke-dasharray="9 7"/>
<rect x="470" y="470" width="260" height="30" rx="15" fill="#f8fafc" stroke="#94a3b8"/>
<text x="600" y="490" text-anchor="middle" class="small">Berechtigungsgrenze</text>
<rect x="35" y="510" width="1130" height="175" rx="18" fill="#f1f5f9" stroke="#cbd5e1" stroke-width="2"/>
<text x="60" y="543" class="lane-title">Checkmk-Zone: LocalSystem ohne BizTalk-/SQL-Rechte</text>
<g filter="url(#shadow)">
<rect x="75" y="575" width="245" height="75" rx="14" class="consumer-box"/>
<text x="197" y="607" text-anchor="middle" class="box-title">Checkmk Windows Agent</text>
<text x="197" y="632" text-anchor="middle" class="box-text">LocalSystem</text>
</g>
<g filter="url(#shadow)">
<rect x="430" y="565" width="300" height="95" rx="14" class="consumer-box"/>
<text x="580" y="597" text-anchor="middle" class="box-title">--consume</text>
<text x="580" y="622" text-anchor="middle" class="box-text">liest nur den Snapshot</text>
<text x="580" y="643" text-anchor="middle" class="box-text">kein WMI, SQL oder Netzwerkzugriff</text>
</g>
<g filter="url(#shadow)">
<rect x="840" y="575" width="285" height="75" rx="14" class="consumer-box"/>
<text x="982" y="607" text-anchor="middle" class="box-title">9 kompakte Local Checks</text>
<text x="982" y="632" text-anchor="middle" class="box-text">inkl. Endpoint Reachability</text>
</g>
<path d="M320 612 H420" class="blue-arrow"/>
<path d="M730 612 H830" class="blue-arrow"/>
<path d="M705 380 H765 V545 H580 V555" class="blue-arrow"/>
<text x="775" y="463" class="label">nur lesen</text>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

+587
View File
@@ -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()
# 12: 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()
# 67: 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>
+352 -46
View File
@@ -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 + ".");
} }
} }
} }
+15 -7
View File
@@ -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; }
}
} }
+4
View File
@@ -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" />
@@ -18,6 +20,8 @@
<add key="ProbeEndpointConnectivity" value="true" /> <add key="ProbeEndpointConnectivity" value="true" />
<add key="EndpointCatalogPath" value="%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml" /> <add key="EndpointCatalogPath" value="%ProgramData%\BizTalkCheckmkPulse\data\endpoints.xml" />
<add key="EndpointCatalogMaxBytes" value="1048576" /> <add key="EndpointCatalogMaxBytes" value="1048576" />
<!-- Artefaktgrenze; das Netzwerkbudget wird separat ueber eindeutige Ziele begrenzt. -->
<add key="EndpointCatalogMaxEntries" value="1000" />
<!-- 168 Stunden = woechentlicher Abgleich mit der BizTalk-Umgebung. --> <!-- 168 Stunden = woechentlicher Abgleich mit der BizTalk-Umgebung. -->
<add key="EndpointDiscoveryIntervalHours" value="168" /> <add key="EndpointDiscoveryIntervalHours" value="168" />
<add key="EndpointProbeTimeoutMilliseconds" value="3000" /> <add key="EndpointProbeTimeoutMilliseconds" value="3000" />
@@ -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,10 +380,15 @@ 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.UnsupportedActive > 0 || endpointState.UnresolvedActive > 0
|| endpointState.RefreshRequired && !endpointState.RefreshSucceeded || endpointState.RefreshRequired && !endpointState.RefreshSucceeded
|| !string.IsNullOrWhiteSpace(endpointState.Failure); || !string.IsNullOrWhiteSpace(endpointState.Failure);
var state = failed.Length > 0 var state = failed.Length > 0
@@ -391,21 +398,31 @@ 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_unsupported={6};;1;0|biztalk_endpoints_inactive_skipped={7};;;0|biztalk_endpoint_probe_duration_ms={8};;;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,
endpointState.Results.Count, endpointState.Results.Count,
available, available,
failed.Length, failed.Length,
endpointState.UnsupportedActive, endpointState.UnresolvedActive,
endpointState.ExpectedNonProbeableActive,
endpointState.BestEffortActive,
ignoredBestEffort.Length,
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(", best_effort=").Append(endpointState.BestEffortActive)
.Append(", best_effort_ignored=").Append(ignoredBestEffort.Length)
.Append(", manual_overrides=").Append(endpointState.ManualOverridesActive)
.Append(", probe_ms=").Append(endpointState.ProbeDurationMilliseconds).Append(")"); .Append(", probe_ms=").Append(endpointState.ProbeDurationMilliseconds).Append(")");
} }
else else
@@ -414,7 +431,11 @@ namespace BizTalkCheckmkPulse
.Append(", tested=").Append(endpointState.Results.Count) .Append(", tested=").Append(endpointState.Results.Count)
.Append(", available=").Append(available) .Append(", available=").Append(available)
.Append(", failed=").Append(failed.Length) .Append(", failed=").Append(failed.Length)
.Append(", unsupported_external=").Append(endpointState.UnsupportedActive) .Append(", unresolved=").Append(endpointState.UnresolvedActive)
.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(", unique_targets=").Append(endpointState.UniqueTargets) .Append(", unique_targets=").Append(endpointState.UniqueTargets)
.Append(", probe_ms=").Append(endpointState.ProbeDurationMilliseconds); .Append(", probe_ms=").Append(endpointState.ProbeDurationMilliseconds);
AppendLimitedList(detail, "unavailable", failed.Select(x => AppendLimitedList(detail, "unavailable", failed.Select(x =>
@@ -423,6 +444,8 @@ namespace BizTalkCheckmkPulse
{ {
detail.Append("; catalog_or_probe_error=").Append(endpointState.Failure); detail.Append("; catalog_or_probe_error=").Append(endpointState.Failure);
} }
AppendLimitedList(detail, "unresolved_endpoints", endpointState.ResolutionIssues);
} }
if (endpointState.CatalogSynchronizedUtc.HasValue) if (endpointState.CatalogSynchronizedUtc.HasValue)
+549 -64
View File
@@ -13,41 +13,138 @@ 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,
out string reason) out string reason)
{ {
entry = null; var resolution = Analyze(candidate);
reason = string.Empty; entry = resolution.Entry;
reason = resolution.Reason ?? string.Empty;
return resolution.Status == EndpointResolutionStatus.Probeable;
}
/// <summary>
/// Trennt sicher pruefbare Ziele von erwartbar lokalen/adapterinternen Adressen und echten Aufloesungsluecken.
/// </summary>
/// <param name="candidate">Zu klassifizierender BizTalk-Transport.</param>
/// <returns>Klassifizierung sowie gegebenenfalls das pruefbare Socket-Ziel.</returns>
public static EndpointResolution Analyze(EndpointCandidate candidate)
{
if (candidate == null) if (candidate == null)
{ {
reason = "Endpoint-Kandidat fehlt."; return Unresolved("Endpoint-Kandidat fehlt.");
return false;
} }
if (candidate.Dynamic) if (candidate.Dynamic)
{ {
reason = "Dynamischer Send Port besitzt kein statisch pruefbares Ziel."; return Expected("Dynamischer Send Port besitzt kein statisch pruefbares Ziel.");
return false; }
// 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)
{ {
reason = "Transportadresse ist leer."; return Unresolved("Aktive statische Transportadresse ist leer.");
return false;
} }
if (Contains(candidate.AdapterName, "SMTP"))
{
return Expected("SMTP-Empfaenger beziehungsweise Adapteradresse enthaelt nicht den SMTP-Server.");
}
// SFTP-/FTP-Adressen duerfen Benutzerinformationen enthalten (user@host).
// Deshalb erst nach der adapterspezifischen Aufloesung als Empfaengerliste einstufen.
if (LooksLikeEmailRecipients(address)
&& !Contains(candidate.AdapterName, "SFTP")
&& !Contains(candidate.AdapterName, "FTP"))
{
return Expected("SMTP-Empfaenger beziehungsweise Adapteradresse enthaelt nicht den SMTP-Server.");
}
if (Regex.IsMatch(address, @"^[a-zA-Z]:[\\/]", RegexOptions.CultureInvariant)
|| Contains(candidate.AdapterName, "FILE") && !address.StartsWith("\\\\", StringComparison.Ordinal))
{
return Expected("Lokaler Dateipfad benoetigt keine Netzwerkprobe.");
}
if (address.StartsWith("net.pipe:", StringComparison.OrdinalIgnoreCase)
|| address.StartsWith("npipe:", StringComparison.OrdinalIgnoreCase))
{
return Expected("Named Pipes besitzen kein TCP-/UDP-Ziel.");
}
if (address.StartsWith("/", StringComparison.Ordinal)
&& IsInboundListener(candidate))
{
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;
string protocol; string protocol;
string host; string host;
int port; int port;
if (!TryResolve(address, candidate.AdapterName, out protocol, out host, out port, out reason)) string reason;
if (!TryResolve(
address,
candidate.AdapterName,
IsInboundListener(candidate),
out protocol,
out host,
out port,
out reason))
{ {
return false; if (address.StartsWith("file:", StringComparison.OrdinalIgnoreCase)
|| address.StartsWith("mailto:", StringComparison.OrdinalIgnoreCase))
{
return Expected(reason);
}
return Unresolved(reason);
} }
entry = new EndpointCatalogEntry entry = new EndpointCatalogEntry
@@ -62,62 +159,318 @@ namespace BizTalkCheckmkPulse
Host = host, Host = host,
Port = port, Port = port,
Enabled = true, Enabled = true,
AutoDiscovered = true AutoDiscovered = true,
BestEffort = false
}; };
return true; return Probeable(entry);
} }
/// <summary> /// <summary>
/// Unterscheidet externe, aber nicht automatisch aufloesbare Ziele von bewusst lokalen/variablen Adressen. /// Unterscheidet externe, aber nicht automatisch aufloesbare Ziele von bewusst lokalen/variablen Adressen.
/// </summary> /// </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) public static bool IsPotentialExternalEndpoint(EndpointCandidate candidate)
{ {
if (candidate == null || candidate.Dynamic) 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; return false;
} }
var address = (candidate.Address ?? string.Empty).Trim();
if (address.Length == 0
|| LooksLikeEmailRecipients(address)
|| Regex.IsMatch(address, @"^[a-zA-Z]:[\\/]", RegexOptions.CultureInvariant)
|| address.StartsWith("/", StringComparison.Ordinal)
|| address.StartsWith("net.pipe:", StringComparison.OrdinalIgnoreCase)
|| address.StartsWith("npipe:", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (Contains(candidate.AdapterName, "SMTP")
|| Contains(candidate.AdapterName, "FILE") && !address.StartsWith("\\\\", StringComparison.Ordinal))
{
return false;
}
if (address.StartsWith("\\\\", StringComparison.Ordinal)
|| HostPortPattern.IsMatch(address)
|| Regex.IsMatch(address, @"(?:FORMATNAME:)?DIRECT=(?:OS|TCP):", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
{
return true;
}
Uri uri; Uri uri;
if (Uri.TryCreate(address, UriKind.Absolute, out uri)) if (!Uri.TryCreate(address, UriKind.Absolute, out uri)
|| string.IsNullOrWhiteSpace(uri.Host))
{ {
return !string.Equals(uri.Scheme, "file", StringComparison.OrdinalIgnoreCase) return false;
&& !string.Equals(uri.Scheme, "mailto", StringComparison.OrdinalIgnoreCase);
} }
return Contains(candidate.AdapterName, "SFTP") var oracle = Regex.IsMatch(
|| Contains(candidate.AdapterName, "FTP") address,
|| Contains(candidate.AdapterName, "HTTP") @"/Oracle(?:[/;?]|$)",
|| Contains(candidate.AdapterName, "SOAP") RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|| Contains(candidate.AdapterName, "WCF"); 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("&amp;", "&");
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
{
Status = EndpointResolutionStatus.Probeable,
Entry = entry,
Reason = string.Empty
};
} }
private static bool TryResolve( private static bool TryResolve(
string address, string address,
string adapterName, string adapterName,
bool allowWildcardListener,
out string protocol, out string protocol,
out string host, out string host,
out int port, out int port,
@@ -128,7 +481,15 @@ namespace BizTalkCheckmkPulse
port = 0; port = 0;
reason = string.Empty; reason = string.Empty;
if (LooksLikeEmailRecipients(address)) if (allowWildcardListener
&& TryResolveWildcardListener(address, out protocol, out host, out port, out reason))
{
return true;
}
if (LooksLikeEmailRecipients(address)
&& !Contains(adapterName, "SFTP")
&& !Contains(adapterName, "FTP"))
{ {
reason = "SMTP-Empfaengerliste ist kein pruefbarer Netzwerk-Endpunkt."; reason = "SMTP-Empfaengerliste ist kein pruefbarer Netzwerk-Endpunkt.";
return false; return false;
@@ -155,6 +516,28 @@ namespace BizTalkCheckmkPulse
return Validate(host, port, out reason); return Validate(host, port, out reason);
} }
// Host:Port muss vor Uri.TryCreate ausgewertet werden: .NET interpretiert
// beispielsweise "server:2222/path" sonst als URI mit dem Schema "server".
var match = HostPortPattern.Match(address);
if (match.Success)
{
host = match.Groups["host"].Value.Trim('[', ']');
if (!int.TryParse(match.Groups["port"].Value, out port))
{
port = 0;
}
return Validate(host, port, out reason);
}
// BizTalk-FTP-/SFTP-Konfigurationen kommen auch ohne Schema vor. Durch
// die temporaere URI-Normalisierung werden user@host, IPv6 und Ports
// sauber getrennt; Benutzername und Pfad gelangen nicht in den Katalog.
if (TryResolveAdapterUri(address, adapterName, out host, out port))
{
return Validate(host, port, out reason);
}
Uri uri; Uri uri;
if (Uri.TryCreate(address, UriKind.Absolute, out uri) if (Uri.TryCreate(address, UriKind.Absolute, out uri)
&& !string.IsNullOrWhiteSpace(uri.Scheme)) && !string.IsNullOrWhiteSpace(uri.Scheme))
@@ -168,7 +551,7 @@ namespace BizTalkCheckmkPulse
return false; return false;
} }
host = uri.Host; host = NormalizeHost(uri.Host);
port = 445; port = 445;
return Validate(host, port, out reason); return Validate(host, port, out reason);
} }
@@ -179,7 +562,7 @@ namespace BizTalkCheckmkPulse
return false; return false;
} }
host = uri.Host; host = NormalizeHost(uri.Host);
protocol = scheme == "udp" ? "UDP" : "TCP"; protocol = scheme == "udp" ? "UDP" : "TCP";
port = ResolvePort(uri, scheme); port = ResolvePort(uri, scheme);
if (port <= 0 && HasExplicitPort(address)) if (port <= 0 && HasExplicitPort(address))
@@ -196,28 +579,16 @@ namespace BizTalkCheckmkPulse
return Validate(host, port, out reason); return Validate(host, port, out reason);
} }
var match = HostPortPattern.Match(address);
if (match.Success)
{
host = match.Groups["host"].Value.Trim('[', ']');
if (!int.TryParse(match.Groups["port"].Value, out port))
{
port = 0;
}
return Validate(host, port, out reason);
}
if (Contains(adapterName, "SFTP")) if (Contains(adapterName, "SFTP"))
{ {
host = address.Trim('/'); host = ExtractBareAdapterHost(address);
port = 22; port = 22;
return Validate(host, port, out reason); return Validate(host, port, out reason);
} }
if (Contains(adapterName, "FTP")) if (Contains(adapterName, "FTP"))
{ {
host = address.Trim('/'); host = ExtractBareAdapterHost(address);
port = 21; port = 21;
return Validate(host, port, out reason); return Validate(host, port, out reason);
} }
@@ -248,10 +619,118 @@ namespace BizTalkCheckmkPulse
case "imap": return 143; case "imap": return 143;
case "imaps": return 993; case "imaps": return 993;
case "msmq": return 1801; case "msmq": return 1801;
case "net.tcp": return 808;
case "mssql": return 1433;
// Ohne Pfad ist der Hostteil ein tnsnames.ora-Alias und kein sicher pruefbarer DNS-Host.
case "oracledb": return uri.AbsolutePath.Trim('/').Length > 0 ? 1521 : 0;
default: return uri.Port > 0 ? uri.Port : 0; default: return uri.Port > 0 ? uri.Port : 0;
} }
} }
/// <summary>
/// Reduziert lokale HTTP-/WCF-Wildcard-Listener auf einen nebenwirkungsfreien Loopback-Porttest.
/// </summary>
private static bool TryResolveWildcardListener(
string address,
out string protocol,
out string host,
out int port,
out string reason)
{
protocol = "TCP";
host = string.Empty;
port = 0;
reason = string.Empty;
var match = Regex.Match(
address,
@"^(?<scheme>https?|net\.tcp)://(?:\+|\*)(?::(?<port>\d{1,5}))?(?:[/\\].*)?$",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
if (!match.Success)
{
return false;
}
var scheme = match.Groups["scheme"].Value.ToLowerInvariant();
if (!string.IsNullOrWhiteSpace(match.Groups["port"].Value)
&& !int.TryParse(match.Groups["port"].Value, out port))
{
port = 0;
}
if (port <= 0)
{
port = scheme == "https" ? 443 : scheme == "net.tcp" ? 808 : 80;
}
host = "127.0.0.1";
return Validate(host, port, out reason);
}
private static string ExtractBareAdapterHost(string address)
{
var value = (address ?? string.Empty).Trim().Trim('/');
var separator = value.IndexOfAny(new[] { '/', '\\' });
return separator < 0 ? value : value.Substring(0, separator);
}
private static bool TryResolveAdapterUri(string address, string adapterName, out string host, out int port)
{
host = string.Empty;
port = 0;
var scheme = Contains(adapterName, "SFTP")
? "sftp"
: Contains(adapterName, "FTP") ? "ftp" : string.Empty;
if (scheme.Length == 0 || address.IndexOf("://", StringComparison.Ordinal) >= 0)
{
return false;
}
Uri uri;
var normalized = scheme + "://" + address.TrimStart('/');
if (!Uri.TryCreate(normalized, UriKind.Absolute, out uri)
|| string.IsNullOrWhiteSpace(uri.Host))
{
return false;
}
host = NormalizeHost(uri.Host);
port = uri.IsDefaultPort || uri.Port <= 0
? scheme == "sftp" ? 22 : 21
: uri.Port;
return true;
}
/// <summary>
/// Erkennt Adapter, deren relative Receive-Adresse eine lokale Listenerbindung statt eines Remoteziels beschreibt.
/// </summary>
private static bool IsInboundListener(EndpointCandidate candidate)
{
return string.Equals(candidate.ArtifactType, "ReceiveLocation", StringComparison.OrdinalIgnoreCase)
&& (Contains(candidate.AdapterName, "HTTP")
|| Contains(candidate.AdapterName, "SOAP")
|| Contains(candidate.AdapterName, "WCF"));
}
private static EndpointResolution Expected(string reason)
{
return new EndpointResolution
{
Status = EndpointResolutionStatus.ExpectedNonProbeable,
Reason = reason
};
}
private static EndpointResolution Unresolved(string reason)
{
return new EndpointResolution
{
Status = EndpointResolutionStatus.Unresolved,
Reason = string.IsNullOrWhiteSpace(reason)
? "Transportadresse konnte nicht sicher auf Host und Port reduziert werden."
: reason
};
}
private static bool HasExplicitPort(string address) private static bool HasExplicitPort(string address)
{ {
var authorityEnd = address.IndexOfAny(new[] { '/', '?' }, address.IndexOf("://", StringComparison.Ordinal) + 3); var authorityEnd = address.IndexOfAny(new[] { '/', '?' }, address.IndexOf("://", StringComparison.Ordinal) + 3);
@@ -288,10 +767,16 @@ namespace BizTalkCheckmkPulse
&& value.IndexOf(fragment, StringComparison.OrdinalIgnoreCase) >= 0; && value.IndexOf(fragment, StringComparison.OrdinalIgnoreCase) >= 0;
} }
private static string NormalizeHost(string host)
{
return (host ?? string.Empty).Trim().Trim('[', ']');
}
private static bool Validate(string host, int port, out string reason) private static bool Validate(string host, int port, out string reason)
{ {
host = (host ?? string.Empty).Trim(); host = NormalizeHost(host);
if (host.Length == 0 || host.IndexOfAny(new[] { ' ', '\t', '\r', '\n', '*', '%' }) >= 0) if (host.Length == 0
|| host.IndexOfAny(new[] { ' ', '\t', '\r', '\n', '*', '%', '/', '\\', '@', ';', '?', '#' }) >= 0)
{ {
reason = "Host ist leer oder enthaelt nicht aufgeloeste Platzhalter."; reason = "Host ist leer oder enthaelt nicht aufgeloeste Platzhalter.";
return false; return false;
@@ -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);
@@ -90,14 +101,14 @@ namespace BizTalkCheckmkPulse
EnvironmentName = catalogEnvironment, EnvironmentName = catalogEnvironment,
SynchronizedUtc = synchronizedUtc, SynchronizedUtc = synchronizedUtc,
ActiveCandidates = ReadInt(root, "activeCandidates", 0, 100000), ActiveCandidates = ReadInt(root, "activeCandidates", 0, 100000),
UnsupportedCandidates = ReadInt(root, "unsupportedCandidates", 0, 100000) UnresolvedCandidates = ReadInt(root, "unsupportedCandidates", 0, 100000)
}; };
foreach (var node in root.Elements("Endpoint")) foreach (var node in root.Elements("Endpoint"))
{ {
if (catalog.Entries.Count >= _maxEntries) if (catalog.Entries.Count >= _maxEntries)
{ {
throw new InvalidDataException("Endpoint catalog exceeds EndpointMaxCount."); throw new InvalidDataException("Endpoint catalog exceeds the configured entry limit.");
} }
var entry = new EndpointCatalogEntry var entry = new EndpointCatalogEntry
@@ -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)
@@ -135,7 +152,7 @@ namespace BizTalkCheckmkPulse
if (catalog.Entries.Count > _maxEntries) if (catalog.Entries.Count > _maxEntries)
{ {
throw new InvalidDataException("Endpoint catalog exceeds EndpointMaxCount."); throw new InvalidDataException("Endpoint catalog exceeds the configured entry limit.");
} }
foreach (var entry in catalog.Entries) foreach (var entry in catalog.Entries)
@@ -150,7 +167,8 @@ namespace BizTalkCheckmkPulse
new XAttribute("environment", catalog.EnvironmentName ?? string.Empty), new XAttribute("environment", catalog.EnvironmentName ?? string.Empty),
new XAttribute("synchronizedUtc", catalog.SynchronizedUtc.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture)), new XAttribute("synchronizedUtc", catalog.SynchronizedUtc.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture)),
new XAttribute("activeCandidates", catalog.ActiveCandidates), new XAttribute("activeCandidates", catalog.ActiveCandidates),
new XAttribute("unsupportedCandidates", catalog.UnsupportedCandidates)); // Der XML-Attributsname bleibt fuer vorhandene Katalogdateien der Version 1 stabil.
new XAttribute("unsupportedCandidates", catalog.UnresolvedCandidates));
foreach (var entry in catalog.Entries foreach (var entry in catalog.Entries
.OrderBy(x => x.ApplicationName, StringComparer.OrdinalIgnoreCase) .OrderBy(x => x.ApplicationName, StringComparer.OrdinalIgnoreCase)
.ThenBy(x => x.ArtifactType, StringComparer.OrdinalIgnoreCase) .ThenBy(x => x.ArtifactType, StringComparer.OrdinalIgnoreCase)
@@ -169,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);
@@ -283,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;
@@ -44,7 +53,7 @@ namespace BizTalkCheckmkPulse
var store = new EndpointCatalogStore( var store = new EndpointCatalogStore(
_options.EndpointCatalogPath, _options.EndpointCatalogPath,
_options.EndpointCatalogMaxBytes, _options.EndpointCatalogMaxBytes,
_options.EndpointMaxCount); _options.EndpointCatalogMaxEntries);
EndpointCatalog catalog = null; EndpointCatalog catalog = null;
string readFailure = null; string readFailure = null;
try try
@@ -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)
@@ -76,7 +86,7 @@ namespace BizTalkCheckmkPulse
"Endpoint catalog synchronized. path=" + _options.EndpointCatalogPath "Endpoint catalog synchronized. path=" + _options.EndpointCatalogPath
+ " active_candidates=" + catalog.ActiveCandidates + " active_candidates=" + catalog.ActiveCandidates
+ " configured=" + catalog.Entries.Count + " configured=" + catalog.Entries.Count
+ " unsupported_external=" + catalog.UnsupportedCandidates); + " unresolved_candidates=" + catalog.UnresolvedCandidates);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -104,19 +114,28 @@ namespace BizTalkCheckmkPulse
state.CatalogAvailable = true; state.CatalogAvailable = true;
state.CatalogSynchronizedUtc = catalog.SynchronizedUtc; state.CatalogSynchronizedUtc = catalog.SynchronizedUtc;
state.Configured = catalog.Entries.Count; state.Configured = catalog.Entries.Count;
state.UnsupportedActive = CountUnsupportedExternal(result.EndpointCandidates); var activeEntries = ResolveActiveEndpoints(catalog, result.EndpointCandidates, state);
var activeTargetCount = activeEntries
.Select(TargetKey)
.Distinct(StringComparer.OrdinalIgnoreCase)
.Count();
if (activeTargetCount > _options.EndpointMaxCount)
{
state.Failure = "Eindeutige aktive Netzwerkziele=" + activeTargetCount
+ " ueberschreiten EndpointMaxCount=" + _options.EndpointMaxCount + ".";
return;
}
var activeKeys = new HashSet<string>(
result.EndpointCandidates.Where(x => x.Active).Select(x => x.Key),
StringComparer.OrdinalIgnoreCase);
var activeEntries = catalog.Entries
.Where(x => x.Enabled)
.Where(x => string.Equals(x.ArtifactType, "Manual", StringComparison.OrdinalIgnoreCase)
|| activeKeys.Contains(x.Key))
.Take(_options.EndpointMaxCount)
.ToArray();
state.Active = activeEntries.Length; state.Active = activeEntries.Length;
state.SkippedInactive = catalog.Entries.Count(x => x.Enabled) - activeEntries.Length;
// Detailwarnungen nur beim Katalogabgleich schreiben; der Minutensnapshot
// enthaelt die aktuelle begrenzte Liste bereits und das Tageslog bleibt kompakt.
foreach (var issue in state.RefreshRequired
? state.ResolutionIssues.Take(20)
: Enumerable.Empty<string>())
{
_logger.Warning("Endpoint target unresolved. " + issue);
}
try try
{ {
@@ -131,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)
@@ -147,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,
@@ -155,26 +187,26 @@ namespace BizTalkCheckmkPulse
var active = (candidates ?? Enumerable.Empty<EndpointCandidate>()) var active = (candidates ?? Enumerable.Empty<EndpointCandidate>())
.Where(x => x != null && x.Active) .Where(x => x != null && x.Active)
.ToArray(); .ToArray();
var discovered = new List<EndpointCatalogEntry>();
var unsupported = 0;
foreach (var candidate in active)
{
EndpointCatalogEntry entry;
string reason;
if (EndpointAddressParser.TryCreate(candidate, out entry, out reason))
{
discovered.Add(entry);
}
else if (EndpointAddressParser.IsPotentialExternalEndpoint(candidate))
{
unsupported++;
}
}
var manual = existing == null var manual = existing == null
? new EndpointCatalogEntry[0] ? new EndpointCatalogEntry[0]
: existing.Entries.Where(x => !x.AutoDiscovered).ToArray(); : existing.Entries.Where(x => !x.AutoDiscovered).ToArray();
var manualKeys = new HashSet<string>(manual.Select(x => x.Key), StringComparer.OrdinalIgnoreCase); var manualKeys = new HashSet<string>(manual.Select(x => x.Key), StringComparer.OrdinalIgnoreCase);
var discovered = new List<EndpointCatalogEntry>();
var unresolved = 0;
foreach (var candidate in active)
{
var resolution = EndpointAddressParser.Analyze(candidate);
if (resolution.Status == EndpointResolutionStatus.Probeable)
{
discovered.Add(resolution.Entry);
}
else if (resolution.Status == EndpointResolutionStatus.Unresolved
&& !manualKeys.Contains(candidate.Key))
{
unresolved++;
}
}
var merged = manual var merged = manual
.Concat(discovered.Where(x => !manualKeys.Contains(x.Key))) .Concat(discovered.Where(x => !manualKeys.Contains(x.Key)))
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase) .GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
@@ -187,7 +219,8 @@ namespace BizTalkCheckmkPulse
EnvironmentName = existing == null ? string.Empty : existing.EnvironmentName, EnvironmentName = existing == null ? string.Empty : existing.EnvironmentName,
SynchronizedUtc = synchronizedUtc.ToUniversalTime(), SynchronizedUtc = synchronizedUtc.ToUniversalTime(),
ActiveCandidates = active.Length, ActiveCandidates = active.Length,
UnsupportedCandidates = unsupported // Attributsname bleibt fuer die Rueckwaertskompatibilitaet des Katalogformats bestehen.
UnresolvedCandidates = unresolved
}; };
catalog.Entries.AddRange(merged); catalog.Entries.AddRange(merged);
return catalog; return catalog;
@@ -200,29 +233,113 @@ namespace BizTalkCheckmkPulse
{ {
var catalog = SynchronizeCatalog(existing, candidates, synchronizedUtc); var catalog = SynchronizeCatalog(existing, candidates, synchronizedUtc);
catalog.EnvironmentName = _options.EnvironmentName ?? string.Empty; catalog.EnvironmentName = _options.EnvironmentName ?? string.Empty;
if (catalog.Entries.Count > _options.EndpointMaxCount) if (catalog.Entries.Count > _options.EndpointCatalogMaxEntries)
{ {
throw new InvalidDataException("Probeable endpoints exceed EndpointMaxCount."); throw new InvalidDataException("Endpoint catalog exceeds EndpointCatalogMaxEntries.");
} }
return catalog; return catalog;
} }
private static int CountUnsupportedExternal(IEnumerable<EndpointCandidate> candidates) /// <summary>
/// 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.
/// </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(
EndpointCatalog catalog,
IEnumerable<EndpointCandidate> candidates,
EndpointConnectivityState state)
{ {
var count = 0; if (catalog == null)
foreach (var candidate in candidates.Where(x => x.Active))
{ {
EndpointCatalogEntry ignored; throw new ArgumentNullException("catalog");
string reason; }
if (!EndpointAddressParser.TryCreate(candidate, out ignored, out reason)
&& EndpointAddressParser.IsPotentialExternalEndpoint(candidate)) if (state == null)
{
throw new ArgumentNullException("state");
}
var activeCandidates = (candidates ?? Enumerable.Empty<EndpointCandidate>())
.Where(x => x != null && x.Active)
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
.Select(x => x.First())
.ToArray();
var activeKeys = new HashSet<string>(activeCandidates.Select(x => x.Key), StringComparer.OrdinalIgnoreCase);
var manualOverrides = catalog.Entries
.Where(x => x.Enabled && !x.AutoDiscovered)
.Where(x => !string.Equals(x.ArtifactType, "Manual", StringComparison.OrdinalIgnoreCase))
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
.ToDictionary(x => x.Key, x => x.First(), StringComparer.OrdinalIgnoreCase);
var selected = catalog.Entries
.Where(x => x.Enabled && string.Equals(x.ArtifactType, "Manual", StringComparison.OrdinalIgnoreCase))
.ToList();
foreach (var candidate in activeCandidates)
{
EndpointCatalogEntry manual;
if (manualOverrides.TryGetValue(candidate.Key, out manual))
{ {
count++; selected.Add(manual);
state.ManualOverridesActive++;
continue;
}
var resolution = EndpointAddressParser.Analyze(candidate);
if (resolution.Status == EndpointResolutionStatus.Probeable)
{
// Immer das aktuelle WMI-Ziel pruefen; der Wochenkatalog darf keine alte Adresse erzwingen.
selected.Add(resolution.Entry);
}
else if (resolution.Status == EndpointResolutionStatus.ExpectedNonProbeable)
{
state.ExpectedNonProbeableActive++;
}
else
{
state.UnresolvedActive++;
state.ResolutionIssues.Add(DisplayResolutionIssue(candidate, resolution.Reason));
} }
} }
return count; state.SkippedInactive = catalog.Entries.Count(x =>
x.Enabled
&& !string.Equals(x.ArtifactType, "Manual", StringComparison.OrdinalIgnoreCase)
&& !activeKeys.Contains(x.Key));
var resolved = selected
.GroupBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
.Select(x => x.First())
.ToArray();
state.BestEffortActive = resolved.Count(x => x.BestEffort);
return resolved;
}
/// <summary>
/// Formatiert eine Aufloesungsluecke ohne Transportadresse, Pfad, Querystring oder Zugangsdaten.
/// </summary>
private static string DisplayResolutionIssue(EndpointCandidate candidate, string reason)
{
var artifact = string.IsNullOrWhiteSpace(candidate.ApplicationName)
|| string.Equals(candidate.ApplicationName, "(unknown)", StringComparison.OrdinalIgnoreCase)
? candidate.ArtifactName
: candidate.ApplicationName + "\\" + candidate.ArtifactName;
return "endpoint=" + CompactText(candidate.ArtifactType) + ":" + CompactText(artifact)
+ "[" + CompactText(candidate.TransportRole) + "]"
+ " adapter=" + CompactText(candidate.AdapterName)
+ " reason=" + CompactText(reason);
}
private static string CompactText(string value)
{
return (value ?? string.Empty)
.Replace('\r', ' ')
.Replace('\n', ' ')
.Replace('|', '/')
.Trim();
} }
private async Task<IReadOnlyCollection<EndpointProbeResult>> ProbeAllAsync(EndpointCatalogEntry[] endpoints) private async Task<IReadOnlyCollection<EndpointProbeResult>> ProbeAllAsync(EndpointCatalogEntry[] endpoints)
@@ -267,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)
@@ -376,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)
+47 -2
View File
@@ -269,6 +269,26 @@ namespace BizTalkCheckmkPulse
} }
} }
/// <summary>
/// Ergebnis der geheimnisfreien Auswertung einer BizTalk-Transportadresse.
/// </summary>
internal enum EndpointResolutionStatus
{
Probeable,
ExpectedNonProbeable,
Unresolved
}
/// <summary>
/// Klassifiziert einen Kandidaten und enthaelt nur bei sicherer Aufloesung ein Socket-Ziel.
/// </summary>
internal sealed class EndpointResolution
{
public EndpointResolutionStatus Status { get; set; }
public EndpointCatalogEntry Entry { get; set; }
public string Reason { get; set; }
}
/// <summary> /// <summary>
/// Ein geheimnisfreier, lokal persistierbarer Netzwerk-Endpunkt. /// Ein geheimnisfreier, lokal persistierbarer Netzwerk-Endpunkt.
/// </summary> /// </summary>
@@ -285,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)
@@ -314,7 +347,7 @@ namespace BizTalkCheckmkPulse
public string MachineName { get; set; } public string MachineName { get; set; }
public string EnvironmentName { get; set; } public string EnvironmentName { get; set; }
public int ActiveCandidates { get; set; } public int ActiveCandidates { get; set; }
public int UnsupportedCandidates { get; set; } public int UnresolvedCandidates { get; set; }
public List<EndpointCatalogEntry> Entries { get; private set; } public List<EndpointCatalogEntry> Entries { get; private set; }
} }
@@ -337,6 +370,7 @@ namespace BizTalkCheckmkPulse
public EndpointConnectivityState() public EndpointConnectivityState()
{ {
Results = new List<EndpointProbeResult>(); Results = new List<EndpointProbeResult>();
ResolutionIssues = new List<string>();
} }
public bool Disabled { get; set; } public bool Disabled { get; set; }
@@ -348,10 +382,21 @@ namespace BizTalkCheckmkPulse
public int Configured { get; set; } public int Configured { get; set; }
public int Active { get; set; } public int Active { get; set; }
public int SkippedInactive { get; set; } public int SkippedInactive { get; set; }
public int UnsupportedActive { get; set; } public int UnresolvedActive { 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 UniqueTargets { get; set; } public int UniqueTargets { get; set; }
public long ProbeDurationMilliseconds { get; set; } public long ProbeDurationMilliseconds { get; set; }
public string Failure { get; set; } public string Failure { get; set; }
public List<EndpointProbeResult> Results { get; private set; } public List<EndpointProbeResult> Results { get; private set; }
public List<string> ResolutionIssues { get; private set; }
} }
} }
+57 -2
View File
@@ -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; }
@@ -41,12 +43,17 @@ namespace BizTalkCheckmkPulse
public bool ProbeEndpointConnectivity { get; set; } public bool ProbeEndpointConnectivity { get; set; }
public string EndpointCatalogPath { get; set; } public string EndpointCatalogPath { get; set; }
public int EndpointCatalogMaxBytes { get; set; } public int EndpointCatalogMaxBytes { get; set; }
public int EndpointCatalogMaxEntries { get; set; }
public int EndpointDiscoveryIntervalHours { get; set; } public int EndpointDiscoveryIntervalHours { get; set; }
public int EndpointProbeTimeoutMilliseconds { get; set; } public int EndpointProbeTimeoutMilliseconds { get; set; }
public int EndpointProbeMaxConcurrency { get; set; } public int EndpointProbeMaxConcurrency { get; set; }
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.
@@ -56,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;
@@ -83,20 +91,23 @@ namespace BizTalkCheckmkPulse
ProbeEndpointConnectivity = true; ProbeEndpointConnectivity = true;
EndpointCatalogPath = Path.Combine(commonData, "BizTalkCheckmkPulse", "data", "endpoints.xml"); EndpointCatalogPath = Path.Combine(commonData, "BizTalkCheckmkPulse", "data", "endpoints.xml");
EndpointCatalogMaxBytes = 1048576; EndpointCatalogMaxBytes = 1048576;
EndpointCatalogMaxEntries = 1000;
EndpointDiscoveryIntervalHours = 168; EndpointDiscoveryIntervalHours = 168;
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;
@@ -115,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);
@@ -141,6 +156,7 @@ namespace BizTalkCheckmkPulse
options.ProbeEndpointConnectivity = ReadBool(settings, "ProbeEndpointConnectivity", options.ProbeEndpointConnectivity); options.ProbeEndpointConnectivity = ReadBool(settings, "ProbeEndpointConnectivity", options.ProbeEndpointConnectivity);
options.EndpointCatalogPath = Environment.ExpandEnvironmentVariables(ReadString(settings, "EndpointCatalogPath", options.EndpointCatalogPath)); options.EndpointCatalogPath = Environment.ExpandEnvironmentVariables(ReadString(settings, "EndpointCatalogPath", options.EndpointCatalogPath));
options.EndpointCatalogMaxBytes = ReadInt(settings, "EndpointCatalogMaxBytes", options.EndpointCatalogMaxBytes, 4096, 16777216); options.EndpointCatalogMaxBytes = ReadInt(settings, "EndpointCatalogMaxBytes", options.EndpointCatalogMaxBytes, 4096, 16777216);
options.EndpointCatalogMaxEntries = ReadInt(settings, "EndpointCatalogMaxEntries", options.EndpointCatalogMaxEntries, 1, 100000);
options.EndpointDiscoveryIntervalHours = ReadInt(settings, "EndpointDiscoveryIntervalHours", options.EndpointDiscoveryIntervalHours, 1, 8760); options.EndpointDiscoveryIntervalHours = ReadInt(settings, "EndpointDiscoveryIntervalHours", options.EndpointDiscoveryIntervalHours, 1, 8760);
options.EndpointProbeTimeoutMilliseconds = ReadInt(settings, "EndpointProbeTimeoutMilliseconds", options.EndpointProbeTimeoutMilliseconds, 250, 30000); options.EndpointProbeTimeoutMilliseconds = ReadInt(settings, "EndpointProbeTimeoutMilliseconds", options.EndpointProbeTimeoutMilliseconds, 250, 30000);
options.EndpointProbeMaxConcurrency = ReadInt(settings, "EndpointProbeMaxConcurrency", options.EndpointProbeMaxConcurrency, 1, 64); options.EndpointProbeMaxConcurrency = ReadInt(settings, "EndpointProbeMaxConcurrency", options.EndpointProbeMaxConcurrency, 1, 64);
@@ -172,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;
@@ -204,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.");
+54 -2
View File
@@ -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,20 @@ 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="
+ result.EndpointConnectivity.UnresolvedActive
+ " endpoints_expected_non_socket="
+ result.EndpointConnectivity.ExpectedNonProbeableActive
+ " endpoints_manual_overrides="
+ result.EndpointConnectivity.ManualOverridesActive
+ " endpoints_unique_targets=" + " endpoints_unique_targets="
+ result.EndpointConnectivity.UniqueTargets + result.EndpointConnectivity.UniqueTargets
+ " endpoint_probe_ms=" + " endpoint_probe_ms="
@@ -125,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")]
+207
View File
@@ -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]
};
}
}
}
+6 -4
View File
@@ -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
}; };
} }
+747 -2
View File
@@ -29,11 +29,38 @@ namespace BizTalkCheckmkPulse.Tests
Run("SendPortAllowlistSeparatesExpectedState", SendPortAllowlistSeparatesExpectedState); Run("SendPortAllowlistSeparatesExpectedState", SendPortAllowlistSeparatesExpectedState);
Run("ArtifactSummaryIsBounded", ArtifactSummaryIsBounded); Run("ArtifactSummaryIsBounded", ArtifactSummaryIsBounded);
Run("EndpointAddressesResolveWithoutSecrets", EndpointAddressesResolveWithoutSecrets); Run("EndpointAddressesResolveWithoutSecrets", EndpointAddressesResolveWithoutSecrets);
Run("AdapterSpecificEndpointAddressesResolve", AdapterSpecificEndpointAddressesResolve);
Run("SchedulerDatabaseAndSapEndpointsAreClassifiedSafely", SchedulerDatabaseAndSapEndpointsAreClassifiedSafely);
Run("EndpointClassificationSeparatesExpectedAndUnresolved", EndpointClassificationSeparatesExpectedAndUnresolved);
Run("EndpointCatalogPreservesManualOverrides", EndpointCatalogPreservesManualOverrides); Run("EndpointCatalogPreservesManualOverrides", EndpointCatalogPreservesManualOverrides);
Run("EndpointRuntimeUsesCurrentAddressAndManualOverrides", EndpointRuntimeUsesCurrentAddressAndManualOverrides);
Run("EndpointCatalogRoundTrip", EndpointCatalogRoundTrip); Run("EndpointCatalogRoundTrip", EndpointCatalogRoundTrip);
Run("EndpointOutputListsOnlyUnavailableTargets", EndpointOutputListsOnlyUnavailableTargets); Run("EndpointOutputListsOnlyUnavailableTargets", EndpointOutputListsOnlyUnavailableTargets);
Run("BestEffortEndpointFailuresRemainNonAlerting", BestEffortEndpointFailuresRemainNonAlerting);
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);
@@ -326,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$");
@@ -368,9 +396,186 @@ namespace BizTalkCheckmkPulse.Tests
candidate.Address = "/Orders/Receive.svc"; candidate.Address = "/Orders/Receive.svc";
candidate.AdapterName = "WCF-CustomIsolated"; candidate.AdapterName = "WCF-CustomIsolated";
candidate.ArtifactType = "ReceiveLocation";
candidate.TransportRole = "Inbound";
Assert(!EndpointAddressParser.IsPotentialExternalEndpoint(candidate), "relative local receive address must not cause UNKNOWN"); Assert(!EndpointAddressParser.IsPotentialExternalEndpoint(candidate), "relative local receive address must not cause UNKNOWN");
} }
private static void AdapterSpecificEndpointAddressesResolve()
{
EndpointCatalogEntry endpoint;
string reason;
var candidate = new EndpointCandidate
{
ArtifactType = "SendPort",
ApplicationName = "Orders",
ArtifactName = "SP SQL",
TransportRole = "Primary",
AdapterName = "WCF-Custom",
Address = "mssql://sql01/INSTANCE/Orders",
Active = true
};
Assert(EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "WCF-SQL URI should resolve: " + reason);
AssertEqual("sql01", endpoint.Host, "WCF-SQL host");
AssertEqual(1433, endpoint.Port, "WCF-SQL default port");
candidate.Address = "mssql://sql01:15433/INSTANCE/Orders";
Assert(EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "WCF-SQL explicit port should resolve: " + reason);
AssertEqual(15433, endpoint.Port, "WCF-SQL explicit port");
candidate.Address = "net.tcp://service01/Orders";
candidate.AdapterName = "WCF-NetTcp";
Assert(EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "net.tcp URI should resolve: " + reason);
AssertEqual(808, endpoint.Port, "net.tcp default port");
candidate.Address = "transfer.example.test:2222/outbound";
candidate.AdapterName = "Custom Adapter";
Assert(EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "plain host:port should resolve before URI parsing: " + reason);
AssertEqual("transfer.example.test", endpoint.Host, "plain host:port host");
AssertEqual(2222, endpoint.Port, "plain host:port port");
candidate.Address = "integration-user@sftp.example.test/inbound";
candidate.AdapterName = "SFTP";
Assert(EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "schema-less SFTP URI with user info should resolve: " + reason);
AssertEqual("sftp.example.test", endpoint.Host, "schema-less SFTP host");
AssertEqual(22, endpoint.Port, "schema-less SFTP default port");
Assert(endpoint.Host.IndexOf("integration-user", StringComparison.OrdinalIgnoreCase) < 0, "SFTP user info must not enter catalog host");
candidate.Address = "ftp-user@ftp.example.test:2121/drop";
candidate.AdapterName = "FTP";
Assert(EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "schema-less FTP URI with user info should resolve: " + reason);
AssertEqual("ftp.example.test", endpoint.Host, "schema-less FTP host");
AssertEqual(2121, endpoint.Port, "schema-less FTP explicit port");
candidate.Address = "sftp://[2001:db8::10]:2222/inbound";
candidate.AdapterName = "SFTP";
Assert(EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "bracketed IPv6 URI should resolve: " + reason);
AssertEqual("2001:db8::10", endpoint.Host, "IPv6 catalog host must not retain URI brackets");
AssertEqual(2222, endpoint.Port, "IPv6 explicit port");
candidate.Address = "oracledb://oracle01/ORDERS/Dedicated";
candidate.AdapterName = "WCF-OracleDB";
Assert(EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "direct Oracle DB URI should resolve: " + reason);
AssertEqual("oracle01", endpoint.Host, "Oracle DB host");
AssertEqual(1521, endpoint.Port, "Oracle DB default port");
candidate.Address = "oracledb://ORDERS_TNS";
Assert(!EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "tnsnames alias must not be guessed as DNS host");
candidate.ArtifactType = "ReceiveLocation";
candidate.TransportRole = "Inbound";
candidate.Address = "http://+:8080/Orders";
candidate.AdapterName = "WCF-WebHttp";
Assert(EndpointAddressParser.TryCreate(candidate, out endpoint, out reason), "wildcard listener should resolve: " + reason);
AssertEqual("127.0.0.1", endpoint.Host, "wildcard listener loopback host");
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()
{
var candidate = new EndpointCandidate
{
ArtifactType = "ReceiveLocation",
ApplicationName = "Orders",
ArtifactName = "RL HTTP",
TransportRole = "Inbound",
AdapterName = "WCF-CustomIsolated",
Address = "/Orders/Receive.svc",
Active = true
};
AssertEqual(
EndpointResolutionStatus.ExpectedNonProbeable,
EndpointAddressParser.Analyze(candidate).Status,
"relative listener classification");
candidate.ArtifactType = "SendPort";
candidate.TransportRole = "Primary";
AssertEqual(
EndpointResolutionStatus.Unresolved,
EndpointAddressParser.Analyze(candidate).Status,
"relative outbound address classification");
candidate.Address = "http://+:8080/Orders";
AssertEqual(
EndpointResolutionStatus.Unresolved,
EndpointAddressParser.Analyze(candidate).Status,
"outbound wildcard must not become a loopback target");
candidate.Dynamic = true;
AssertEqual(
EndpointResolutionStatus.ExpectedNonProbeable,
EndpointAddressParser.Analyze(candidate).Status,
"dynamic send port classification");
}
private static void EndpointCatalogPreservesManualOverrides() private static void EndpointCatalogPreservesManualOverrides()
{ {
var candidate = new EndpointCandidate var candidate = new EndpointCandidate
@@ -417,6 +622,42 @@ namespace BizTalkCheckmkPulse.Tests
Assert(synchronized.Entries.All(x => x.ArtifactName != "RL Disabled"), "inactive receive location must not be added"); Assert(synchronized.Entries.All(x => x.ArtifactName != "RL Disabled"), "inactive receive location must not be added");
} }
private static void EndpointRuntimeUsesCurrentAddressAndManualOverrides()
{
var candidate = new EndpointCandidate
{
ArtifactType = "SendPort",
ApplicationName = "Orders",
ArtifactName = "SP Orders",
TransportRole = "Primary",
AdapterName = "WCF-Custom",
Address = "https://new.example.test/orders",
Active = true
};
var catalog = new EndpointCatalog();
var stale = TestEndpoint("SP Orders", "old.example.test");
stale.Key = candidate.Key;
catalog.Entries.Add(stale);
var state = new EndpointConnectivityState();
var selected = EndpointConnectivityProbe.ResolveActiveEndpoints(catalog, new[] { candidate }, state);
AssertEqual(1, selected.Length, "current endpoint selection count");
AssertEqual("new.example.test", selected[0].Host, "current WMI address must replace stale catalog target");
candidate.Address = "adapter-specific-target-without-port";
var manual = TestEndpoint("SP Orders", "manual.example.test");
manual.Key = candidate.Key;
manual.AutoDiscovered = false;
manual.Port = 7443;
catalog.Entries.Clear();
catalog.Entries.Add(manual);
state = new EndpointConnectivityState();
selected = EndpointConnectivityProbe.ResolveActiveEndpoints(catalog, new[] { candidate }, state);
AssertEqual(1, selected.Length, "manual endpoint selection count");
AssertEqual("manual.example.test", selected[0].Host, "manual override host");
AssertEqual(0, state.UnresolvedActive, "manual override must close resolution gap");
AssertEqual(1, state.ManualOverridesActive, "manual override metric");
}
private static void EndpointOutputListsOnlyUnavailableTargets() private static void EndpointOutputListsOnlyUnavailableTargets()
{ {
var result = new ProbeResult(); var result = new ProbeResult();
@@ -445,6 +686,54 @@ 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()
{
var result = new ProbeResult();
result.EndpointConnectivity.RuntimeStateAvailable = true;
result.EndpointConnectivity.CatalogAvailable = true;
result.EndpointConnectivity.RefreshSucceeded = true;
result.EndpointConnectivity.ExpectedNonProbeableActive = 4;
var line = FindServiceLine(result, "Endpoint Reachability");
Assert(line.StartsWith("0 \"BizTalk Endpoint Reachability\"", StringComparison.Ordinal), "expected non-socket endpoints must remain OK");
Assert(line.Contains("biztalk_endpoints_expected_non_socket=4"), "expected non-socket metric missing");
Assert(line.IndexOf("unsupported", StringComparison.OrdinalIgnoreCase) < 0, "legacy unsupported wording must not be emitted");
result.EndpointConnectivity.UnresolvedActive = 1;
result.EndpointConnectivity.ResolutionIssues.Add(
"endpoint=SendPort:Orders\\SP Custom[Primary] adapter=WCF-Custom reason=Schema ohne Port");
line = FindServiceLine(result, "Endpoint Reachability");
Assert(line.StartsWith("3 \"BizTalk Endpoint Reachability\"", StringComparison.Ordinal), "real resolution gap must be UNKNOWN");
Assert(line.Contains("unresolved_endpoints="), "resolution issue list missing");
Assert(line.Contains("SP Custom"), "resolution issue artifact missing");
}
private static void EndpointCatalogRoundTrip() private static void EndpointCatalogRoundTrip()
{ {
var directory = Path.Combine(Path.GetTempPath(), "BizTalkCheckmkPulse.CatalogTests." + Guid.NewGuid().ToString("N")); var directory = Path.Combine(Path.GetTempPath(), "BizTalkCheckmkPulse.CatalogTests." + Guid.NewGuid().ToString("N"));
@@ -457,9 +746,11 @@ namespace BizTalkCheckmkPulse.Tests
EnvironmentName = "ACC", EnvironmentName = "ACC",
SynchronizedUtc = DateTime.UtcNow, SynchronizedUtc = DateTime.UtcNow,
ActiveCandidates = 150, ActiveCandidates = 150,
UnsupportedCandidates = 0 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");
@@ -467,7 +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");
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
{ {
@@ -511,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"));
@@ -524,8 +989,10 @@ 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=\"NewSetting\" value=\"new-default\" />" + "<add key=\"NewSetting\" value=\"new-default\" />"
+ "</appSettings></configuration>", + "</appSettings></configuration>",
new UTF8Encoding(false)); new UTF8Encoding(false));
@@ -546,8 +1013,10 @@ 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=\"NewSetting\" value=\"new-default\""), "new source setting must be added"); Assert(merged.Contains("key=\"NewSetting\" value=\"new-default\""), "new source setting must be added");
Assert(merged.IndexOf("RemovedLegacySetting", StringComparison.Ordinal) < 0, "removed legacy key must not be resurrected"); Assert(merged.IndexOf("RemovedLegacySetting", StringComparison.Ordinal) < 0, "removed legacy key must not be resurrected");
@@ -564,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) =>
@@ -713,6 +1450,14 @@ namespace BizTalkCheckmkPulse.Tests
} }
} }
private static void AssertEqual(EndpointResolutionStatus expected, EndpointResolutionStatus actual, string label)
{
if (expected != actual)
{
throw new InvalidOperationException(label + ": expected " + expected + ", actual " + actual);
}
}
private static void AssertEqual(string expected, string actual, string label) private static void AssertEqual(string expected, string actual, string label)
{ {
if (!string.Equals(expected, actual, StringComparison.Ordinal)) if (!string.Equals(expected, actual, StringComparison.Ordinal))