diff --git a/BizTalkCheckmkPulse.sln b/BizTalkCheckmkPulse.sln index 0558533..b44d8e4 100644 --- a/BizTalkCheckmkPulse.sln +++ b/BizTalkCheckmkPulse.sln @@ -1,9 +1,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31624.102 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkCheckmkPulse", "src\BizTalkCheckmkPulse\BizTalkCheckmkPulse.csproj", "{A4D4D050-9EA7-4A71-B510-7D9D699B9F38}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkCheckmkPulse.Tests", "tests\BizTalkCheckmkPulse.Tests\BizTalkCheckmkPulse.Tests.csproj", "{E219C45A-CC95-4F45-9B6F-8244CBAEE35A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -14,9 +16,12 @@ Global {A4D4D050-9EA7-4A71-B510-7D9D699B9F38}.Debug|Any CPU.Build.0 = Debug|Any CPU {A4D4D050-9EA7-4A71-B510-7D9D699B9F38}.Release|Any CPU.ActiveCfg = Release|Any CPU {A4D4D050-9EA7-4A71-B510-7D9D699B9F38}.Release|Any CPU.Build.0 = Release|Any CPU + {E219C45A-CC95-4F45-9B6F-8244CBAEE35A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E219C45A-CC95-4F45-9B6F-8244CBAEE35A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E219C45A-CC95-4F45-9B6F-8244CBAEE35A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E219C45A-CC95-4F45-9B6F-8244CBAEE35A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection EndGlobal - diff --git a/Dokumentation.md b/Dokumentation.md index 58bdce1..9712426 100644 --- a/Dokumentation.md +++ b/Dokumentation.md @@ -100,7 +100,7 @@ BizTalkCheckmkPulse.exe Checkmk Local Check Zeilen nach STDOUT ``` -Der `.cmd`-Wrapper liefert auch dann eine gueltige `UNKNOWN`-Zeile mit Massnahme, wenn die EXE fehlt oder bereits der Prozessstart mit einem Exitcode fehlschlaegt. Die EXE selbst faengt Laufzeitfehler ab und schreibt fuer alle stabilen Services `UNKNOWN`, damit unvollstaendige Deployments oder WMI-Probleme in Checkmk sichtbar bleiben. +Der `.cmd`-Wrapper liefert auch dann fuer alle sechs stabilen Services gueltige `UNKNOWN`-Zeilen mit Massnahme, wenn die EXE fehlt oder bereits der Prozessstart mit einem Exitcode fehlschlaegt. Die EXE selbst faengt Laufzeitfehler ab und schreibt ebenfalls fuer alle stabilen Services `UNKNOWN`, damit unvollstaendige Deployments oder WMI-Probleme in Checkmk sichtbar bleiben. ## Datenquellen @@ -116,14 +116,15 @@ Genutzte Klassen: | Klasse | Zweck | | --- | --- | -| `MSBTS_GroupSetting` | BizTalk-Gruppe und Management-DB-Hinweise. | -| `MSBTS_MessageBoxSetting` | MessageBox-DB-Hinweise. | -| `MSBTS_HostInstance` | Host-Instance-Zustand. | +| `MSBTS_GroupSetting` | BizTalk-Gruppe, Management-DB und Master-MessageBox (`SubscriptionDB*`). | +| `MSBTS_HostInstance` | Host-Instance-Zustand; Ergebnis wird clientseitig auf den ueberwachten Server begrenzt. | | `MSBTS_ServiceInstance` | Suspended service instances. | | `MSBTS_ReceiveLocation` | Receive-Location-Zustand. | | `MSBTS_SendPort` | Send-Port-Zustand. | | `MSBTS_Orchestration` | Orchestration-Zustand. | -| `MSBTS_ReceivePort` | Application-Mapping fuer Details. | +| `MSBTS_ReceivePort` | Optionales Best-Effort-Application-Mapping, nur wenn per-Application-Services aktiviert sind. | + +Die Plattformabfrage verwendet absichtlich keine vermeintliche Klasse `MSBTS_MessageBoxSetting`: Sie ist nicht Bestandteil des dokumentierten BizTalk-WMI-Schemas. Auch die vorhandene Klasse `MSBTS_MsgBoxSetting` ist fuer die Zielermittlung nicht erforderlich. `MSBTS_GroupSetting` liefert mit `SubscriptionDBServerName` und `SubscriptionDBName` bereits das Ziel der Master-MessageBox. Dadurch entfallen eine Providerabfrage und eine unnoetige Berechtigungs-/Schemaschnittstelle. ### Windows Application Event Log @@ -139,7 +140,7 @@ Die Liste ist ueber `EventLogSources` konfigurierbar. ### SQL-Zugriffsprobe -Nach erfolgreicher oder teilweise erfolgreicher Plattformabfrage uebernimmt der SQL-Probe die per WMI ermittelten Management- und MessageBox-Ziele. Fuer jedes eindeutige Ziel wird mit `System.Data.SqlClient` eine Verbindung mit integrierter Windows-Authentifizierung geoeffnet und `SELECT 1` ausgefuehrt. Die Verbindung wird unmittelbar danach geschlossen; es werden keine BizTalk-Tabellen gelesen oder veraendert. +Nach erfolgreicher oder teilweise erfolgreicher Plattformabfrage uebernimmt der SQL-Probe die per `MSBTS_GroupSetting` ermittelten Management- und Master-MessageBox-Ziele. Fuer jedes eindeutige Ziel wird mit `System.Data.SqlClient` eine Verbindung mit integrierter Windows-Authentifizierung geoeffnet und `SELECT 1` ausgefuehrt. Die Verbindung wird unmittelbar danach geschlossen; es werden keine BizTalk-Tabellen gelesen oder veraendert. Der Test laeuft unter derselben Identitaet wie der Checkmk Local Check. Damit wird sichtbar, ob `LocalSystem` beziehungsweise das Maschinenkonto des BizTalk-Servers das SQL-Ziel tatsaechlich erreichen und die Datenbank oeffnen kann. Die Probe ist mit `ProbeSqlConnectivity=false` deaktivierbar und verwendet `SqlConnectionTimeoutSeconds` mit dem Default 5 Sekunden je Ziel als Timeout. @@ -200,13 +201,14 @@ WMI-Verbindungsaufbau und jede erforderliche WMI-Klasse werden separat bewertet. | `Wmi/Permission` | Namespace oder BizTalk-WMI-Klasse verweigert den Zugriff. | Namespace-ACL bei Verbindungsfehlern beziehungsweise BizTalk-Operator-Gruppe bei Klassenfehlern pruefen. | | `Wmi/Connectivity` | WMI-/RPC-Ziel nicht erreichbar. | WMI-Dienst, Provider und bei Remote-WMI zusaetzlich DNS/RPC/Firewall pruefen. | | `Wmi/Timeout` | WMI-Abfrage ueberschreitet `QueryTimeoutSeconds`. | WMI-, BizTalk- und SQL-Auslastung untersuchen, bevor der Timeout erhoeht wird. | -| `Wmi/Configuration` | Namespace oder Klasse fehlt. | BizTalk-WMI-Provider und BizTalk-Installation pruefen beziehungsweise reparieren. | +| `Wmi/Configuration` | BizTalk-WMI-Namespace fehlt oder Plattformdaten sind unvollstaendig. | Provider, Namespace und BizTalk-Konfiguration pruefen. | +| `Wmi/Schema` | `InvalidClass` oder `InvalidQuery`; Klasse beziehungsweise WQL passt nicht zum installierten Provider. | Klasse/Properties gegen das BizTalk-WMI-Schema pruefen; keine Rechteerhoehung vornehmen. | | `Sql/Permission` | Login oder Datenbankzugriff wird abgelehnt. | Maschinenkonto der BizTalk-Operator-Gruppe zuordnen, Kerberos erneuern, keine direkten DB-Rollen vergeben. | | `Sql/Connectivity` | SQL-Server oder Instanz nicht erreichbar. | Servername, DNS, SQL-Dienst, TCP-Protokoll, Port und Firewall pruefen. | | `Sql/Timeout` | SQL-Verbindung oder Testabfrage laeuft in den Timeout. | Netzwerk und SQL-Auslastung pruefen; Timeout nur begruendet anheben. | | `Sql/Configuration` | WMI-Ziele unvollstaendig oder TLS-, Zertifikats-, SPN-/SSPI-Konfiguration fehlerhaft. | Plattformabfragen beziehungsweise Zertifikatskette, Verschluesselung, SPN und Kerberos gezielt pruefen. | -Jede Diagnose enthaelt Bereich/Kategorie, betroffene Komponente, eine kurze Ursache, `Massnahme:` und `Technik:` mit Exception-Typ, HRESULT oder SQL-Fehlernummer. Erforderliche Datenquellen besitzen eigene Verfuegbarkeitsflags. Schlaegt beispielsweise `MSBTS_ServiceInstance` fehl, wird `BizTalk Suspended Instances` zwingend `UNKNOWN`; eine leere Ergebnisliste darf nicht als 'keine Suspensions' fehlinterpretiert werden. +Jede Diagnose enthaelt Bereich/Kategorie, betroffene Komponente, eine kurze Ursache, `Massnahme:` und `Technik:` mit Exception-Typ, HRESULT oder SQL-Fehlernummer. Bei WMI-Abfragefehlern werden zusaetzlich WQL und Laufzeit bis zum Fehler ausgegeben. Erforderliche Datenquellen besitzen eigene Verfuegbarkeitsflags. Schlaegt beispielsweise `MSBTS_ServiceInstance` fehl, wird `BizTalk Suspended Instances` zwingend `UNKNOWN`; eine leere Ergebnisliste darf nicht als 'keine Suspensions' fehlinterpretiert werden. Die Operator-Mitgliedschaft des Computerkontos steht allen auf diesem Server als `LocalSystem` laufenden Diensten fuer Netzwerkzugriffe zur Verfuegung. Falls diese Sicherheitsauswirkung nicht akzeptabel ist, kann ein separater Collector unter einem dedizierten gMSA- oder Dienstkonto mit Operator-Rechten Checkmk-Spooldaten erzeugen. Diese Variante ist noch nicht Bestandteil der aktuellen Implementierung. Der komplette Checkmk-Agent sollte nicht allein fuer dieses Plugin auf eine andere Identitaet umgestellt werden, weil dies alle Agent-Sektionen und Local Checks betrifft. @@ -223,9 +225,13 @@ Quellen: Das Plugin ist bewusst defensiv gebaut: +- Der dependency-freie Regressionstest laeuft mit dem gleichen .NET-Framework-/MSBuild-Baseline wie die Anwendung. - WMI-Queries haben ein konfigurierbares Timeout. - WMI-Namespace und Pflichtklassen werden getrennt auf Berechtigung, Konfiguration, Erreichbarkeit und Timeout geprueft. -- Management- und MessageBox-Datenbank werden mit der echten Agent-Identitaet getestet. +- Management- und Master-MessageBox-Datenbank werden aus einer dokumentierten WMI-Klasse ermittelt und mit der echten Agent-Identitaet getestet. +- `InvalidClass`/`InvalidQuery` werden als Schemafehler statt als Berechtigungsproblem ausgewiesen. +- Host Instances werden ohne namensabhaengigen WQL-Filter clientseitig auf den ueberwachten Server begrenzt. +- Bereits fuer Runtime-Zustaende gelesene Artefakte werden fuer das Best-Effort-Application-Mapping wiederverwendet; doppelte WMI-Abfragen entfallen. - SQL-Fehlernummern werden in Berechtigung, Erreichbarkeit, Timeout oder Providerfehler klassifiziert. - Fehlgeschlagene Pflichtabfragen erzeugen `UNKNOWN` statt irrefuehrender Nullwerte. - Optionale WMI-Klassen erzeugen Diagnosehinweise statt Totalabbruch. @@ -338,6 +344,7 @@ Der Checkmk Windows Agent laeuft normalerweise als LocalSystem. Daher immer auch Die Diagnose im Service-Text nach `Massnahme:` abarbeiten. Wichtige Kategorien: - `Wmi/Permission`: lokale Namespace-ACL oder BizTalk-Operator-Zuordnung pruefen. +- `Wmi/Schema`: Klasse/WQL gegen das installierte BizTalk-Schema pruefen; `InvalidClass` ist nicht durch zusaetzliche Rechte loesbar. - `Sql/Permission`: Maschinenkonto `DOMAIN\BIZTALKSERVER$` und BizTalk-Operator-Gruppe pruefen. - `Sql/Connectivity`: SQL-Server-/Instanzname, DNS, SQL-Dienst, TCP und Firewall pruefen. - `Sql/Timeout`: SQL- und Netzwerkauslastung pruefen; Timeout nicht als erste Massnahme erhoehen. diff --git a/README.md b/README.md index 1f167e6..4a111bc 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ Der Ansatz ist fuer Umgebungen wie `ACC`, `DEV`, `TST` und `PRD` gedacht, in denen pro Umgebung ein BizTalk Server 2020 und ein SQL Server betrieben werden. SQL Server bleibt beim mitgelieferten Checkmk-MSSQL-Plugin; dieses Projekt ergaenzt die BizTalk-spezifische Sicht. +Die Plattform- und SQL-Zielermittlung verwendet ausschliesslich die dokumentierte WMI-Klasse `MSBTS_GroupSetting`. Management-Datenbank und Master-MessageBox werden ueber `MgmtDbServerName`/`MgmtDbName` sowie `SubscriptionDBServerName`/`SubscriptionDBName` gelesen. Eine Klasse `MSBTS_MessageBoxSetting` gehoert nicht zum BizTalk-WMI-Schema und wird bewusst nicht abgefragt. + ## Warum Local Check statt serverseitigem Check-Plugin? Checkmk 2.4 kann Windows-Agent-Plugins und Local Checks direkt ausfuehren. Fuer diese Umgebung ist ein C#/.NET-Framework-Programm mit `.cmd`-Wrapper die robusteste Variante: @@ -34,7 +36,7 @@ Beispielausgaben stehen in [docs/ExampleOutput.md](docs/ExampleOutput.md). Voraussetzungen auf einem Windows-Build-Host: -- Visual Studio 2022 Build Tools oder Visual Studio +- Visual Studio 2019/2022 Build Tools oder Visual Studio - MSBuild im `PATH` - .NET Framework 4.7.2 Developer Pack @@ -44,6 +46,12 @@ Build: scripts\build-release.cmd ``` +Build plus dependency-free regression tests: + +```cmd +scripts\test-release.cmd +``` + Deployment-Paket erstellen: ```cmd @@ -66,6 +74,8 @@ Validierung nach dem Build: artifacts\BizTalkCheckmkPulse-deploy\biztalk_checkmk_pulse.cmd --self-test ``` +Der Self-Test fuehrt keine WMI-, SQL- oder Event-Log-Abfrage aus und muss genau sechs `OK`-Zeilen liefern. Der Test-Runner prueft zusaetzlich UNKNOWN-Fallbacks, dynamische Application-Services, FQDN-/Kurznamensvergleich ohne WQL-Namensfilter und den dokumentierten `MSBTS_GroupSetting`-Queryvertrag. Die zusaetzliche lokale Verifikation mit Mono/MSBuild 16 prueft Build und Ausgabeformat, ersetzt aber nicht den Windows-/BizTalk-Test. + ## Installation auf dem BizTalk-Server Kopiere den Inhalt von `artifacts\BizTalkCheckmkPulse-deploy` nach: @@ -113,7 +123,7 @@ Wichtige Werte: | `ServicePrefix` | `BizTalk` | Prefix fuer alle Checkmk-Services. | | `EnvironmentName` | leer | Optional `ACC`, `DEV`, `TST` oder `PRD`. | | `QueryTimeoutSeconds` | `25` | WMI-Timeout pro Query. | -| `ProbeSqlConnectivity` | `true` | Prueft die integrierte Windows-Anmeldung an den per WMI ermittelten Management- und MessageBox-Datenbanken. | +| `ProbeSqlConnectivity` | `true` | Prueft die integrierte Windows-Anmeldung an der per WMI ermittelten Management- und Master-MessageBox-Datenbank. | | `SqlConnectionTimeoutSeconds` | `5` | Timeout je SQL-Ziel fuer Verbindungsaufbau und harmlose Testabfrage. | | `WarnResumableThreshold` | `1` | WARN ab n resumable suspended instances. | | `CritNonResumableThreshold` | `1` | CRIT ab n non-resumable suspended instances. | @@ -202,13 +212,15 @@ Erwartet werden die sechs stabilen `BizTalk ...` Services mit plausiblen Daten. Das Programm prueft Berechtigungen aktiv: - Der WMI-Namespace-Verbindungsaufbau und jede erforderliche WMI-Klasse werden getrennt bewertet. -- Fehler werden als `Permission`, `Connectivity`, `Timeout`, `Configuration` oder `Provider` klassifiziert. -- `BizTalk SQL Access` oeffnet mit integrierter Windows-Authentifizierung eine Verbindung zur ermittelten Management- und MessageBox-Datenbank und fuehrt `SELECT 1` aus. +- Fehler werden als `Permission`, `Connectivity`, `Timeout`, `Configuration`, `Schema` oder `Provider` klassifiziert. +- `BizTalk SQL Access` oeffnet mit integrierter Windows-Authentifizierung eine Verbindung zur ermittelten Management- und Master-MessageBox-Datenbank und fuehrt `SELECT 1` aus. - Die Service-Ausgabe nennt Ausfuehrungsidentitaet, erwartete Netzwerkidentitaet, betroffene Komponente, technische Ursache und konkrete Massnahme. - Fehlgeschlagene Pflichtabfragen werden nie als leerer, erfolgreicher Datenbestand gewertet. Der betroffene Service wird `UNKNOWN`. `Sql/Configuration` weist je nach Detailtext entweder auf eine unvollstaendige WMI-Zielermittlung oder auf TLS-, Zertifikats-, SPN-/SSPI-Probleme hin. Die Diagnose empfiehlt bewusst nicht, SQL-Verschluesselung pauschal abzuschalten. +`Wmi/Schema` kennzeichnet `InvalidClass` oder `InvalidQuery`. Das ist kein Berechtigungsfehler: Klassen- und Property-Namen muessen gegen das installierte BizTalk-WMI-Schema geprueft werden; eine Rechteerhoehung kann eine nicht vorhandene Klasse nicht erzeugen. Die technischen Details enthalten die ausgefuehrte WQL-Abfrage und deren Laufzeit bis zum Fehler. + Der SQL-Zugriffstest ersetzt nicht das Checkmk-MSSQL-Plugin auf dem SQL-Server. Er prueft ausschliesslich, ob genau die Identitaet des BizTalk Local Checks die fuer seine Diagnose benoetigten BizTalk-Datenbankziele erreichen kann. Da bei fehlenden Rechten jeder Lauf einen abgewiesenen SQL-Login erzeugen kann, wird fuer den produktiven Betrieb eine asynchrone Ausfuehrung mit 300 Sekunden Cache empfohlen. Das begrenzt SQL-Logeintraege und Last, verzoegert einen Zustandswechsel aber um maximal fuenf Minuten. Beispiel in `%ProgramData%\checkmk\agent\check_mk.user.yml`: @@ -240,6 +252,7 @@ Entscheidungsmatrix: | --- | --- | | Alle BizTalk-Services liefern plausible Daten | Keine Berechtigungsaenderung erforderlich. | | Verbindung zu `root\MicrosoftBizTalkServer` scheitert | Namespace, BizTalk-WMI-Provider, WMI-Dienst und Namespace-ACL gezielt pruefen. Keine pauschalen WMI-Rechte vergeben. | +| Service meldet `Wmi/Schema` beziehungsweise `InvalidClass`/`InvalidQuery` | WMI-Klasse und Properties gegen das BizTalk-Schema pruefen. Keine Berechtigungen erweitern. | | `BizTalk SQL Access` meldet `Sql/Permission` | Computerkonto `DOMAIN\BIZTALKSERVER$` in die konfigurierte BizTalk-Operator-Gruppe aufnehmen, Kerberos erneuern und erneut testen. Keine direkten BizTalk-DB-Rollen vergeben. | | `BizTalk SQL Access` meldet `Sql/Connectivity` oder `Sql/Timeout` | Server-/Instanzname, DNS, SQL-Dienst, TCP-Protokoll, Port und Firewall aus Sicht des BizTalk-Servers pruefen. | | Nur SQL-gestuetzte BizTalk-Klassen liefern `Access denied` oder `UNKNOWN` | Computerkonto `DOMAIN\BIZTALKSERVER$` zunaechst in `BizTalk Server Operators` aufnehmen. | @@ -270,6 +283,8 @@ Empfohlene Alarmierung: - Checkmk MKP-Pakete: https://docs.checkmk.com/latest/en/mkps.html - Microsoft BizTalk WMI `MSBTS_ServiceInstance`: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-serviceinstance-wmi - Microsoft BizTalk WMI `ServiceStatus`: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-serviceinstance-servicestatus-property-wmi +- Microsoft BizTalk WMI `MSBTS_GroupSetting`: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-groupsetting-wmi +- Microsoft BizTalk WMI Core Server Classes: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/core-server-classes - Microsoft BizTalk Mindestberechtigungen: https://learn.microsoft.com/en-us/biztalk/core/minimum-security-user-rights - Microsoft BizTalk Access Control: https://learn.microsoft.com/en-us/biztalk/core/access-control-and-data-security - Microsoft LocalSystem und Computerkonten: https://learn.microsoft.com/en-us/entra/architecture/service-accounts-computer diff --git a/deployment/checkmk/biztalk_checkmk_pulse.cmd b/deployment/checkmk/biztalk_checkmk_pulse.cmd index 8f3af51..85ced31 100644 --- a/deployment/checkmk/biztalk_checkmk_pulse.cmd +++ b/deployment/checkmk/biztalk_checkmk_pulse.cmd @@ -6,6 +6,11 @@ set "EXE=%BASE%\BizTalkCheckmkPulse.exe" if not exist "%EXE%" ( echo 3 "BizTalk Platform" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Deployment-Paket und Dateirechte pruefen und danach cmk-agent-ctl.exe dump erneut ausfuehren. + echo 3 "BizTalk SQL Access" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Deployment-Paket und Dateirechte pruefen. + echo 3 "BizTalk Suspended Instances" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Deployment-Paket und Dateirechte pruefen. + echo 3 "BizTalk Host Instances" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Deployment-Paket und Dateirechte pruefen. + echo 3 "BizTalk Runtime Artifacts" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Deployment-Paket und Dateirechte pruefen. + echo 3 "BizTalk Event Log" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Deployment-Paket und Dateirechte pruefen. exit /b 0 ) @@ -13,5 +18,10 @@ if not exist "%EXE%" ( set "EXITCODE=%ERRORLEVEL%" if not "%EXITCODE%"=="0" ( echo 3 "BizTalk Platform" - BizTalkCheckmkPulse.exe konnte nicht erfolgreich gestartet werden, Exitcode=%EXITCODE%. Massnahme: .NET Framework 4.7.2, EXE/Config-Dateien und Windows Application Log pruefen. + echo 3 "BizTalk SQL Access" - BizTalkCheckmkPulse.exe endete mit Exitcode=%EXITCODE%. Massnahme: .NET Framework 4.7.2, EXE/Config-Dateien und Windows Application Log pruefen. + echo 3 "BizTalk Suspended Instances" - BizTalkCheckmkPulse.exe endete mit Exitcode=%EXITCODE%. Massnahme: Programmstart und Windows Application Log pruefen. + echo 3 "BizTalk Host Instances" - BizTalkCheckmkPulse.exe endete mit Exitcode=%EXITCODE%. Massnahme: Programmstart und Windows Application Log pruefen. + echo 3 "BizTalk Runtime Artifacts" - BizTalkCheckmkPulse.exe endete mit Exitcode=%EXITCODE%. Massnahme: Programmstart und Windows Application Log pruefen. + echo 3 "BizTalk Event Log" - BizTalkCheckmkPulse.exe endete mit Exitcode=%EXITCODE%. Massnahme: Programmstart und Windows Application Log pruefen. ) exit /b 0 diff --git a/docs/CheckmkServices.md b/docs/CheckmkServices.md index e346fa9..5a6e86c 100644 --- a/docs/CheckmkServices.md +++ b/docs/CheckmkServices.md @@ -13,14 +13,14 @@ Die Ausgabe ist so gebaut, dass sie auch mit Checkmk 2.4.0p4 kompatibel bleibt: Zweck: - Prueft, ob der BizTalk-WMI-Namespace erreichbar ist. -- Zeigt Server, Gruppe, Management-DB und MessageBox-DB, soweit WMI diese Werte liefert. +- Zeigt Server, Gruppe, Management-DB und Master-MessageBox-DB aus `MSBTS_GroupSetting`. Status: | Status | Bedingung | | --- | --- | -| `OK` | WMI-Verbindung und beide Plattform-Pflichtklassen erfolgreich. | -| `UNKNOWN` | Namespace, `MSBTS_GroupSetting`, `MSBTS_MessageBoxSetting` oder Programmstart fehlgeschlagen. | +| `OK` | WMI-Verbindung erfolgreich und `MSBTS_GroupSetting` liefert beide vollstaendigen Datenbankziele. | +| `UNKNOWN` | Namespace, `MSBTS_GroupSetting`, ein erforderliches Datenbankziel oder der Programmstart fehlgeschlagen. | Metriken: @@ -30,7 +30,7 @@ Metriken: Zweck: -- Ermittelt Management- und MessageBox-Datenbank ueber BizTalk-WMI. +- Ermittelt Management- und Master-MessageBox-Datenbank ueber `MSBTS_GroupSetting`. - Oeffnet mit der Windows-Identitaet des Checkmk-Agentprozesses eine integrierte SQL-Verbindung. - Fuehrt die harmlose Abfrage `SELECT 1` aus und schliesst die Verbindung sofort wieder. - Zeigt lokale Ausfuehrungsidentitaet und erwartete Netzwerkidentitaet an. @@ -93,6 +93,7 @@ Metriken: Zweck: - Ueberwacht 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. - Nicht gestartete Host Instances sind normalerweise betriebsrelevant. Status: @@ -134,7 +135,7 @@ Metriken: | Metrik | Bedeutung | | --- | --- | -| `biztalk_applications` | Anzahl erkannter BizTalk-Anwendungen. | +| `biztalk_applications` | Anzahl der per WMI erkannten Anwendungs-Buckets; `(unknown)` ist moeglich, da die dokumentierten Artefaktklassen keine verlaessliche Application-Property garantieren. | | `biztalk_receive_locations` | Gesamtzahl Receive Locations. | | `biztalk_receive_locations_disabled` | Deaktivierte Receive Locations. | | `biztalk_send_ports` | Gesamtzahl Send Ports. | @@ -171,10 +172,10 @@ Metriken: ## Optionale per-Application-Services -Wenn `EmitPerApplicationSuspensionServices=true` gesetzt ist, werden zusaetzliche Services fuer Anwendungen erzeugt, die suspendierte Instanzen haben: +Wenn `EmitPerApplicationSuspensionServices=true` gesetzt ist, werden zusaetzliche Services fuer Anwendungen erzeugt, die suspendierte Instanzen haben und deren Anwendung per WMI tatsaechlich zugeordnet werden konnte: ```text BizTalk Suspended ``` -Das ist fuer Anwendungsteams hilfreich, kann aber die Service-Anzahl erhoehen. Fuer den Basisausbau wird empfohlen, diese Option deaktiviert zu lassen. +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. diff --git a/docs/ExampleOutput.md b/docs/ExampleOutput.md index 3a00e4a..9c445a5 100644 --- a/docs/ExampleOutput.md +++ b/docs/ExampleOutput.md @@ -6,13 +6,16 @@ 0 "BizTalk Platform" - Self test OK. Checkmk local check output is valid. 0 "BizTalk SQL Access" biztalk_sql_targets_total=0;;;0 Self test OK. No SQL connection was opened. 0 "BizTalk Suspended Instances" biztalk_suspended_total=0;;;0 Self test OK. +0 "BizTalk Host Instances" biztalk_host_instances_total=0;;;0 Self test OK. No WMI query was executed. +0 "BizTalk Runtime Artifacts" biztalk_applications=0;;;0 Self test OK. No WMI query was executed. +0 "BizTalk Event Log" biztalk_eventlog_errors=0;;;0 Self test OK. No event log was read. ``` ## Gesunder BizTalk-Server ```text 0 "BizTalk Platform" - BizTalk WMI and platform data reachable, server=BIZTALK01, group=BizTalkMgmtDb, mgmt_db=SQL01\BizTalkMgmtDb, msgbox_db=SQL01\BizTalkMsgBoxDb -0 "BizTalk SQL Access" biztalk_sql_targets_total=2;;;0|biztalk_sql_targets_available=2;;;0|biztalk_sql_targets_failed=0;;1;0 SQL access targets=2, available=2, discovery_complete=True, execution_identity=NT AUTHORITY\SYSTEM, network_identity=\BIZTALK01$; databases=Management=SQL01\BizTalkMgmtDb:OK, MessageBox=SQL01\BizTalkMsgBoxDb:OK +0 "BizTalk SQL Access" biztalk_sql_targets_total=2;;;0|biztalk_sql_targets_available=2;;;0|biztalk_sql_targets_failed=0;;1;0 SQL access targets=2, available=2, discovery_complete=True, execution_identity=NT AUTHORITY\SYSTEM, network_identity=\BIZTALK01$; databases=Management=SQL01\BizTalkMgmtDb:OK, MasterMessageBox=SQL01\BizTalkMsgBoxDb:OK 0 "BizTalk Suspended Instances" biztalk_suspended_total=0;;;0|biztalk_suspended_resumable=0;1;;0|biztalk_suspended_nonresumable=0;;1;0 No suspended BizTalk service instances found. 0 "BizTalk Host Instances" biztalk_host_instances_total=4;;;0|biztalk_host_instances_started=4;;;0|biztalk_host_instances_stopped=0;;1;0|biztalk_host_instances_pending=0;1;;0|biztalk_host_instances_unknown=0;;1;0 Host instances total=4, started=4, stopped=0, pending=0, unknown=0 0 "BizTalk Runtime Artifacts" biztalk_applications=8;;;0|biztalk_receive_locations=24;;;0|biztalk_receive_locations_disabled=0;1;;0|biztalk_send_ports=31;;;0|biztalk_send_ports_started=31;;;0|biztalk_send_ports_inactive=0;1;;0|biztalk_send_ports_unknown=0;;1;0|biztalk_orchestrations=12;;;0|biztalk_orchestrations_started=12;;;0|biztalk_orchestrations_inactive=0;1;;0|biztalk_orchestrations_unknown=0;;1;0 Applications=8; receive_locations total=24, disabled=0; send_ports total=31, started=31, inactive=0, unknown=0; orchestrations total=12, started=12, inactive=0, unknown=0 @@ -28,7 +31,7 @@ ## SQL-Berechtigungsfehler ```text -3 "BizTalk SQL Access" biztalk_sql_targets_total=2;;;0|biztalk_sql_targets_available=0;;;0|biztalk_sql_targets_failed=2;;1;0 SQL access targets=2, available=0, discovery_complete=True, execution_identity=NT AUTHORITY\SYSTEM, network_identity=\BIZTALK01$; databases=Management=SQL01\BizTalkMgmtDb:FAILED, MessageBox=SQL01\BizTalkMsgBoxDb:FAILED; diagnostics: Sql/Permission [Management SQL01\BizTalkMgmtDb]: SQL Server hat die integrierte Anmeldung abgelehnt. Erwartete Netzwerkidentitaet: \BIZTALK01$. Massnahme: \BIZTALK01$ in die konfigurierte BizTalk-Operator-Gruppe aufnehmen; keine direkten BizTalk-DB-Rollen vergeben. Danach Kerberos-Tickets erneuern und den Agent-Dump wiederholen. Technik: SqlException Number=18456: Login failed for user +3 "BizTalk SQL Access" biztalk_sql_targets_total=2;;;0|biztalk_sql_targets_available=0;;;0|biztalk_sql_targets_failed=2;;1;0 SQL access targets=2, available=0, discovery_complete=True, execution_identity=NT AUTHORITY\SYSTEM, network_identity=\BIZTALK01$; databases=Management=SQL01\BizTalkMgmtDb:FAILED, MasterMessageBox=SQL01\BizTalkMsgBoxDb:FAILED; diagnostics: Sql/Permission [Management SQL01\BizTalkMgmtDb]: SQL Server hat die integrierte Anmeldung abgelehnt. Erwartete Netzwerkidentitaet: \BIZTALK01$. Massnahme: \BIZTALK01$ in die konfigurierte BizTalk-Operator-Gruppe aufnehmen; keine direkten BizTalk-DB-Rollen vergeben. Danach Kerberos-Tickets erneuern und den Agent-Dump wiederholen. Technik: SqlException Number=18456: Login failed for user ``` ## WMI-Berechtigungsfehler @@ -37,6 +40,12 @@ 3 "BizTalk Suspended Instances" - Suspendierte Instanzen konnten nicht verlaesslich gelesen werden. Ein leerer Datenbestand wird nicht als OK gewertet.; diagnostics: Wmi/Permission [MSBTS_ServiceInstance]: Zugriff auf MSBTS_ServiceInstance wurde im LocalSystem-Kontext verweigert. Massnahme: Agent-Dump pruefen und Computerkonto \BIZTALK01$ zunaechst der konfigurierten BizTalk-Operator-Gruppe zuordnen. Technik: UnauthorizedAccessException HRESULT=0x80070005: Access is denied ``` +## WMI-Schemafehler + +```text +3 "BizTalk Runtime Artifacts" - BizTalk Runtime Artifacts konnten nicht vollstaendig gelesen werden. Teilwerte werden nicht als OK gewertet.; diagnostics: Wmi/Schema [MSBTS_Orchestration]: WMI-Klasse oder WQL fuer MSBTS_Orchestration wird vom installierten BizTalk-Provider nicht unterstuetzt. Massnahme: Klassen- und Property-Namen gegen das installierte BizTalk-WMI-Schema und die Microsoft-Dokumentation pruefen. Rechteerhoehung behebt InvalidClass/InvalidQuery nicht. Technik: ManagementException HRESULT=0x80041010: Invalid class WQL={SELECT * FROM MSBTS_Orchestration} DurationMs=18 +``` + ## Fehlendes Deployment ```text diff --git a/docs/Integration.md b/docs/Integration.md index 4632e83..aa6a16f 100644 --- a/docs/Integration.md +++ b/docs/Integration.md @@ -97,7 +97,7 @@ Der direkte Start des Wrappers in einer administrativen Eingabeaufforderung ist Die Ausgabe muss die sechs stabilen `BizTalk ...` Services mit plausiblen Daten enthalten. `Access denied`, `UnauthorizedAccessException`, `Sql/Permission` und ein berechtigungsbedingtes `UNKNOWN` weisen auf eine fehlende Rollenzuordnung hin. -Die EXE klassifiziert WMI- und SQL-Probleme und schreibt zu jeder Diagnose `Massnahme:` und `Technik:`. Eine fehlgeschlagene erforderliche WMI-Abfrage erzeugt beim betroffenen Service immer `UNKNOWN`; fehlende Daten werden nicht als Nullbestand und damit nicht als `OK` ausgegeben. +Die EXE klassifiziert WMI- und SQL-Probleme und schreibt zu jeder Diagnose `Massnahme:` und `Technik:`. WMI-Schemafehler (`InvalidClass`/`InvalidQuery`) werden separat als `Wmi/Schema` ausgewiesen und duerfen nicht durch Rechteerweiterungen behandelt werden. Eine fehlgeschlagene erforderliche WMI-Abfrage erzeugt beim betroffenen Service immer `UNKNOWN`; fehlende Daten werden nicht als Nullbestand und damit nicht als `OK` ausgegeben. Fuer den produktiven Betrieb wird empfohlen, den Local Check alle 300 Sekunden asynchron auszufuehren. Dadurch erzeugen fehlende SQL-Rechte nicht bei jedem Agent-Abruf neue fehlgeschlagene Logins. In `%ProgramData%\checkmk\agent\check_mk.user.yml`: @@ -130,6 +130,7 @@ Entscheidungsmatrix: | --- | --- | | Alle sechs stabilen Services liefern plausible Werte | Keine Aenderung erforderlich. | | `root\MicrosoftBizTalkServer` ist nicht erreichbar | BizTalk-WMI-Provider, WMI-Dienst, Namespace und dessen ACL gezielt pruefen. | +| `Wmi/Schema` beziehungsweise `InvalidClass`/`InvalidQuery` | Klassen- und Property-Namen gegen das installierte BizTalk-WMI-Schema pruefen; keine Berechtigungen erweitern. | | `BizTalk SQL Access` meldet `Sql/Permission` | Maschinenkonto in die konfigurierte BizTalk-Operator-Gruppe aufnehmen, Kerberos erneuern und Agent-Dump wiederholen. | | `BizTalk SQL Access` meldet `Sql/Connectivity` | SQL-Server-/Instanzname, DNS, Dienst, TCP-Port und Firewall pruefen. | | `BizTalk SQL Access` meldet `Sql/Timeout` | SQL-/Netzwerkauslastung untersuchen; Timeout nur nach Ursachenanalyse anpassen. | @@ -149,6 +150,8 @@ Quellen: - Checkmk Local Checks: https://docs.checkmk.com/latest/en/localchecks.html - BizTalk Minimum Security User Rights: https://learn.microsoft.com/en-us/biztalk/core/minimum-security-user-rights - BizTalk Access Control and Data Security: https://learn.microsoft.com/en-us/biztalk/core/access-control-and-data-security +- BizTalk `MSBTS_GroupSetting`: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-groupsetting-wmi +- BizTalk WMI Core Server Classes: https://learn.microsoft.com/en-us/biztalk/core/technical-reference/core-server-classes - Microsoft LocalSystem und Computerkonten: https://learn.microsoft.com/en-us/entra/architecture/service-accounts-computer - Microsoft WMI Namespace Security: https://learn.microsoft.com/en-us/windows/win32/wmisdk/access-to-wmi-namespaces diff --git a/scripts/build-release.cmd b/scripts/build-release.cmd index f166915..0cb0811 100644 --- a/scripts/build-release.cmd +++ b/scripts/build-release.cmd @@ -7,6 +7,5 @@ if errorlevel 1 ( exit /b 1 ) -msbuild BizTalkCheckmkPulse.sln /p:Configuration=Release /p:Platform="Any CPU" /m /v:minimal +msbuild BizTalkCheckmkPulse.sln /p:Configuration=Release /p:Platform="Any CPU" /m:1 /v:minimal exit /b %ERRORLEVEL% - diff --git a/scripts/test-release.cmd b/scripts/test-release.cmd new file mode 100644 index 0000000..88e3f1f --- /dev/null +++ b/scripts/test-release.cmd @@ -0,0 +1,8 @@ +@echo off +setlocal + +call "%~dp0build-release.cmd" +if errorlevel 1 exit /b 1 + +"%~dp0..\tests\BizTalkCheckmkPulse.Tests\bin\Release\BizTalkCheckmkPulse.Tests.exe" +exit /b %ERRORLEVEL% diff --git a/src/BizTalkCheckmkPulse/BizTalkCheckmkPulse.csproj b/src/BizTalkCheckmkPulse/BizTalkCheckmkPulse.csproj index 71a4534..c561437 100644 --- a/src/BizTalkCheckmkPulse/BizTalkCheckmkPulse.csproj +++ b/src/BizTalkCheckmkPulse/BizTalkCheckmkPulse.csproj @@ -40,6 +40,7 @@ + diff --git a/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs b/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs index 92cfe16..bd87166 100644 --- a/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs +++ b/src/BizTalkCheckmkPulse/CheckmkLocalFormatter.cs @@ -62,6 +62,9 @@ namespace BizTalkCheckmkPulse yield return BuildLine(CheckState.Ok, _options.ServiceName("Platform"), "-", "Self test OK. Checkmk local check output is valid."); yield return BuildLine(CheckState.Ok, _options.ServiceName("SQL Access"), "biztalk_sql_targets_total=0;;;0", "Self test OK. No SQL connection was opened."); yield return BuildLine(CheckState.Ok, _options.ServiceName("Suspended Instances"), "biztalk_suspended_total=0;;;0", "Self test OK."); + yield return BuildLine(CheckState.Ok, _options.ServiceName("Host Instances"), "biztalk_host_instances_total=0;;;0", "Self test OK. No WMI query was executed."); + yield return BuildLine(CheckState.Ok, _options.ServiceName("Runtime Artifacts"), "biztalk_applications=0;;;0", "Self test OK. No WMI query was executed."); + yield return BuildLine(CheckState.Ok, _options.ServiceName("Event Log"), "biztalk_eventlog_errors=0;;;0", "Self test OK. No event log was read."); } /// @@ -96,7 +99,7 @@ namespace BizTalkCheckmkPulse AppendOptional(detail, "mgmt_db", JoinDb(result.Platform.ManagementDbServer, result.Platform.ManagementDbName)); AppendOptional(detail, "msgbox_db", JoinDb(result.Platform.MessageBoxDbServer, result.Platform.MessageBoxDbName)); - var diagnostics = SelectDiagnostics(result, DiagnosticArea.Wmi, "WMI namespace connection", "MSBTS_GroupSetting", "MSBTS_MessageBoxSetting"); + var diagnostics = SelectDiagnostics(result, DiagnosticArea.Wmi, "WMI namespace connection", "MSBTS_GroupSetting"); return BuildLine(state, _options.ServiceName("Platform"), "-", AppendDiagnostics(detail.ToString(), diagnostics)); } @@ -334,7 +337,10 @@ namespace BizTalkCheckmkPulse /// Checkmk-Zeilen je betroffener BizTalk-Anwendung. private IEnumerable FormatApplicationSuspensions(ProbeResult result) { - foreach (var group in result.SuspendedInstances.GroupBy(x => x.ApplicationName).OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase)) + foreach (var group in result.SuspendedInstances + .Where(x => !string.Equals(x.ApplicationName, "(unknown)", StringComparison.OrdinalIgnoreCase)) + .GroupBy(x => x.ApplicationName) + .OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase)) { var resumable = group.Count(x => x.Kind == SuspendedKind.Resumable); var nonresumable = group.Count(x => x.Kind == SuspendedKind.NonResumable); diff --git a/src/BizTalkCheckmkPulse/Models.cs b/src/BizTalkCheckmkPulse/Models.cs index f684462..3833d43 100644 --- a/src/BizTalkCheckmkPulse/Models.cs +++ b/src/BizTalkCheckmkPulse/Models.cs @@ -43,6 +43,7 @@ namespace BizTalkCheckmkPulse Connectivity, Timeout, Configuration, + Schema, Provider, Unexpected } diff --git a/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs b/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..1729e66 --- /dev/null +++ b/src/BizTalkCheckmkPulse/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("BizTalkCheckmkPulse.Tests")] diff --git a/src/BizTalkCheckmkPulse/SqlConnectivityProbe.cs b/src/BizTalkCheckmkPulse/SqlConnectivityProbe.cs index c3cbc21..d3f7f9b 100644 --- a/src/BizTalkCheckmkPulse/SqlConnectivityProbe.cs +++ b/src/BizTalkCheckmkPulse/SqlConnectivityProbe.cs @@ -35,7 +35,7 @@ namespace BizTalkCheckmkPulse } AddTarget(result, "Management", result.Platform.ManagementDbServer, result.Platform.ManagementDbName); - AddTarget(result, "MessageBox", result.Platform.MessageBoxDbServer, result.Platform.MessageBoxDbName); + AddTarget(result, "MasterMessageBox", result.Platform.MessageBoxDbServer, result.Platform.MessageBoxDbName); if (!result.Platform.PlatformDataAvailable) { @@ -46,7 +46,7 @@ namespace BizTalkCheckmkPulse Component = "BizTalk database discovery", Required = true, Summary = "SQL-Zielermittlung ist unvollstaendig, weil nicht alle erforderlichen BizTalk-Plattformklassen gelesen wurden.", - Action = "Zuerst den Service 'BizTalk Platform' sowie MSBTS_GroupSetting und MSBTS_MessageBoxSetting pruefen. Danach den Agent-Dump erneut ausfuehren." + Action = "Zuerst den Service 'BizTalk Platform' und MSBTS_GroupSetting pruefen. Danach den Agent-Dump erneut ausfuehren." }); } diff --git a/src/BizTalkCheckmkPulse/WmiBizTalkProbe.cs b/src/BizTalkCheckmkPulse/WmiBizTalkProbe.cs index dd1480c..ea0675a 100644 --- a/src/BizTalkCheckmkPulse/WmiBizTalkProbe.cs +++ b/src/BizTalkCheckmkPulse/WmiBizTalkProbe.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Management; using System.Runtime.InteropServices; @@ -23,6 +24,8 @@ namespace BizTalkCheckmkPulse private const int OrchestrationStopped = 3; private const int OrchestrationStarted = 4; private const int EAccessDenied = unchecked((int)0x80070005); + internal const string GroupSettingQuery = + "SELECT Name, MgmtDbServerName, MgmtDbName, SubscriptionDBServerName, SubscriptionDBName FROM MSBTS_GroupSetting"; private readonly MonitoringOptions _options; /// @@ -53,9 +56,14 @@ namespace BizTalkCheckmkPulse result.Platform.WmiConnected = true; result.Platform.PlatformDataAvailable = QueryPlatform(scope, result); - result.Platform.HostInstancesDataAvailable = QueryHostInstances(scope, result); - var applicationIndex = BuildApplicationIndex(scope, result); - result.Platform.RuntimeArtifactsDataAvailable = QueryApplicationRuntime(scope, result); + result.Platform.HostInstancesDataAvailable = QueryHostInstances(scope, result, server); + var applicationIndex = new Dictionary(StringComparer.OrdinalIgnoreCase); + result.Platform.RuntimeArtifactsDataAvailable = QueryApplicationRuntime(scope, result, applicationIndex); + if (_options.EmitPerApplicationSuspensionServices) + { + AddArtifactIndex(scope, result, applicationIndex, "MSBTS_ReceivePort"); + } + result.Platform.SuspendedInstancesDataAvailable = QuerySuspendedInstances(scope, result, applicationIndex); } @@ -74,7 +82,7 @@ namespace BizTalkCheckmkPulse } catch (Exception ex) when (IsExpectedWmiException(ex)) { - AddWmiDiagnostic(result, "WMI namespace connection", ex, true, true); + AddWmiDiagnostic(result, "WMI namespace connection", ex, true, true, null, null); return false; } } @@ -88,23 +96,19 @@ namespace BizTalkCheckmkPulse private bool QueryPlatform(ManagementScope scope, ProbeResult result) { var groupFound = false; - var groupAvailable = TryQuery(scope, result, "MSBTS_GroupSetting", "SELECT * FROM MSBTS_GroupSetting", item => + var groupAvailable = TryQuery(scope, result, "MSBTS_GroupSetting", GroupSettingQuery, item => { + if (groupFound) + { + return; + } + groupFound = true; result.Platform.GroupName = FirstNonEmpty(WmiHelpers.GetString(item, "Name"), WmiHelpers.GetString(item, "MgmtDbName")); - result.Platform.ManagementDbServer = FirstNonEmpty(WmiHelpers.GetString(item, "MgmtDbServerName"), WmiHelpers.GetString(item, "DBServerName")); - result.Platform.ManagementDbName = FirstNonEmpty(WmiHelpers.GetString(item, "MgmtDbName"), WmiHelpers.GetString(item, "DatabaseName")); - }, true); - - var messageBoxFound = false; - var messageBoxAvailable = TryQuery(scope, result, "MSBTS_MessageBoxSetting", "SELECT * FROM MSBTS_MessageBoxSetting", item => - { - messageBoxFound = true; - if (string.IsNullOrWhiteSpace(result.Platform.MessageBoxDbServer)) - { - result.Platform.MessageBoxDbServer = FirstNonEmpty(WmiHelpers.GetString(item, "DBServerName"), WmiHelpers.GetString(item, "ServerName")); - result.Platform.MessageBoxDbName = FirstNonEmpty(WmiHelpers.GetString(item, "DBName"), WmiHelpers.GetString(item, "DatabaseName")); - } + result.Platform.ManagementDbServer = WmiHelpers.GetString(item, "MgmtDbServerName"); + result.Platform.ManagementDbName = WmiHelpers.GetString(item, "MgmtDbName"); + result.Platform.MessageBoxDbServer = WmiHelpers.GetString(item, "SubscriptionDBServerName"); + result.Platform.MessageBoxDbName = WmiHelpers.GetString(item, "SubscriptionDBName"); }, true); if (groupAvailable && !groupFound) @@ -112,32 +116,33 @@ namespace BizTalkCheckmkPulse AddMissingDataDiagnostic(result, "MSBTS_GroupSetting", "Keine BizTalk-Gruppe gefunden."); } - if (messageBoxAvailable && !messageBoxFound) + var managementTargetComplete = HasDatabaseTarget( + result.Platform.ManagementDbServer, + result.Platform.ManagementDbName); + if (groupAvailable && groupFound && !managementTargetComplete) { - AddMissingDataDiagnostic(result, "MSBTS_MessageBoxSetting", "Keine BizTalk-MessageBox-Datenbank gefunden."); + AddMissingDataDiagnostic( + result, + "MSBTS_GroupSetting.ManagementDatabase", + "BizTalk Management-Datenbankziel ist in MSBTS_GroupSetting unvollstaendig."); } - return groupAvailable && groupFound && messageBoxAvailable && messageBoxFound; + var messageBoxTargetComplete = HasDatabaseTarget( + result.Platform.MessageBoxDbServer, + result.Platform.MessageBoxDbName); + if (groupAvailable && groupFound && !messageBoxTargetComplete) + { + AddMissingDataDiagnostic( + result, + "MSBTS_GroupSetting.SubscriptionDatabase", + "Master-MessageBox-Datenbankziel ist in MSBTS_GroupSetting unvollstaendig."); + } + + return groupAvailable && groupFound && managementTargetComplete && messageBoxTargetComplete; } /// - /// Erstellt einen Index, um suspendierte Instanzen BizTalk-Anwendungen zuzuordnen. - /// - /// Verbundener BizTalk-WMI-Scope. - /// Ergebnisobjekt fuer optionale Diagnosen. - /// Index aus moeglichen Artefaktschluesseln und Anwendungsnamen. - private Dictionary BuildApplicationIndex(ManagementScope scope, ProbeResult result) - { - var index = new Dictionary(StringComparer.OrdinalIgnoreCase); - AddArtifactIndex(scope, result, index, "MSBTS_SendPort"); - AddArtifactIndex(scope, result, index, "MSBTS_ReceivePort"); - AddArtifactIndex(scope, result, index, "MSBTS_ReceiveLocation"); - AddArtifactIndex(scope, result, index, "MSBTS_Orchestration"); - return index; - } - - /// - /// Fuegt die Schluessel einer WMI-Artefaktklasse zum Anwendungsindex hinzu. + /// Fuegt die Schluessel einer optionalen WMI-Artefaktklasse zum Anwendungsindex hinzu. /// /// Verbundener BizTalk-WMI-Scope. /// Ergebnisobjekt fuer optionale Diagnosen. @@ -147,14 +152,7 @@ namespace BizTalkCheckmkPulse { TryQuery(scope, result, className + " application index", "SELECT * FROM " + className, item => { - var app = GetApplicationName(item); - foreach (var key in CandidateKeys(item)) - { - if (!string.IsNullOrWhiteSpace(key) && !index.ContainsKey(key)) - { - index.Add(key, app); - } - } + AddArtifactIndexEntry(index, item); }, false); } @@ -163,16 +161,28 @@ namespace BizTalkCheckmkPulse /// /// Verbundener BizTalk-WMI-Scope. /// Zu aktualisierendes Ergebnisobjekt. + /// BizTalk-Server, dessen Host Instances bewertet werden. /// true, wenn die Pflichtabfrage erfolgreich war. - private bool QueryHostInstances(ManagementScope scope, ProbeResult result) + private bool QueryHostInstances(ManagementScope scope, ProbeResult result, string server) { - return TryQuery(scope, result, "MSBTS_HostInstance", "SELECT * FROM MSBTS_HostInstance", item => + return TryQuery( + scope, + result, + "MSBTS_HostInstance", + "SELECT Name, HostName, RunningServer, ServiceState FROM MSBTS_HostInstance", + item => { + var runningServer = WmiHelpers.GetString(item, "RunningServer"); + if (!string.IsNullOrWhiteSpace(runningServer) && !IsSameServer(runningServer, server)) + { + return; + } + result.HostInstances.Add(new HostInstanceState { InstanceName = FirstNonEmpty(WmiHelpers.GetString(item, "InstanceName"), WmiHelpers.GetString(item, "Name")), HostName = WmiHelpers.GetString(item, "HostName"), - RunningServer = WmiHelpers.GetString(item, "RunningServer"), + RunningServer = runningServer, ServiceState = WmiHelpers.GetInt32(item, "ServiceState", 0) }); }, true); @@ -183,13 +193,18 @@ namespace BizTalkCheckmkPulse /// /// Verbundener BizTalk-WMI-Scope. /// Zu aktualisierendes Ergebnisobjekt. + /// Best-Effort-Index fuer Suspensionsdetails. /// true, wenn alle drei Pflichtabfragen erfolgreich waren. - private bool QueryApplicationRuntime(ManagementScope scope, ProbeResult result) + private bool QueryApplicationRuntime( + ManagementScope scope, + ProbeResult result, + Dictionary applicationIndex) { var apps = new Dictionary(StringComparer.OrdinalIgnoreCase); var receiveLocationsAvailable = TryQuery(scope, result, "MSBTS_ReceiveLocation", "SELECT * FROM MSBTS_ReceiveLocation", item => { + AddArtifactIndexEntry(applicationIndex, item); var app = GetApplication(apps, GetApplicationName(item)); app.ReceiveLocationTotal++; if (WmiHelpers.GetBoolean(item, "IsDisabled", false)) @@ -200,6 +215,7 @@ namespace BizTalkCheckmkPulse var sendPortsAvailable = TryQuery(scope, result, "MSBTS_SendPort", "SELECT * FROM MSBTS_SendPort", item => { + AddArtifactIndexEntry(applicationIndex, item); var app = GetApplication(apps, GetApplicationName(item)); app.SendPortTotal++; switch (WmiHelpers.GetInt32(item, "Status", 0)) @@ -221,6 +237,7 @@ namespace BizTalkCheckmkPulse var orchestrationsAvailable = TryQuery(scope, result, "MSBTS_Orchestration", "SELECT * FROM MSBTS_Orchestration", item => { + AddArtifactIndexEntry(applicationIndex, item); var app = GetApplication(apps, GetApplicationName(item)); app.OrchestrationTotal++; switch (WmiHelpers.GetInt32(item, "OrchestrationStatus", 0)) @@ -285,6 +302,7 @@ namespace BizTalkCheckmkPulse /// true, wenn die Abfrage vollstaendig ausgefuehrt wurde. private bool TryQuery(ManagementScope scope, ProbeResult result, string label, string queryText, Action action, bool required) { + var stopwatch = Stopwatch.StartNew(); try { ForEachObject(scope, queryText, action); @@ -292,7 +310,7 @@ namespace BizTalkCheckmkPulse } catch (Exception ex) when (IsExpectedWmiException(ex)) { - AddWmiDiagnostic(result, label, ex, required, false); + AddWmiDiagnostic(result, label, ex, required, false, queryText, stopwatch.Elapsed); return false; } } @@ -344,6 +362,28 @@ namespace BizTalkCheckmkPulse return app; } + /// + /// Nimmt nur tatsaechlich vorhandene Anwendungszuordnungen in den Best-Effort-Index auf. + /// + /// Zu erweiternder Index. + /// BizTalk-Artefakt aus WMI. + private static void AddArtifactIndexEntry(Dictionary index, ManagementBaseObject item) + { + var app = GetApplicationName(item); + if (string.Equals(app, UnknownApplication, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + foreach (var key in CandidateKeys(item)) + { + if (!string.IsNullOrWhiteSpace(key) && !index.ContainsKey(key)) + { + index.Add(key, app); + } + } + } + /// /// Ermittelt den Anwendungsnamen direkt oder ueber den Artefaktindex. /// @@ -406,6 +446,32 @@ namespace BizTalkCheckmkPulse return values == null ? null : values.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x)); } + /// + /// Prueft, ob Server und Datenbank eines SQL-Ziels vorhanden sind. + /// + private static bool HasDatabaseTarget(string server, string database) + { + return !string.IsNullOrWhiteSpace(server) && !string.IsNullOrWhiteSpace(database); + } + + /// + /// Vergleicht kurzen Namen und FQDN eines BizTalk-Servers ohne WQL-Namensfilter. + /// + internal static bool IsSameServer(string left, string right) + { + return string.Equals( + NormalizeServerName(left), + NormalizeServerName(right), + StringComparison.OrdinalIgnoreCase); + } + + private static string NormalizeServerName(string value) + { + var normalized = (value ?? string.Empty).Trim().TrimStart('\\'); + var dotIndex = normalized.IndexOf('.'); + return dotIndex > 0 ? normalized.Substring(0, dotIndex) : normalized; + } + /// /// Prueft, ob eine Exception aus dem erwarteten WMI-/COM-Fehlerbereich stammt. /// @@ -426,9 +492,29 @@ namespace BizTalkCheckmkPulse /// Ausgeloeste Exception. /// Kennzeichen fuer eine Pflichtabfrage. /// Kennzeichen fuer Fehler beim Namespace-Verbindungsaufbau. - private static void AddWmiDiagnostic(ProbeResult result, string component, Exception exception, bool required, bool connectionFailure) + private static void AddWmiDiagnostic( + ProbeResult result, + string component, + Exception exception, + bool required, + bool connectionFailure, + string queryText, + TimeSpan? duration) { var category = ClassifyWmiException(exception); + var technicalDetails = exception.GetType().Name + + " HRESULT=0x" + exception.HResult.ToString("X8") + + ": " + exception.Message; + if (!string.IsNullOrWhiteSpace(queryText)) + { + technicalDetails += " WQL={" + queryText + "}"; + } + + if (duration.HasValue) + { + technicalDetails += " DurationMs=" + Math.Round(duration.Value.TotalMilliseconds); + } + result.Diagnostics.Add(new ProbeDiagnostic { Area = DiagnosticArea.Wmi, @@ -437,7 +523,7 @@ namespace BizTalkCheckmkPulse Required = required, Summary = BuildWmiSummary(category, component, connectionFailure), Action = BuildWmiAction(category, connectionFailure), - TechnicalDetails = exception.GetType().Name + " HRESULT=0x" + exception.HResult.ToString("X8") + ": " + exception.Message + TechnicalDetails = technicalDetails }); } @@ -477,12 +563,25 @@ namespace BizTalkCheckmkPulse } if (managementException != null - && (managementException.ErrorCode == ManagementStatus.InvalidNamespace - || managementException.ErrorCode == ManagementStatus.InvalidClass)) + && managementException.ErrorCode == ManagementStatus.InvalidNamespace) { return DiagnosticCategory.Configuration; } + if (managementException != null + && (managementException.ErrorCode == ManagementStatus.InvalidClass + || managementException.ErrorCode == ManagementStatus.InvalidProperty + || managementException.ErrorCode == ManagementStatus.InvalidQuery + || managementException.ErrorCode == ManagementStatus.UnparsableQuery)) + { + return DiagnosticCategory.Schema; + } + + if (managementException != null && managementException.ErrorCode == ManagementStatus.Timedout) + { + return DiagnosticCategory.Timeout; + } + var message = exception.Message ?? string.Empty; if (ContainsAny(message, "timed out", "timeout", "Zeitlimit")) { @@ -513,7 +612,9 @@ namespace BizTalkCheckmkPulse case DiagnosticCategory.Configuration: return connectionFailure ? "BizTalk-WMI-Namespace ist nicht vorhanden oder nicht korrekt registriert." - : "BizTalk-WMI-Klasse " + component + " ist nicht vorhanden oder nicht korrekt registriert."; + : "BizTalk-WMI-Konfiguration fuer " + component + " ist unvollstaendig."; + case DiagnosticCategory.Schema: + return "WMI-Klasse oder WQL fuer " + component + " wird vom installierten BizTalk-Provider nicht unterstuetzt."; case DiagnosticCategory.Timeout: return "WMI-Abfrage fuer " + component + " hat das konfigurierte Zeitlimit ueberschritten."; case DiagnosticCategory.Connectivity: @@ -539,6 +640,8 @@ namespace BizTalkCheckmkPulse : "Agent-Dump pruefen und Computerkonto \\" + Environment.MachineName + "$ zunaechst der konfigurierten BizTalk-Operator-Gruppe zuordnen."; case DiagnosticCategory.Configuration: return "BizTalk-WMI-Provider/Namespace auf dem BizTalk-Server pruefen und gegebenenfalls mit dem BizTalk-Setup reparieren."; + case DiagnosticCategory.Schema: + return "Klassen- und Property-Namen gegen das installierte BizTalk-WMI-Schema und die Microsoft-Dokumentation pruefen. Rechteerhoehung behebt InvalidClass/InvalidQuery nicht."; case DiagnosticCategory.Timeout: return "WMI-Dienst, BizTalk-/SQL-Auslastung und Event Logs pruefen; Timeout erst danach erhoehen."; case DiagnosticCategory.Connectivity: diff --git a/tests/BizTalkCheckmkPulse.Tests/BizTalkCheckmkPulse.Tests.csproj b/tests/BizTalkCheckmkPulse.Tests/BizTalkCheckmkPulse.Tests.csproj new file mode 100644 index 0000000..a9a5d74 --- /dev/null +++ b/tests/BizTalkCheckmkPulse.Tests/BizTalkCheckmkPulse.Tests.csproj @@ -0,0 +1,44 @@ + + + + + Debug + AnyCPU + {E219C45A-CC95-4F45-9B6F-8244CBAEE35A} + Exe + BizTalkCheckmkPulse.Tests + BizTalkCheckmkPulse.Tests + v4.7.2 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + 4 + + + pdbonly + true + bin\Release\ + TRACE + 4 + + + + + + + + + + + {A4D4D050-9EA7-4A71-B510-7D9D699B9F38} + BizTalkCheckmkPulse + + + + diff --git a/tests/BizTalkCheckmkPulse.Tests/Program.cs b/tests/BizTalkCheckmkPulse.Tests/Program.cs new file mode 100644 index 0000000..4daa469 --- /dev/null +++ b/tests/BizTalkCheckmkPulse.Tests/Program.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace BizTalkCheckmkPulse.Tests +{ + internal static class Program + { + private static readonly List Failures = new List(); + + private static int Main() + { + Run("SelfTestEmitsAllStableServices", SelfTestEmitsAllStableServices); + Run("UnavailableSourcesAreUnknown", UnavailableSourcesAreUnknown); + Run("UnknownApplicationDoesNotCreateService", UnknownApplicationDoesNotCreateService); + Run("KnownApplicationCreatesService", KnownApplicationCreatesService); + Run("ServerNamesAreComparedWithoutWql", ServerNamesAreComparedWithoutWql); + Run("GroupSettingQueryUsesDocumentedSchema", GroupSettingQueryUsesDocumentedSchema); + + if (Failures.Count == 0) + { + Console.WriteLine("PASS: 6 tests"); + return 0; + } + + foreach (var failure in Failures) + { + Console.Error.WriteLine("FAIL: " + failure); + } + + return 1; + } + + private static void SelfTestEmitsAllStableServices() + { + var lines = new CheckmkLocalFormatter(new MonitoringOptions()).FormatSelfTest().ToArray(); + AssertEqual(6, lines.Length, "self-test line count"); + AssertEqual(6, lines.Distinct(StringComparer.Ordinal).Count(), "unique self-test lines"); + Assert(lines.All(x => x.StartsWith("0 \"BizTalk ", StringComparison.Ordinal)), "every self-test line must be OK"); + Assert(lines.Any(x => x.Contains("\"BizTalk Event Log\"")), "Event Log service missing"); + } + + private static void UnavailableSourcesAreUnknown() + { + var lines = new CheckmkLocalFormatter(new MonitoringOptions()).Format(new ProbeResult()).Take(6).ToArray(); + AssertEqual(6, lines.Length, "stable service count"); + Assert(lines.All(x => x.StartsWith("3 \"BizTalk ", StringComparison.Ordinal)), "unavailable sources must be UNKNOWN"); + } + + private static void UnknownApplicationDoesNotCreateService() + { + var options = new MonitoringOptions { EmitPerApplicationSuspensionServices = true }; + var result = CreateSuspensionResult("(unknown)"); + var lines = new CheckmkLocalFormatter(options).Format(result).ToArray(); + AssertEqual(6, lines.Length, "unknown application must not create a dynamic service"); + } + + private static void KnownApplicationCreatesService() + { + var options = new MonitoringOptions { EmitPerApplicationSuspensionServices = true }; + var result = CreateSuspensionResult("Orders"); + var lines = new CheckmkLocalFormatter(options).Format(result).ToArray(); + AssertEqual(7, lines.Length, "known application should create one dynamic service"); + Assert(lines.Any(x => x.Contains("\"BizTalk Suspended Orders\"")), "known application service missing"); + } + + private static void ServerNamesAreComparedWithoutWql() + { + Assert(WmiBizTalkProbe.IsSameServer("BIZTALK01.corp.example", "biztalk01"), "FQDN and short name must match"); + Assert(WmiBizTalkProbe.IsSameServer(@"\\BIZ-TALK+01", "biz-talk+01.example"), "special characters must be compared client-side"); + Assert(!WmiBizTalkProbe.IsSameServer("BIZTALK01", "BIZTALK02"), "different servers must not match"); + } + + private static void GroupSettingQueryUsesDocumentedSchema() + { + var query = WmiBizTalkProbe.GroupSettingQuery; + Assert(query.Contains("FROM MSBTS_GroupSetting"), "MSBTS_GroupSetting query missing"); + Assert(query.Contains("SubscriptionDBServerName"), "master MessageBox server property missing"); + Assert(query.Contains("SubscriptionDBName"), "master MessageBox database property missing"); + Assert(query.IndexOf("MessageBoxSetting", StringComparison.OrdinalIgnoreCase) < 0, "unsupported MessageBoxSetting class present"); + } + + private static ProbeResult CreateSuspensionResult(string applicationName) + { + var result = new ProbeResult(); + result.Platform.SuspendedInstancesDataAvailable = true; + result.SuspendedInstances.Add(new SuspendedInstance + { + ApplicationName = applicationName, + ServiceName = "TestService", + Kind = SuspendedKind.Resumable + }); + return result; + } + + private static void Run(string name, Action test) + { + try + { + test(); + } + catch (Exception ex) + { + Failures.Add(name + ": " + ex.Message); + } + } + + private static void Assert(bool condition, string message) + { + if (!condition) + { + throw new InvalidOperationException(message); + } + } + + private static void AssertEqual(int expected, int actual, string label) + { + if (expected != actual) + { + throw new InvalidOperationException(label + ": expected " + expected + ", actual " + actual); + } + } + } +}