diff --git a/BizTalkCheckmkPulse.sln b/BizTalkCheckmkPulse.sln index b44d8e4..1c7087a 100644 --- a/BizTalkCheckmkPulse.sln +++ b/BizTalkCheckmkPulse.sln @@ -6,6 +6,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkCheckmkPulse", "src\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkCheckmkPulse.Tests", "tests\BizTalkCheckmkPulse.Tests\BizTalkCheckmkPulse.Tests.csproj", "{E219C45A-CC95-4F45-9B6F-8244CBAEE35A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkCheckmkPulse.Setup", "src\BizTalkCheckmkPulse.Setup\BizTalkCheckmkPulse.Setup.csproj", "{764AC43A-26D8-43C1-9121-13FC8A9CC8B1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkCheckmkPulse.Packager", "src\BizTalkCheckmkPulse.Packager\BizTalkCheckmkPulse.Packager.csproj", "{B35A04E0-253B-4CEB-9033-F5109F3B9F82}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -20,6 +24,14 @@ Global {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 + {764AC43A-26D8-43C1-9121-13FC8A9CC8B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {764AC43A-26D8-43C1-9121-13FC8A9CC8B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {764AC43A-26D8-43C1-9121-13FC8A9CC8B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {764AC43A-26D8-43C1-9121-13FC8A9CC8B1}.Release|Any CPU.Build.0 = Release|Any CPU + {B35A04E0-253B-4CEB-9033-F5109F3B9F82}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B35A04E0-253B-4CEB-9033-F5109F3B9F82}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B35A04E0-253B-4CEB-9033-F5109F3B9F82}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B35A04E0-253B-4CEB-9033-F5109F3B9F82}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Dokumentation.md b/Dokumentation.md index 1cb96d4..a78a0d9 100644 --- a/Dokumentation.md +++ b/Dokumentation.md @@ -16,7 +16,7 @@ Verbindliche Randbedingungen: - Der Agentpfad fuehrt niemals WMI- oder SQL-Abfragen aus. - Fehler muessen als gueltige Checkmk-`UNKNOWN`-Services sichtbar werden. - Schreiben und Lesen duerfen nie einen halben Snapshot exponieren. -- Laufzeitcode benoetigt keine PowerShell und keine BizTalk-DLL-Referenz. +- Installation und Laufzeit benoetigen keine PowerShell und keine BizTalk-DLL-Referenz. - Installation, ACL und Scheduled Task muessen reproduzierbar sein. ## 2. Ausloeser der Architekturanderung @@ -377,29 +377,30 @@ scripts\test-release.cmd scripts\package-release.cmd ``` +Der Release-Build der Visual-Studio-2019-Solution baut Anwendung, grafischen +Installer und Packaging-Werkzeug. Danach liegen der Paketordner +`artifacts\BizTalkCheckmkPulse-Setup` und das direkt transportierbare +`artifacts\BizTalkCheckmkPulse-Setup.zip` bereit. + ### 8.2 Normales Servicekonto -```powershell -.\Install-BizTalkCheckmkPulse.ps1 ` - -CollectorAccount 'BEW\svc_biztalk_cmk' ` - -EnvironmentName ACC -``` +Das ZIP wird auf dem Zielserver vollstaendig entpackt. Danach `Setup.exe` als +Administrator starten, Konto im Format `DOMAIN\Benutzer` (zum Beispiel +`BEW\t231bizmon`), Kennwort und Umgebung eintragen und die Installation +starten. Der Installer muss als lokaler Administrator laufen. Er vergibt keine AD-/BizTalk-/SQL-Rechte; diese bleiben getrennte administrative Freigaben. -Der installierte Task selbst laeuft mit `RunLevel Limited`. Der Installer fragt -das Kennwort mit `Get-Credential` ab; es wird nicht in Config oder Log +Der installierte Task selbst laeuft mit `RunLevel Limited`. Der Installer +uebergibt das Kennwort direkt an die Windows-Task-Scheduler-COM-Schnittstelle; +es wird weder in einer Prozesskommandozeile noch in Config oder Log geschrieben. Das Konto benoetigt `Log on as a batch job`. ### 8.3 Optionales gMSA -```powershell -.\Install-BizTalkCheckmkPulse.ps1 ` - -CollectorAccount 'BEW\svc_biztalk_cmk$' ` - -Gmsa ` - -EnvironmentName ACC -``` +Im Installer die gMSA-Checkbox aktivieren und ein auf `$` endendes Konto wie +`BEW\svc_biztalk_cmk$` eingeben. Ein Kennwort wird dann nicht abgefragt. ### 8.4 Checkmk @@ -451,12 +452,12 @@ aelter als `LogRetentionDays`. ### 10.1 Grundpruefung -```powershell -Get-ScheduledTaskInfo -TaskName 'BizTalk Checkmk Pulse Provider' -Get-Item "$env:ProgramData\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot" -Get-Content "$env:ProgramData\BizTalkCheckmkPulse\logs\*.log" -Tail 100 -& "$env:ProgramFiles\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume -& "C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump +```cmd +schtasks.exe /query /tn "BizTalk Checkmk Pulse Provider" /v /fo list +dir "%ProgramData%\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot" +type "%ProgramData%\BizTalkCheckmkPulse\logs\biztalk-checkmk-pulse-*.log" +"%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume +"C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump ``` ### 10.2 Kein Snapshot @@ -615,32 +616,17 @@ Erst nach ACC-Abnahme erfolgt der gestufte Rollout nach DEV/TST/PRD. ## 13. Deinstallation und Rollback -```powershell -.\Uninstall-BizTalkCheckmkPulse.ps1 -``` - -`-KeepRuntimeData` erhaelt Logs und Snapshot. Das Skript entfernt Task, -Wrapper und Programmdateien, aber keine AD-Gruppenmitgliedschaft. Die -Berechtigung wird separat nach Change-Verfahren entzogen. +`Setup.exe` aus dem entpackten Paket erneut starten und **Deinstallieren** +waehlen. Der Installer entfernt Task, Wrapper und Programmdateien, behaelt +Logs und Snapshot aber absichtlich bei. Die AD-Gruppenmitgliedschaft wird +nicht geaendert und separat nach Change-Verfahren entzogen. Rollback auf die alte direkte `LocalSystem`-Architektur ist nicht empfohlen. Falls kurzfristig notwendig, muss Security die Maschinenkonto-Berechtigung explizit freigeben; der Checkmk-Agentdienst selbst darf nicht auf das Collector-Konto umgestellt werden. -## 14. Transport per certutil - -Die Source-Uebergabe besteht aus ZIP plus Base64-TXT: - -```cmd -certutil -decode .zip.b64.txt .zip -certutil -hashfile .zip SHA256 -tar -xf .zip -``` - -ZIP und Base64 werden erst nach dem Git-Commit aus genau diesem Commit erzeugt. - -## 15. Referenzen +## 14. Referenzen - https://learn.microsoft.com/en-us/biztalk/core/managing-biztalk-server-security - https://learn.microsoft.com/en-us/biztalk/core/windows-groups-and-user-accounts-in-biztalk-server diff --git a/README.md b/README.md index 9bdb2c2..b431e1c 100644 --- a/README.md +++ b/README.md @@ -164,22 +164,29 @@ scripts\test-release.cmd scripts\package-release.cmd ``` -Das Paket wird unter `artifacts\BizTalkCheckmkPulse-deploy` erzeugt: +Jeder Release-Build der Solution erzeugt automatisch den fertigen Paketordner +und das transportierbare ZIP: ```text -BizTalkCheckmkPulse-deploy\ - Install-BizTalkCheckmkPulse.ps1 - Uninstall-BizTalkCheckmkPulse.ps1 - biztalk_checkmk_pulse.cmd - application\ - BizTalkCheckmkPulse.exe - BizTalkCheckmkPulse.exe.config +artifacts\ + BizTalkCheckmkPulse-Setup.zip + BizTalkCheckmkPulse-Setup\ + Setup.exe + INSTALLATION.md + biztalk_checkmk_pulse.cmd + application\ + BizTalkCheckmkPulse.exe + BizTalkCheckmkPulse.exe.config ``` +Das Packaging-Projekt ist Teil von `BizTalkCheckmkPulse.sln`. Ein normaler +`Release`-Build in Visual Studio 2019 genuegt daher; ein separates +PowerShell-/WiX-/MSI-Tooling wird nicht benoetigt. + Format-Self-Test ohne WMI, SQL oder Event Log: ```cmd -artifacts\BizTalkCheckmkPulse-deploy\application\BizTalkCheckmkPulse.exe --self-test +artifacts\BizTalkCheckmkPulse-Setup\application\BizTalkCheckmkPulse.exe --self-test ``` Erwartet werden exakt neun `OK`-Zeilen. Die Regressionstests pruefen @@ -191,16 +198,7 @@ Windows-/BizTalk-Laufzeitvalidierung. ## Berechtigung vorbereiten Die exakte Read-Only-Gruppe wird in der BizTalk Administration Console unter -den Eigenschaften der BizTalk-Gruppe abgelesen. Ein bereits berechtigtes Konto -kann sie alternativ ermitteln: - -```powershell -Get-CimInstance ` - -Namespace root/MicrosoftBizTalkServer ` - -ClassName MSBTS_GroupSetting | - Select-Object Name, BizTalkReadOnlyUserGroup, BizTalkOperatorGroup, - MgmtDbServerName, MgmtDbName -``` +den Eigenschaften der BizTalk-Gruppe abgelesen. Ein AD-Administrator nimmt das neue Provider-Konto in `BizTalkReadOnlyUserGroup` auf. Nach AD-Replikation muss das Servicekonto durch @@ -215,22 +213,23 @@ fuer das Provider-Konto. ## Installation mit normalem Servicekonto -```powershell -Set-Location C:\Temp\BizTalkCheckmkPulse-deploy -.\Install-BizTalkCheckmkPulse.ps1 ` - -CollectorAccount 'BEW\svc_biztalk_cmk' ` - -EnvironmentName ACC -``` +1. `BizTalkCheckmkPulse-Setup.zip` auf dem Zielserver vollstaendig entpacken. +2. `Setup.exe` starten und die UAC-Abfrage bestaetigen. +3. Collector-Konto, zum Beispiel `BEW\t231bizmon`, Kennwort und Umgebung + eingeben. +4. **Installieren / aktualisieren** waehlen. -Der Installer fragt das Kennwort ueber `Get-Credential` ab und speichert es -durch die Windows-Aufgabenplanung. Das Kennwort steht weder in der -Konfigurationsdatei noch in den Logs. +Der Installer uebergibt das Kennwort direkt ueber die Windows-Task-Scheduler- +COM-Schnittstelle. Es steht weder in einer Prozesskommandozeile noch in der +Konfigurationsdatei oder in Logs. Der Backslash in `DOMAIN\Benutzer` wird als +normaler Bestandteil des Kontonamens verarbeitet. Wenn das Kennwort rotiert oder ablaeuft, muss es im Scheduled Task aktualisiert -werden. Bis dahin wird der Snapshot nach 180 Sekunden stale und Checkmk zeigt -alle neun Services als `UNKNOWN`. +werden. Dazu `Setup.exe` mit dem neuen Kennwort erneut ausfuehren. Bis dahin +wird der Snapshot nach 180 Sekunden stale und Checkmk zeigt alle acht Services +als `UNKNOWN`. -Ein gMSA kann weiterhin optional mit `-Gmsa` installiert werden; die +Ein gMSA kann ueber die Checkbox ohne Kennworteingabe installiert werden; die produktive Standardbeschreibung geht vom normalen Servicekonto aus. Der Installer: @@ -245,48 +244,34 @@ Der Installer: `IgnoreNew`, fuenf Minuten Laufzeitlimit und zwei Wiederholungen, 6. fuehrt den Self-Test aus und startet den Provider einmalig. -PowerShell wird nur fuer Installation und Betriebsdiagnose verwendet. Der -minuetliche Provider und der Checkmk-Consumer sind .NET-/CMD-Laufzeitcode und -haengen nicht von der PowerShell Execution Policy ab. +Installation, Update, Deinstallation und Laufzeit verwenden keine PowerShell. ## Verifikation auf dem Server Task und letzter Lauf: -```powershell -Get-ScheduledTask -TaskName 'BizTalk Checkmk Pulse Provider' | - Select-Object TaskName, State -Get-ScheduledTaskInfo -TaskName 'BizTalk Checkmk Pulse Provider' | - Select-Object LastRunTime, LastTaskResult, NextRunTime +```cmd +schtasks.exe /query /tn "BizTalk Checkmk Pulse Provider" /v /fo list ``` Provider-Log: -```powershell -Get-ChildItem "$env:ProgramData\BizTalkCheckmkPulse\logs" | - Sort-Object LastWriteTime -Descending | - Select-Object -First 3 Name, Length, LastWriteTime - -Get-Content ` - "$env:ProgramData\BizTalkCheckmkPulse\logs\biztalk-checkmk-pulse-*.log" ` - -Tail 100 +```cmd +dir /o-d "%ProgramData%\BizTalkCheckmkPulse\logs" +type "%ProgramData%\BizTalkCheckmkPulse\logs\biztalk-checkmk-pulse-*.log" ``` Snapshot und Consumer: -```powershell -Get-Item ` - "$env:ProgramData\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot" | - Select-Object FullName, Length, LastWriteTimeUtc - -& "$env:ProgramFiles\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume +```cmd +dir "%ProgramData%\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot" +"%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume ``` Verbindlicher Test im echten `LocalSystem`-Kontext: -```powershell -& "C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump | - Select-String -Pattern "BizTalk|UNKNOWN|Snapshot|Permission" -Context 0,1 +```cmd +"C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump ``` Danach in Checkmk eine Service Discovery ausfuehren, die neun Services @@ -386,33 +371,12 @@ Ausfuehrliche Betriebs- und Architekturdetails stehen in ## Deinstallation -```powershell -.\Uninstall-BizTalkCheckmkPulse.ps1 -``` - -Das Skript fragt vor dem Entfernen. Mit `-KeepRuntimeData` bleiben Snapshot und -Logs erhalten: - -```powershell -.\Uninstall-BizTalkCheckmkPulse.ps1 -KeepRuntimeData -``` +`Setup.exe` erneut starten und **Deinstallieren** waehlen. Snapshot und Logs +bleiben dabei unter `%ProgramData%\BizTalkCheckmkPulse` erhalten. Die AD-Gruppenmitgliedschaft des Provider-Kontos wird bewusst nicht automatisch geaendert und muss separat durch AD-/BizTalk-Administration entfernt werden. -## Certutil-Transport - -Zu jeder Uebergabe wird ein Source-ZIP und eine certutil-kompatible -Base64-Textdatei erzeugt. Auf Windows: - -```cmd -certutil -decode biztalk-checkmk-pulse-source--.zip.b64.txt biztalk-checkmk-pulse-source.zip -certutil -hashfile biztalk-checkmk-pulse-source.zip SHA256 -tar -xf biztalk-checkmk-pulse-source.zip -``` - -Die konkrete Datei und SHA-256-Summe werden bei der Uebergabe genannt. - ## Quellen - Microsoft: BizTalk `MSBTS_GroupSetting.BizTalkReadOnlyUserGroup` diff --git a/deployment/INSTALLATION.md b/deployment/INSTALLATION.md new file mode 100644 index 0000000..52541bf --- /dev/null +++ b/deployment/INSTALLATION.md @@ -0,0 +1,21 @@ +# Installation auf dem BizTalk-Server + +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 + `BEW\t231bizmon`. +4. Kennwort und Umgebung eingeben und **Installieren / aktualisieren** waehlen. +5. Im Aufgabenplaner den Task `BizTalk Checkmk Pulse Provider` und danach den + Checkmk-Agent-Dump kontrollieren. + +PowerShell wird fuer Installation, Update, Deinstallation und Laufzeit nicht +benoetigt. Das Kennwort wird direkt an die Windows-Aufgabenplanung uebergeben +und weder in einer Datei noch in einer Prozesskommandozeile abgelegt. + +Der Installer vergibt keine AD-, BizTalk- oder SQL-Berechtigungen. Das Konto +muss separat Mitglied der fuer die BizTalk-Gruppe konfigurierten Read-Only- +Gruppe sein und lokal das Recht `Log on as a batch job` besitzen. + +Bei einer Deinstallation bleiben Snapshot und Logs absichtlich unter +`%ProgramData%\BizTalkCheckmkPulse` erhalten. Sie koennen nach der +Betriebsfreigabe manuell entfernt werden. diff --git a/deployment/Install-BizTalkCheckmkPulse.ps1 b/deployment/Install-BizTalkCheckmkPulse.ps1 deleted file mode 100644 index 190e3d7..0000000 --- a/deployment/Install-BizTalkCheckmkPulse.ps1 +++ /dev/null @@ -1,171 +0,0 @@ -[CmdletBinding()] -param( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string]$CollectorAccount, - - [switch]$Gmsa, - - [ValidateSet("", "ACC", "DEV", "TST", "PRD")] - [string]$EnvironmentName = "", - - [string]$InstallDirectory = "$env:ProgramFiles\BizTalkCheckmkPulse", - - [string]$CheckmkLocalDirectory = "$env:ProgramData\checkmk\agent\local" -) - -$ErrorActionPreference = "Stop" -Set-StrictMode -Version 2.0 - -$taskName = "BizTalk Checkmk Pulse Provider" -$runtimeRoot = Join-Path $env:ProgramData "BizTalkCheckmkPulse" -$dataDirectory = Join-Path $runtimeRoot "data" -$logDirectory = Join-Path $runtimeRoot "logs" -$sourceApplication = Join-Path $PSScriptRoot "application" -$sourceWrapper = Join-Path $PSScriptRoot "biztalk_checkmk_pulse.cmd" -$targetExe = Join-Path $InstallDirectory "BizTalkCheckmkPulse.exe" -$targetConfig = "$targetExe.config" -$targetWrapper = Join-Path $CheckmkLocalDirectory "biztalk_checkmk_pulse.cmd" - -function Assert-Administrator { - $identity = [Security.Principal.WindowsIdentity]::GetCurrent() - $principal = New-Object Security.Principal.WindowsPrincipal($identity) - if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - throw "Die Installation muss in einer administrativen Windows PowerShell ausgefuehrt werden." - } -} - -function Invoke-Icacls { - param( - [Parameter(Mandatory = $true)][string]$Path, - [Parameter(Mandatory = $true)][string[]]$Arguments - ) - - & icacls.exe $Path @Arguments | Out-Host - if ($LASTEXITCODE -ne 0) { - throw "icacls ist fuer '$Path' mit Exitcode $LASTEXITCODE fehlgeschlagen." - } -} - -Assert-Administrator - -if (-not (Test-Path -LiteralPath $sourceApplication -PathType Container)) { - throw "Quellverzeichnis fehlt: $sourceApplication" -} -if (-not (Test-Path -LiteralPath $sourceWrapper -PathType Leaf)) { - throw "Checkmk-Wrapper fehlt: $sourceWrapper" -} - -if ($Gmsa -and -not $CollectorAccount.EndsWith('$')) { - throw "Ein gMSA-Kontoname muss mit '$' enden, z.B. DOMAIN\svc_biztalk_cmk$." -} - -Write-Host "Installiere Programmdateien nach $InstallDirectory ..." -New-Item -ItemType Directory -Path $InstallDirectory -Force | Out-Null -Copy-Item -Path (Join-Path $sourceApplication "*") -Destination $InstallDirectory -Force - -[xml]$configuration = Get-Content -LiteralPath $targetConfig -$environmentSetting = $configuration.configuration.appSettings.add | - Where-Object { $_.key -eq "EnvironmentName" } | - Select-Object -First 1 -if ($null -eq $environmentSetting) { - throw "EnvironmentName fehlt in $targetConfig." -} -$environmentSetting.value = $EnvironmentName -$configuration.Save($targetConfig) - -Write-Host "Erzeuge Runtime-Verzeichnisse und Least-Privilege-ACLs ..." -New-Item -ItemType Directory -Path $runtimeRoot, $dataDirectory, $logDirectory -Force | Out-Null - -# Runtime-Root: Provider lesen, SYSTEM lesen, lokale Administratoren verwalten. -Invoke-Icacls -Path $runtimeRoot -Arguments @( - "/inheritance:r", - "/grant:r", - "*S-1-5-18:(OI)(CI)(RX)", - "*S-1-5-32-544:(OI)(CI)(F)", - "${CollectorAccount}:(OI)(CI)(RX)" -) - -# Daten: ausschliesslich Provider schreibt, LocalSystem liest den Snapshot. -Invoke-Icacls -Path $dataDirectory -Arguments @( - "/inheritance:r", - "/grant:r", - "*S-1-5-18:(OI)(CI)(RX)", - "*S-1-5-32-544:(OI)(CI)(F)", - "${CollectorAccount}:(OI)(CI)(M)" -) - -# Logs: Provider und LocalSystem-Consumer duerfen getrennte Diagnosezeilen anhaengen. -Invoke-Icacls -Path $logDirectory -Arguments @( - "/inheritance:r", - "/grant:r", - "*S-1-5-18:(OI)(CI)(M)", - "*S-1-5-32-544:(OI)(CI)(F)", - "${CollectorAccount}:(OI)(CI)(M)" -) - -Write-Host "Installiere Checkmk Local Check nach $CheckmkLocalDirectory ..." -New-Item -ItemType Directory -Path $CheckmkLocalDirectory -Force | Out-Null -Copy-Item -LiteralPath $sourceWrapper -Destination $targetWrapper -Force - -Write-Host "Registriere Scheduled Task '$taskName' ..." -$action = New-ScheduledTaskAction ` - -Execute $targetExe ` - -Argument "--collect" ` - -WorkingDirectory $InstallDirectory -$trigger = New-ScheduledTaskTrigger ` - -Once ` - -At (Get-Date).AddMinutes(1) ` - -RepetitionInterval (New-TimeSpan -Minutes 1) -$settings = New-ScheduledTaskSettingsSet ` - -MultipleInstances IgnoreNew ` - -ExecutionTimeLimit (New-TimeSpan -Minutes 5) ` - -StartWhenAvailable ` - -RestartCount 2 ` - -RestartInterval (New-TimeSpan -Minutes 1) - -if ($Gmsa) { - $principal = New-ScheduledTaskPrincipal ` - -UserId $CollectorAccount ` - -LogonType Password ` - -RunLevel Limited - $task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings -Principal $principal - Register-ScheduledTask -TaskName $taskName -InputObject $task -Force | Out-Null -} -else { - $credential = Get-Credential ` - -UserName $CollectorAccount ` - -Message "Kennwort fuer das dedizierte BizTalk-Monitoringkonto eingeben" - if ($credential.UserName -ne $CollectorAccount) { - throw "Das eingegebene Konto stimmt nicht mit CollectorAccount ueberein." - } - - $principal = New-ScheduledTaskPrincipal ` - -UserId $CollectorAccount ` - -LogonType Password ` - -RunLevel Limited - $task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings -Principal $principal - Register-ScheduledTask ` - -TaskName $taskName ` - -InputObject $task ` - -User $CollectorAccount ` - -Password $credential.GetNetworkCredential().Password ` - -Force | Out-Null -} - -Write-Host "Fuehre formatseitigen Self-Test aus ..." -$selfTest = & $targetExe --self-test -if ($LASTEXITCODE -ne 0 -or @($selfTest).Count -ne 9) { - throw "Self-Test fehlgeschlagen. Erwartet wurden neun Checkmk-Zeilen." -} - -Write-Host "Starte den Provider einmalig ..." -Start-ScheduledTask -TaskName $taskName - -Write-Host "" -Write-Host "Installation abgeschlossen." -Write-Host "Naechste Pruefungen:" -Write-Host " Get-ScheduledTaskInfo -TaskName '$taskName'" -Write-Host " Get-Content '$logDirectory\biztalk-checkmk-pulse-*.log' -Tail 50" -Write-Host " & '$targetExe' --consume" -Write-Host " & 'C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe' dump" diff --git a/deployment/Uninstall-BizTalkCheckmkPulse.ps1 b/deployment/Uninstall-BizTalkCheckmkPulse.ps1 deleted file mode 100644 index bbd89f6..0000000 --- a/deployment/Uninstall-BizTalkCheckmkPulse.ps1 +++ /dev/null @@ -1,30 +0,0 @@ -[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "High")] -param( - [string]$InstallDirectory = "$env:ProgramFiles\BizTalkCheckmkPulse", - [string]$CheckmkLocalDirectory = "$env:ProgramData\checkmk\agent\local", - [switch]$KeepRuntimeData -) - -$ErrorActionPreference = "Stop" -$taskName = "BizTalk Checkmk Pulse Provider" -$runtimeRoot = Join-Path $env:ProgramData "BizTalkCheckmkPulse" -$wrapper = Join-Path $CheckmkLocalDirectory "biztalk_checkmk_pulse.cmd" - -if ($PSCmdlet.ShouldProcess($taskName, "Scheduled Task entfernen")) { - Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue -} -if (Test-Path -LiteralPath $wrapper) { - if ($PSCmdlet.ShouldProcess($wrapper, "Checkmk Local Check entfernen")) { - Remove-Item -LiteralPath $wrapper -Force - } -} -if (Test-Path -LiteralPath $InstallDirectory) { - if ($PSCmdlet.ShouldProcess($InstallDirectory, "Programmdateien entfernen")) { - Remove-Item -LiteralPath $InstallDirectory -Recurse -Force - } -} -if (-not $KeepRuntimeData -and (Test-Path -LiteralPath $runtimeRoot)) { - if ($PSCmdlet.ShouldProcess($runtimeRoot, "Snapshot und Logs entfernen")) { - Remove-Item -LiteralPath $runtimeRoot -Recurse -Force - } -} diff --git a/deployment/checkmk/biztalk_checkmk_pulse.cmd b/deployment/checkmk/biztalk_checkmk_pulse.cmd index fa6e0e4..9a3db96 100644 --- a/deployment/checkmk/biztalk_checkmk_pulse.cmd +++ b/deployment/checkmk/biztalk_checkmk_pulse.cmd @@ -5,7 +5,7 @@ set "BASE=%ProgramFiles%\BizTalkCheckmkPulse" set "EXE=%BASE%\BizTalkCheckmkPulse.exe" if not exist "%EXE%" ( - echo 3 "BizTalk Platform" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Install-BizTalkCheckmkPulse.ps1 erneut als Administrator ausfuehren und danach cmk-agent-ctl.exe dump erneut ausfuehren. + echo 3 "BizTalk Platform" - BizTalkCheckmkPulse.exe fehlt unter %BASE%. Massnahme: Setup.exe erneut als Administrator ausfuehren 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. diff --git a/docs/ACC-WMI-SQL-Berechtigung.txt b/docs/ACC-WMI-SQL-Berechtigung.txt index 7703239..f6de1e5 100644 --- a/docs/ACC-WMI-SQL-Berechtigung.txt +++ b/docs/ACC-WMI-SQL-Berechtigung.txt @@ -37,15 +37,10 @@ Bevorzugte Rolle fuer BizTalk Server 2020: BizTalk Server Read Only Users SQL-Datenbankrolle: BTS_READONLY_USERS -Die exakt konfigurierte Gruppe mit einem bereits berechtigten Konto ermitteln: - - Get-CimInstance -Namespace root/MicrosoftBizTalkServer ` - -ClassName MSBTS_GroupSetting | - Select-Object Name, BizTalkReadOnlyUserGroup, BizTalkOperatorGroup, - MgmtDbServerName, MgmtDbName - -Das Provider-Konto durch AD-Administration in BizTalkReadOnlyUserGroup -aufnehmen. Nicht vom Standardnamen ausgehen. +Die exakt konfigurierte Gruppe in den Eigenschaften der BizTalk-Gruppe in der +BizTalk Administration Console ablesen. Das Provider-Konto durch +AD-Administration in BizTalkReadOnlyUserGroup aufnehmen. Nicht vom +Standardnamen ausgehen. Die Operator-Gruppe ist nur eine begruendungspflichtige Eskalation, falls eine konkret benoetigte WMI-Klasse trotz bestaetigter Read-Only-Zuordnung und neuem @@ -64,28 +59,19 @@ NICHT DURCHFUEHREN INSTALLATION ============ -Normales Servicekonto: - - .\Install-BizTalkCheckmkPulse.ps1 ` - -CollectorAccount 'BEW\svc_biztalk_cmk' ` - -EnvironmentName ACC +BizTalkCheckmkPulse-Setup.zip vollstaendig entpacken und Setup.exe als +Administrator starten. Konto im Format DOMAIN\Benutzer (zum Beispiel +BEW\t231bizmon), Kennwort und Umgebung eingeben. PowerShell wird nicht +benoetigt. VERIFIKATION ============ - Get-ScheduledTaskInfo -TaskName 'BizTalk Checkmk Pulse Provider' - - Get-Content ` - "$env:ProgramData\BizTalkCheckmkPulse\logs\*.log" ` - -Tail 100 - - Get-Item ` - "$env:ProgramData\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot" - - & "$env:ProgramFiles\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume - - & "C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump | - Select-String -Pattern "BizTalk|UNKNOWN|Permission|snapshot" -Context 0,1 + schtasks.exe /query /tn "BizTalk Checkmk Pulse Provider" /v /fo list + dir "%ProgramData%\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot" + type "%ProgramData%\BizTalkCheckmkPulse\logs\biztalk-checkmk-pulse-*.log" + "%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume + "C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump ERWARTET ======== diff --git a/docs/Integration.md b/docs/Integration.md index 9e03290..4d852c6 100644 --- a/docs/Integration.md +++ b/docs/Integration.md @@ -15,13 +15,10 @@ Provider-Konto und publiziert einen validierbaren Snapshot. ## Installation -Normales Servicekonto: - -```powershell -.\Install-BizTalkCheckmkPulse.ps1 ` - -CollectorAccount 'BEW\svc_biztalk_cmk' ` - -EnvironmentName ACC -``` +`BizTalkCheckmkPulse-Setup.zip` vollstaendig entpacken, `Setup.exe` als +Administrator starten und Konto, Kennwort sowie Umgebung eingeben. Konten wie +`BEW\t231bizmon` werden direkt im Windows-Format `DOMAIN\Benutzer` verarbeitet. +Der Installer benoetigt keine PowerShell. Der Installer legt den Local Check hier ab: @@ -44,15 +41,14 @@ Verzoegerung zum minuetlichen Provider. Aktive Agentkonfiguration pruefen: -```powershell -& "C:\Program Files (x86)\checkmk\service\check_mk_agent.exe" showconfig local +```cmd +"C:\Program Files (x86)\checkmk\service\check_mk_agent.exe" showconfig local ``` Verbindlicher Agent-Dump: -```powershell -& "C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump | - Select-String -Pattern "BizTalk|UNKNOWN|snapshot|Permission" -Context 0,1 +```cmd +"C:\Program Files (x86)\checkmk\service\cmk-agent-ctl.exe" dump ``` ## Service Discovery @@ -95,11 +91,11 @@ Installationsautomation die klarere Variante. ## Healthchecks -```powershell -Get-ScheduledTaskInfo -TaskName 'BizTalk Checkmk Pulse Provider' -Get-Item "$env:ProgramData\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot" -Get-Content "$env:ProgramData\BizTalkCheckmkPulse\logs\*.log" -Tail 50 -& "$env:ProgramFiles\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume +```cmd +schtasks.exe /query /tn "BizTalk Checkmk Pulse Provider" /v /fo list +dir "%ProgramData%\BizTalkCheckmkPulse\data\biztalk-checkmk-pulse.snapshot" +type "%ProgramData%\BizTalkCheckmkPulse\logs\biztalk-checkmk-pulse-*.log" +"%ProgramFiles%\BizTalkCheckmkPulse\BizTalkCheckmkPulse.exe" --consume ``` Soll: diff --git a/scripts/build-release.cmd b/scripts/build-release.cmd index 0cb0811..35f850e 100644 --- a/scripts/build-release.cmd +++ b/scripts/build-release.cmd @@ -7,5 +7,5 @@ if errorlevel 1 ( exit /b 1 ) -msbuild BizTalkCheckmkPulse.sln /p:Configuration=Release /p:Platform="Any CPU" /m:1 /v:minimal +msbuild "%~dp0..\BizTalkCheckmkPulse.sln" /p:Configuration=Release /p:Platform="Any CPU" /m:1 /v:minimal exit /b %ERRORLEVEL% diff --git a/scripts/package-release.cmd b/scripts/package-release.cmd index 7fa7c30..7f4c15a 100644 --- a/scripts/package-release.cmd +++ b/scripts/package-release.cmd @@ -4,18 +4,6 @@ setlocal call "%~dp0build-release.cmd" if errorlevel 1 exit /b 1 -set "ROOT=%~dp0.." -set "OUT=%ROOT%\artifacts\BizTalkCheckmkPulse-deploy" -set "APP=%OUT%\application" - -if exist "%OUT%" rmdir /s /q "%OUT%" -mkdir "%APP%" - -copy "%ROOT%\deployment\checkmk\biztalk_checkmk_pulse.cmd" "%OUT%\" >nul -copy "%ROOT%\deployment\Install-BizTalkCheckmkPulse.ps1" "%OUT%\" >nul -copy "%ROOT%\deployment\Uninstall-BizTalkCheckmkPulse.ps1" "%OUT%\" >nul -copy "%ROOT%\src\BizTalkCheckmkPulse\bin\Release\BizTalkCheckmkPulse.exe" "%APP%\" >nul -copy "%ROOT%\src\BizTalkCheckmkPulse\bin\Release\BizTalkCheckmkPulse.exe.config" "%APP%\" >nul - -echo Deployment package written to %OUT% +echo Install package written to artifacts\BizTalkCheckmkPulse-Setup +echo Install ZIP written to artifacts\BizTalkCheckmkPulse-Setup.zip exit /b 0 diff --git a/src/BizTalkCheckmkPulse.Packager/BizTalkCheckmkPulse.Packager.csproj b/src/BizTalkCheckmkPulse.Packager/BizTalkCheckmkPulse.Packager.csproj new file mode 100644 index 0000000..7f02bd2 --- /dev/null +++ b/src/BizTalkCheckmkPulse.Packager/BizTalkCheckmkPulse.Packager.csproj @@ -0,0 +1,55 @@ + + + + + Debug + AnyCPU + {B35A04E0-253B-4CEB-9033-F5109F3B9F82} + Exe + BizTalkCheckmkPulse.Packager + BizTalkCheckmkPulse.Packager + 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 + false + + + {764AC43A-26D8-43C1-9121-13FC8A9CC8B1} + BizTalkCheckmkPulse.Setup + false + + + + + + + diff --git a/src/BizTalkCheckmkPulse.Packager/Program.cs b/src/BizTalkCheckmkPulse.Packager/Program.cs new file mode 100644 index 0000000..b471e4c --- /dev/null +++ b/src/BizTalkCheckmkPulse.Packager/Program.cs @@ -0,0 +1,59 @@ +using System; +using System.IO; +using System.IO.Compression; + +namespace BizTalkCheckmkPulse.Packager +{ + internal static class Program + { + private static int Main(string[] args) + { + try + { + if (args.Length != 2) throw new ArgumentException("Usage: BizTalkCheckmkPulse.Packager.exe "); + var root = Path.GetFullPath(args[0]); + var configuration = args[1]; + var artifacts = Path.Combine(root, "artifacts"); + var package = Path.Combine(artifacts, "BizTalkCheckmkPulse-Setup"); + var application = Path.Combine(package, "application"); + var zip = Path.Combine(artifacts, "BizTalkCheckmkPulse-Setup.zip"); + + if (Directory.Exists(package)) Directory.Delete(package, true); + Directory.CreateDirectory(application); + + Copy( + Path.Combine(root, "src", "BizTalkCheckmkPulse.Setup", "bin", configuration, "BizTalkCheckmkPulse.Setup.exe"), + Path.Combine(package, "Setup.exe")); + Copy( + Path.Combine(root, "src", "BizTalkCheckmkPulse", "bin", configuration, "BizTalkCheckmkPulse.exe"), + Path.Combine(application, "BizTalkCheckmkPulse.exe")); + Copy( + Path.Combine(root, "src", "BizTalkCheckmkPulse", "bin", configuration, "BizTalkCheckmkPulse.exe.config"), + Path.Combine(application, "BizTalkCheckmkPulse.exe.config")); + Copy( + Path.Combine(root, "deployment", "checkmk", "biztalk_checkmk_pulse.cmd"), + Path.Combine(package, "biztalk_checkmk_pulse.cmd")); + Copy( + Path.Combine(root, "deployment", "INSTALLATION.md"), + Path.Combine(package, "INSTALLATION.md")); + + if (File.Exists(zip)) File.Delete(zip); + ZipFile.CreateFromDirectory(package, zip, CompressionLevel.Optimal, false); + Console.WriteLine("Install package: " + package); + Console.WriteLine("Install ZIP: " + zip); + return 0; + } + catch (Exception ex) + { + Console.Error.WriteLine("Packaging failed: " + ex.Message); + return 1; + } + } + + private static void Copy(string source, string target) + { + if (!File.Exists(source)) throw new FileNotFoundException("Required package file missing: " + source, source); + File.Copy(source, target, true); + } + } +} diff --git a/src/BizTalkCheckmkPulse.Setup/BizTalkCheckmkPulse.Setup.csproj b/src/BizTalkCheckmkPulse.Setup/BizTalkCheckmkPulse.Setup.csproj new file mode 100644 index 0000000..017ab23 --- /dev/null +++ b/src/BizTalkCheckmkPulse.Setup/BizTalkCheckmkPulse.Setup.csproj @@ -0,0 +1,51 @@ + + + + + Debug + AnyCPU + {764AC43A-26D8-43C1-9121-13FC8A9CC8B1} + WinExe + BizTalkCheckmkPulse.Setup + BizTalkCheckmkPulse.Setup + v4.7.2 + 512 + true + true + app.manifest + + + true + full + false + bin\Debug\ + DEBUG;TRACE + 4 + + + pdbonly + true + bin\Release\ + TRACE + 4 + + + + + + + + + + + + + + + + + + + + + diff --git a/src/BizTalkCheckmkPulse.Setup/InstallerEngine.cs b/src/BizTalkCheckmkPulse.Setup/InstallerEngine.cs new file mode 100644 index 0000000..9c54d96 --- /dev/null +++ b/src/BizTalkCheckmkPulse.Setup/InstallerEngine.cs @@ -0,0 +1,176 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Security.AccessControl; +using System.Security.Principal; +using System.Xml; + +namespace BizTalkCheckmkPulse.Setup +{ + internal sealed class InstallerEngine + { + internal const string TaskName = "BizTalk Checkmk Pulse Provider"; + private readonly string packageDirectory; + private readonly string installDirectory; + private readonly string runtimeDirectory; + private readonly string checkmkLocalDirectory; + + public InstallerEngine(string packageDirectory) + { + this.packageDirectory = Path.GetFullPath(packageDirectory); + installDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "BizTalkCheckmkPulse"); + runtimeDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "BizTalkCheckmkPulse"); + checkmkLocalDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "checkmk", "agent", "local"); + } + + public void Install(string account, string password, bool isGmsa, string environmentName, Action report) + { + Validate(account, password, isGmsa, environmentName); + var accountSid = (SecurityIdentifier)new NTAccount(account).Translate(typeof(SecurityIdentifier)); + report("Konto aufgeloest: " + account + " (" + accountSid.Value + ")"); + + var sourceApplication = Path.Combine(packageDirectory, "application"); + var sourceExe = Path.Combine(sourceApplication, "BizTalkCheckmkPulse.exe"); + var sourceConfig = sourceExe + ".config"; + var sourceWrapper = Path.Combine(packageDirectory, "biztalk_checkmk_pulse.cmd"); + RequireFile(sourceExe); + RequireFile(sourceConfig); + RequireFile(sourceWrapper); + + // Ein laufender Provider kann die installierte EXE waehrend eines Updates sperren. + new TaskSchedulerService().DeleteIfExists(TaskName); + report("Vorhandener Scheduled Task angehalten beziehungsweise fuer das Update entfernt."); + + Directory.CreateDirectory(installDirectory); + var targetExe = Path.Combine(installDirectory, "BizTalkCheckmkPulse.exe"); + var targetConfig = targetExe + ".config"; + File.Copy(sourceExe, targetExe, true); + File.Copy(sourceConfig, targetConfig, true); + SetEnvironment(targetConfig, environmentName); + report("Programmdateien installiert: " + installDirectory); + + var dataDirectory = Path.Combine(runtimeDirectory, "data"); + var logDirectory = Path.Combine(runtimeDirectory, "logs"); + Directory.CreateDirectory(runtimeDirectory); + Directory.CreateDirectory(dataDirectory); + Directory.CreateDirectory(logDirectory); + + ApplyDirectoryAcl(installDirectory, accountSid, FileSystemRights.ReadAndExecute, FileSystemRights.ReadAndExecute); + ApplyDirectoryAcl(runtimeDirectory, accountSid, FileSystemRights.ReadAndExecute, FileSystemRights.ReadAndExecute); + ApplyDirectoryAcl(dataDirectory, accountSid, FileSystemRights.Modify, FileSystemRights.ReadAndExecute); + ApplyDirectoryAcl(logDirectory, accountSid, FileSystemRights.Modify, FileSystemRights.Modify); + report("Least-Privilege-Verzeichnisrechte gesetzt."); + + Directory.CreateDirectory(checkmkLocalDirectory); + File.Copy(sourceWrapper, Path.Combine(checkmkLocalDirectory, "biztalk_checkmk_pulse.cmd"), true); + report("Checkmk Local Check installiert: " + checkmkLocalDirectory); + + RunSelfTest(targetExe); + report("Self-Test erfolgreich: acht Checkmk-Services."); + + new TaskSchedulerService().RegisterAndStart( + TaskName, + targetExe, + installDirectory, + account, + isGmsa ? null : password, + isGmsa); + report("Scheduled Task registriert: " + TaskName); + } + + public void Uninstall(bool keepRuntimeData, Action report) + { + new TaskSchedulerService().DeleteIfExists(TaskName); + report("Scheduled Task entfernt."); + + var wrapper = Path.Combine(checkmkLocalDirectory, "biztalk_checkmk_pulse.cmd"); + if (File.Exists(wrapper)) File.Delete(wrapper); + if (Directory.Exists(installDirectory)) Directory.Delete(installDirectory, true); + if (!keepRuntimeData && Directory.Exists(runtimeDirectory)) Directory.Delete(runtimeDirectory, true); + } + + private static void Validate(string account, string password, bool isGmsa, string environmentName) + { + if (string.IsNullOrWhiteSpace(account)) throw new ArgumentException("Collector-Konto fehlt."); + if (account.IndexOf('\\') <= 0 || account.EndsWith("\\", StringComparison.Ordinal)) + throw new ArgumentException(@"Collector-Konto im Format DOMAIN\Benutzer eingeben, z.B. BEW\t231bizmon."); + if (isGmsa && !account.EndsWith("$", StringComparison.Ordinal)) + throw new ArgumentException(@"Ein gMSA-Konto muss mit '$' enden, z.B. BEW\svc_biztalk_cmk$."); + if (!isGmsa && string.IsNullOrEmpty(password)) throw new ArgumentException("Kennwort fehlt."); + if (!new[] { "", "ACC", "DEV", "TST", "PRD" }.Contains(environmentName)) + throw new ArgumentException("Ungueltige Umgebung."); + } + + private static void RequireFile(string path) + { + if (!File.Exists(path)) + throw new FileNotFoundException("Installationspaket ist unvollstaendig. Datei fehlt: " + path, path); + } + + private static void SetEnvironment(string configPath, string environmentName) + { + var document = new XmlDocument { PreserveWhitespace = true }; + document.Load(configPath); + var setting = document.SelectSingleNode("/configuration/appSettings/add[@key='EnvironmentName']") as XmlElement; + if (setting == null) throw new InvalidDataException("EnvironmentName fehlt in " + configPath + "."); + setting.SetAttribute("value", environmentName); + document.Save(configPath); + } + + private static void ApplyDirectoryAcl( + string path, + SecurityIdentifier collectorSid, + FileSystemRights collectorRights, + FileSystemRights systemRights) + { + var inheritance = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit; + var security = new DirectorySecurity(); + security.SetAccessRuleProtection(true, false); + security.AddAccessRule(new FileSystemAccessRule( + new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null), + FileSystemRights.FullControl, + inheritance, + PropagationFlags.None, + AccessControlType.Allow)); + security.AddAccessRule(new FileSystemAccessRule( + new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null), + systemRights, + inheritance, + PropagationFlags.None, + AccessControlType.Allow)); + security.AddAccessRule(new FileSystemAccessRule( + collectorSid, + collectorRights, + inheritance, + PropagationFlags.None, + AccessControlType.Allow)); + new DirectoryInfo(path).SetAccessControl(security); + } + + private static void RunSelfTest(string executable) + { + var start = new ProcessStartInfo(executable, "--self-test") + { + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + using (var process = Process.Start(start)) + { + if (process == null) throw new InvalidOperationException("Self-Test konnte nicht gestartet werden."); + var output = process.StandardOutput.ReadToEnd(); + var error = process.StandardError.ReadToEnd(); + if (!process.WaitForExit(30000)) + { + process.Kill(); + throw new InvalidOperationException("Self-Test hat das Zeitlimit ueberschritten."); + } + var lines = output.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); + if (process.ExitCode != 0 || lines.Length != 8 || lines.Any(x => !x.StartsWith("0 ", StringComparison.Ordinal))) + throw new InvalidOperationException("Self-Test fehlgeschlagen. Exitcode=" + process.ExitCode + ", Zeilen=" + lines.Length + ". " + error); + } + } + } +} diff --git a/src/BizTalkCheckmkPulse.Setup/MainForm.cs b/src/BizTalkCheckmkPulse.Setup/MainForm.cs new file mode 100644 index 0000000..d3b5806 --- /dev/null +++ b/src/BizTalkCheckmkPulse.Setup/MainForm.cs @@ -0,0 +1,181 @@ +using System; +using System.Drawing; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace BizTalkCheckmkPulse.Setup +{ + internal sealed class MainForm : Form + { + private readonly InstallerEngine engine; + private readonly TextBox account = new TextBox(); + private readonly TextBox password = new TextBox(); + private readonly ComboBox environment = new ComboBox(); + private readonly CheckBox gmsa = new CheckBox(); + private readonly Button install = new Button(); + private readonly Button uninstall = new Button(); + private readonly TextBox status = new TextBox(); + + public MainForm(InstallerEngine engine) + { + this.engine = engine; + Text = "BizTalk Checkmk Pulse Setup"; + ClientSize = new Size(650, 485); + MinimumSize = new Size(666, 524); + StartPosition = FormStartPosition.CenterScreen; + Font = new Font("Segoe UI", 9F); + + var title = new Label + { + AutoSize = true, + Font = new Font(Font.FontFamily, 15F, FontStyle.Bold), + Location = new Point(20, 18), + Text = "BizTalk Checkmk Pulse installieren" + }; + var description = new Label + { + AutoSize = false, + Location = new Point(22, 58), + Size = new Size(605, 48), + Text = "Installiert Anwendung, Checkmk Local Check, sichere Verzeichnisrechte und den minuetlichen Scheduled Task. PowerShell wird nicht verwendet." + }; + + AddLabel("Collector-Konto", 22, 120); + account.Location = new Point(190, 116); + account.Size = new Size(420, 23); + account.Text = @"BEW\t231bizmon"; + + AddLabel("Kennwort", 22, 158); + password.Location = new Point(190, 154); + password.Size = new Size(420, 23); + password.UseSystemPasswordChar = true; + + AddLabel("Umgebung", 22, 196); + environment.Location = new Point(190, 192); + environment.Size = new Size(160, 23); + environment.DropDownStyle = ComboBoxStyle.DropDownList; + environment.Items.AddRange(new object[] { "(keine)", "ACC", "DEV", "TST", "PRD" }); + environment.SelectedIndex = 0; + + gmsa.Location = new Point(190, 229); + gmsa.Size = new Size(420, 24); + gmsa.Text = "Konto ist ein gMSA (kein Kennwort erforderlich)"; + gmsa.CheckedChanged += delegate + { + password.Enabled = !gmsa.Checked; + if (gmsa.Checked) password.Clear(); + }; + + install.Location = new Point(190, 267); + install.Size = new Size(200, 34); + install.Text = "Installieren / aktualisieren"; + install.Click += async delegate { await InstallAsync(); }; + + uninstall.Location = new Point(410, 267); + uninstall.Size = new Size(200, 34); + uninstall.Text = "Deinstallieren"; + uninstall.Click += async delegate { await UninstallAsync(); }; + + status.Location = new Point(22, 320); + status.Size = new Size(588, 135); + status.Multiline = true; + status.ReadOnly = true; + status.ScrollBars = ScrollBars.Vertical; + status.Text = "Bereit. Setup.exe muss aus dem vollstaendig entpackten Installationspaket gestartet werden."; + + Controls.AddRange(new Control[] + { + title, description, account, password, environment, gmsa, install, uninstall, status + }); + AcceptButton = install; + } + + private void AddLabel(string text, int left, int top) + { + Controls.Add(new Label { AutoSize = true, Location = new Point(left, top), Text = text }); + } + + private async Task InstallAsync() + { + var collectorAccount = account.Text.Trim(); + var collectorPassword = password.Text; + var environmentName = environment.SelectedIndex <= 0 ? string.Empty : environment.SelectedItem.ToString(); + var collectorIsGmsa = gmsa.Checked; + + SetBusy(true, "Installation laeuft ..."); + try + { + await Task.Run(() => engine.Install( + collectorAccount, + collectorPassword, + collectorIsGmsa, + environmentName, + Report)); + password.Clear(); + Report("Installation abgeschlossen. Der Provider-Task wurde einmalig gestartet."); + MessageBox.Show(this, "Installation erfolgreich abgeschlossen.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (Exception ex) + { + password.Clear(); + Report("FEHLER: " + ex.Message); + MessageBox.Show(this, ex.Message, "Installation fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + SetBusy(false, null); + } + } + + private async Task UninstallAsync() + { + var result = MessageBox.Show( + this, + "Scheduled Task, Checkmk-Wrapper und Programmdateien entfernen? Snapshot und Logs bleiben erhalten.", + "Deinstallation bestaetigen", + MessageBoxButtons.YesNo, + MessageBoxIcon.Warning); + if (result != DialogResult.Yes) return; + + SetBusy(true, "Deinstallation laeuft ..."); + try + { + await Task.Run(() => engine.Uninstall(true, Report)); + Report("Deinstallation abgeschlossen. Runtime-Daten wurden beibehalten."); + MessageBox.Show(this, "Deinstallation erfolgreich abgeschlossen.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information); + } + catch (Exception ex) + { + Report("FEHLER: " + ex.Message); + MessageBox.Show(this, ex.Message, "Deinstallation fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + finally + { + SetBusy(false, null); + } + } + + private void SetBusy(bool busy, string message) + { + install.Enabled = !busy; + uninstall.Enabled = !busy; + account.Enabled = !busy; + environment.Enabled = !busy; + gmsa.Enabled = !busy; + password.Enabled = !busy && !gmsa.Checked; + UseWaitCursor = busy; + if (message != null) Report(message); + } + + private void Report(string message) + { + if (InvokeRequired) + { + BeginInvoke(new Action(Report), message); + return; + } + + status.AppendText(Environment.NewLine + DateTime.Now.ToString("HH:mm:ss") + " " + message); + } + } +} diff --git a/src/BizTalkCheckmkPulse.Setup/Program.cs b/src/BizTalkCheckmkPulse.Setup/Program.cs new file mode 100644 index 0000000..2c6e9e0 --- /dev/null +++ b/src/BizTalkCheckmkPulse.Setup/Program.cs @@ -0,0 +1,16 @@ +using System; +using System.Windows.Forms; + +namespace BizTalkCheckmkPulse.Setup +{ + internal static class Program + { + [STAThread] + private static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new MainForm(new InstallerEngine(AppDomain.CurrentDomain.BaseDirectory))); + } + } +} diff --git a/src/BizTalkCheckmkPulse.Setup/TaskSchedulerService.cs b/src/BizTalkCheckmkPulse.Setup/TaskSchedulerService.cs new file mode 100644 index 0000000..a8f2ad5 --- /dev/null +++ b/src/BizTalkCheckmkPulse.Setup/TaskSchedulerService.cs @@ -0,0 +1,123 @@ +using System; +using System.Runtime.InteropServices; + +namespace BizTalkCheckmkPulse.Setup +{ + internal sealed class TaskSchedulerService + { + private const int TaskCreateOrUpdate = 6; + private const int TaskLogonPassword = 1; + private const int TaskTriggerTime = 1; + private const int TaskActionExecute = 0; + private const int TaskInstancesIgnoreNew = 2; + + public void RegisterAndStart( + string taskName, + string executable, + string workingDirectory, + string account, + string password, + bool isGmsa) + { + dynamic service = null; + dynamic root = null; + dynamic definition = null; + dynamic registeredTask = null; + try + { + var schedulerType = Type.GetTypeFromProgID("Schedule.Service", true); + service = Activator.CreateInstance(schedulerType); + service.Connect(); + root = service.GetFolder("\\"); + definition = service.NewTask(0); + + definition.RegistrationInfo.Description = "Collects BizTalk monitoring data for the Checkmk local check."; + definition.Principal.UserId = account; + // Auch gMSA werden als Password-Logon registriert. Nur das Kennwort + // bleibt leer; TASK_LOGON_SERVICE_ACCOUNT ist ausschliesslich fuer + // LocalSystem, LocalService und NetworkService vorgesehen. + definition.Principal.LogonType = TaskLogonPassword; + definition.Principal.RunLevel = 0; + + definition.Settings.Enabled = true; + definition.Settings.StartWhenAvailable = true; + definition.Settings.MultipleInstances = TaskInstancesIgnoreNew; + definition.Settings.ExecutionTimeLimit = "PT5M"; + definition.Settings.RestartCount = 2; + definition.Settings.RestartInterval = "PT1M"; + definition.Settings.DisallowStartIfOnBatteries = false; + definition.Settings.StopIfGoingOnBatteries = false; + + dynamic trigger = definition.Triggers.Create(TaskTriggerTime); + trigger.StartBoundary = DateTime.Now.AddMinutes(1).ToString("s"); + trigger.Enabled = true; + trigger.Repetition.Interval = "PT1M"; + + dynamic action = definition.Actions.Create(TaskActionExecute); + action.Path = executable; + action.Arguments = "--collect"; + action.WorkingDirectory = workingDirectory; + + registeredTask = root.RegisterTaskDefinition( + taskName, + definition, + TaskCreateOrUpdate, + account, + isGmsa ? null : password, + TaskLogonPassword, + null); + registeredTask.Run(null); + } + finally + { + Release(registeredTask); + Release(definition); + Release(root); + Release(service); + } + } + + public void DeleteIfExists(string taskName) + { + dynamic service = null; + dynamic root = null; + dynamic task = null; + try + { + var schedulerType = Type.GetTypeFromProgID("Schedule.Service", true); + service = Activator.CreateInstance(schedulerType); + service.Connect(); + root = service.GetFolder("\\"); + try + { + task = root.GetTask(taskName); + try + { + task.Stop(0); + } + catch (COMException) + { + // Ein nicht laufender Task muss vor dem Loeschen nicht gestoppt werden. + } + root.DeleteTask(taskName, 0); + } + catch (COMException ex) + { + const int FileNotFound = unchecked((int)0x80070002); + if (ex.ErrorCode != FileNotFound) throw; + } + } + finally + { + Release(task); + Release(root); + Release(service); + } + } + + private static void Release(object value) + { + if (value != null && Marshal.IsComObject(value)) Marshal.FinalReleaseComObject(value); + } + } +} diff --git a/src/BizTalkCheckmkPulse.Setup/app.manifest b/src/BizTalkCheckmkPulse.Setup/app.manifest new file mode 100644 index 0000000..23f7894 --- /dev/null +++ b/src/BizTalkCheckmkPulse.Setup/app.manifest @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + +