diff --git a/.gitignore b/.gitignore index 51b6516..d7da0c7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ Thumbs.db .idea/ *.user *.suo +bin/ +obj/ # Logs / Outputs *.log diff --git a/BizTalkPlatformManagementTool.sln b/BizTalkPlatformManagementTool.sln new file mode 100644 index 0000000..0f2dfdf --- /dev/null +++ b/BizTalkPlatformManagementTool.sln @@ -0,0 +1,21 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 15 +VisualStudioVersion = 15.0.28307.168 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BizTalkPlatformManagementTool", "src\BizTalkPlatformManagementTool\BizTalkPlatformManagementTool.csproj", "{2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/CHANGELOG.md b/CHANGELOG.md index 95b5c2e..1394fa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog +## [Unreleased] - 2026-04-27 +### Changed +- Projekt auf `BizTalkPlatformManagementTool` umbenannt. +- Solution, Projektdatei, Namespace, Assemblyname und Standard-Ausgabepfad angepasst. +- Dokumentation aktualisiert und `Installation.md` ergänzt. + ## [1.3.0] - 2026-02-02 ### Added - HTML Report mit farblicher Kennzeichnung (grün/rot) diff --git a/Dokumentation.md b/Dokumentation.md new file mode 100644 index 0000000..3a51471 --- /dev/null +++ b/Dokumentation.md @@ -0,0 +1,89 @@ +# BizTalk Platform Management Tool Dokumentation + +**Stand:** 2026-04-27 +**Implementierung:** C# WinForms, .NET Framework 4.7.1 +**Archivierte PowerShell-Version:** `archive/powershell/BizTalkPlatformManagementTool.ps1` + +## Zweck + +Das BizTalk Platform Management Tool unterstützt kontrollierte Wartungsfenster für Microsoft BizTalk Server 2020: + +- Exakten Laufzeitstatus vor einer Wartung erfassen. +- Receive Locations, Orchestrations, Send Ports und Host Instances in definierter Reihenfolge herunterfahren. +- Den Laufzeitstatus aus einer gespeicherten `before.json` wiederherstellen. +- Den Status nach der Wartung erfassen und beide Snapshots vergleichen. +- Plan- und Ergebnisdateien für Review, Audit und Nachvollziehbarkeit schreiben. + +## Projektstruktur + +- Solution: `BizTalkPlatformManagementTool.sln` +- Projekt: `src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj` +- UI: `src/BizTalkPlatformManagementTool/Ui/MainForm.cs` +- WMI-Zugriff: `src/BizTalkPlatformManagementTool/Services/BizTalkWmiClient.cs` +- Operationslogik: `src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs` +- PowerShell-Archiv: `archive/powershell/BizTalkPlatformManagementTool.ps1` + +## UI Workflow + +1. BizTalk-Servernamen eintragen. +2. Ausgabeverzeichnis wählen. +3. **Dry run** aktiviert lassen. +4. Mit **Diagnose** den Zugriff auf `root\MicrosoftBizTalkServer` prüfen. +5. Mit **Snapshot Before** die Datei `before.json` erzeugen. +6. Mit **Shutdown** den Plan `shutdown-plan.json` erzeugen und prüfen. +7. **Dry run** erst deaktivieren, wenn der Plan korrekt ist. +8. Nach der Wartung mit **Restore** aus `before.json` wiederherstellen. +9. Mit **Snapshot After** und **Compare** die Umgebung validieren. + +## Sicherheitsdesign + +- Dry-run ist standardmäßig aktiviert. +- Echte Shutdown-/Restore-Aktionen verlangen bei deaktiviertem Dry-run eine zusätzliche Bestätigung. +- Jede Operation schreibt Einträge in das sichtbare Operation Log. +- Operationspläne werden vor Laufzeitänderungen gespeichert. +- WMI-Methodenrückgaben werden geprüft. +- Wartezeiten nutzen konfigurierbare Timeout- und Polling-Werte. +- Host Instances auf anderen Servern werden übersprungen und als Warnung protokolliert. + +## Shutdown-Reihenfolge + +1. Receive Locations deaktivieren, die im Snapshot aktiviert waren. +2. Orchestrations stoppen, die gestartet waren. +3. Send Ports stoppen, die gestartet waren. +4. Host Instances stoppen, die auf dem ausgewählten Server gestartet waren. + +## Restore-Reihenfolge + +1. Host Instances starten, die auf dem ausgewählten Server zuvor gestartet waren. +2. Send Ports wiederherstellen. +3. Orchestrations wiederherstellen, soweit dies sicher möglich ist. +4. Receive Locations zuletzt wiederherstellen. + +## Restore-Grenzen + +- Send Ports werden auf `Started`, `Stopped` oder `Bound` zurückgesetzt. +- Orchestrations mit Status `Started`, `Stopped` oder `Unbound` werden direkt behandelt. +- Orchestrations mit Status `Bound` bleiben unverändert und werden zur manuellen Prüfung markiert, weil ein blindes Unenlist zu `Unbound` führen kann. +- Receive Locations werden zuletzt aktiviert, damit eingehender Verkehr erst nach Host Instances und abhängigen Artefakten wieder aufgenommen wird. + +## Ausgabedateien + +- Snapshots: `before.json`, `after.json` +- Operationspläne: `shutdown-plan.json`, `restore-plan.json` +- Nachher-Snapshots: `shutdown-after.json`, `restore-after.json` +- Diff: `diff.json`, `diff.csv`, `diff.html` +- Snapshot-Reports: `*.csv`, `*.hosts.csv`, `*.html` + +## Status Mapping + +- `MSBTS_SendPort.Status`: `1=Bound`, `2=Stopped`, `3=Started` +- `MSBTS_Orchestration.OrchestrationStatus`: `1=Unbound`, `2=Bound`, `3=Stopped`, `4=Started` +- `MSBTS_ReceiveLocation`: `Enabled = !IsDisabled` +- `MSBTS_HostInstance.ServiceState`: `1=Stopped`, `2=StartPending`, `3=StopPending`, `4=Started` + +## Anforderungen + +- Windows Server oder Administrationshost mit .NET Framework 4.7.1 +- BizTalk Server 2020 oder installierte BizTalk Administration Tools +- Berechtigungen auf den WMI-Namespace `root\MicrosoftBizTalkServer` +- Visual Studio mit .NET Framework 4.7.1 Developer Pack für Builds diff --git a/Installation.md b/Installation.md new file mode 100644 index 0000000..2f7f088 --- /dev/null +++ b/Installation.md @@ -0,0 +1,37 @@ +# Installation + +## Voraussetzungen + +- Windows Server 2019/2022 oder ein Windows-Administrationshost +- Microsoft BizTalk Server 2020 oder BizTalk Administration Tools +- .NET Framework 4.7.1 Runtime +- Für Builds: Visual Studio mit .NET Framework 4.7.1 Developer Pack +- Zugriff auf den WMI-Namespace `root\MicrosoftBizTalkServer` +- Ausreichende Rechte zum Lesen und Ändern von BizTalk-Artefakten + +## Build + +1. Repository öffnen. +2. `BizTalkPlatformManagementTool.sln` in Visual Studio öffnen. +3. Konfiguration `Release|Any CPU` auswählen. +4. Solution bauen. +5. Das Ergebnis liegt unter `src\BizTalkPlatformManagementTool\bin\Release\`. + +## Deployment + +1. Den Release-Ordner auf einen BizTalk-Server oder einen Administrationshost kopieren. +2. Sicherstellen, dass der ausführende Benutzer WMI-Zugriff auf `root\MicrosoftBizTalkServer` hat. +3. `BizTalkPlatformManagementTool.exe` starten. +4. Als Ausgabeverzeichnis einen Ordner wählen, in dem Plan-, Snapshot- und Report-Dateien abgelegt werden dürfen. + +## Erster Funktionstest + +1. Anwendung starten. +2. Zielserver eintragen oder den vorgeschlagenen lokalen Server verwenden. +3. **Dry run** aktiviert lassen. +4. **Diagnose** ausführen. +5. **Snapshot Before** ausführen und prüfen, ob `before.json` sowie CSV/HTML-Reports erzeugt wurden. + +## Produktive Nutzung + +Vor produktiven Änderungen immer zuerst einen Dry-run ausführen und die erzeugten `shutdown-plan.json` beziehungsweise `restore-plan.json` prüfen. Dry-run erst deaktivieren, wenn der Plan fachlich und technisch korrekt ist. diff --git a/README.md b/README.md index 0718f0e..8bae6fb 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,70 @@ -# BizTalk Status Tool (PowerShell only) +# BizTalk Platform Management Tool -**Version:** 1.3.0 -**Kompatibel mit:** Windows Server 2019, PowerShell 5.1, BizTalk Server 2020 +WinForms tool for controlled Microsoft BizTalk Server 2020 platform operations during maintenance windows. The tool captures runtime snapshots, creates shutdown and restore plans, executes those plans in a safe order, and writes diff reports for validation. + +## Current State + +- Application: C# WinForms +- Target framework: .NET Framework 4.7.1 +- Solution: `BizTalkPlatformManagementTool.sln` +- Project: `src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj` +- Primary namespace and assembly: `BizTalkPlatformManagementTool` +- Legacy PowerShell archive: `archive/powershell/BizTalkPlatformManagementTool.ps1` ## Features -- Snapshots **vor**/**nach** Downtime als JSON -- **Diff** zwischen Snapshots (nur Änderungen) -- **HTML‑Report** mit **Farben** (grün = Started/Enabled, rot = Stopped/Disabled/Unbound/Bound) -- **Remote‑fähig**: `-Server` Parameter (WMI unter `root\MicrosoftBizTalkServer`) -- **Logging**: Konsole + `BizTalkStatusTool.log` -## Verwendung (Beispiele) -```powershell -# Snapshot VOR Downtime vom Remote‑Server -.\src\BizTalkStatusTool.ps1 -Before -Server "BTAPP01" -OutDir .\work +- Snapshot before and after downtime as JSON, CSV and HTML +- Diff between `before.json` and `after.json` +- Controlled shutdown from the current runtime state +- Controlled restore from `before.json` +- Host instance handling for the selected BizTalk server +- Dry-run mode enabled by default +- WMI access through `root\MicrosoftBizTalkServer` +- No compile-time dependency on BizTalk ExplorerOM assemblies -# Snapshot NACH Downtime -.\src\BizTalkStatusTool.ps1 -After -Server "BTAPP01" -OutDir .\work +## Safe Usage -# Diff erstellen (Text + HTML im gleichen Ordner) -cd .\work -..\src\BizTalkStatusTool.ps1 -Compare -``` +1. Start the app on a BizTalk server or a management host with WMI access. +2. Keep **Dry run** enabled. +3. Click **Diagnose** to verify WMI access. +4. Click **Snapshot Before**. +5. Click **Shutdown** and review `shutdown-plan.json`. +6. Disable **Dry run** only when the plan is correct. +7. After maintenance, click **Restore** using the saved `before.json`. +8. Click **Snapshot After** and **Compare**. -## Status‑Mapping -- **SendPort.Status**: 1=Bound, 2=Stopped, 3=Started -- **Orchestration.OrchestrationStatus**: 1=Unbound, 2=Bound, 3=Stopped, 4=Started -- **ReceiveLocation**: `Enabled = -not IsDisabled` +## Operation Order -**Hinweis:** Diese Mappings stammen aus der offiziellen BizTalk‑Dokumentation (siehe `REFERENCES.md`). +Shutdown: +- Disable receive locations that were enabled. +- Stop orchestrations that were started. +- Stop send ports that were started. +- Stop host instances that were started on the selected server. -## Releases -Fertige Skripte liegen im Ordner `/release`. +Restore: +- Start host instances that were previously started. +- Restore send ports to Started, Stopped or Bound. +- Restore orchestrations where safe. +- Restore receive locations last. -## Tags -Mit `tools/Create-Tags.ps1` kannst du lokale Git‑Tags (`v1.0.0`, `v1.1.0`, `v1.2.0`, `v1.3.0`) erstellen. +Orchestrations that were `Bound` are deliberately left unchanged during restore to avoid accidentally making them `Unbound`. + +## Outputs + +- `before.json`, `after.json` +- `shutdown-plan.json`, `restore-plan.json` +- `shutdown-after.json`, `restore-after.json` +- `diff.json`, `diff.csv`, `diff.html` +- Snapshot sidecars: `*.csv`, `*.hosts.csv`, `*.html` + +## Build + +Open `BizTalkPlatformManagementTool.sln` in Visual Studio on Windows with the .NET Framework 4.7.1 Developer Pack installed, then build the `Release|Any CPU` configuration. + +The app targets .NET Framework 4.7.1 because BizTalk Server 2020 commonly runs in that Windows/.NET Framework operational environment. + +## Documentation + +- [Installation](Installation.md) +- [Dokumentation](Dokumentation.md) +- [References](REFERENCES.md) diff --git a/REFERENCES.md b/REFERENCES.md index 0fe4950..c52a97f 100644 --- a/REFERENCES.md +++ b/REFERENCES.md @@ -3,5 +3,17 @@ - MSBTS_SendPort.Status (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-status-property-wmi - MSBTS_SendPort (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-wmi +- MSBTS_SendPort.Start (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-start-method-wmi +- MSBTS_SendPort.Stop (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-stop-method-wmi +- MSBTS_SendPort.Enlist (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-enlist-method-wmi +- MSBTS_SendPort.UnEnlist (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-sendport-unenlist-method-wmi - MSBTS_Orchestration.OrchestrationStatus (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-orchestration-orchestrationstatus-property-wmi +- MSBTS_Orchestration.Start (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-orchestration-start-method-wmi +- MSBTS_Orchestration.Stop (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-orchestration-stop-method-wmi +- MSBTS_Orchestration.Unenlist (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-orchestration-unenlist-method-wmi - MSBTS_ReceiveLocation (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-wmi +- MSBTS_ReceiveLocation.Enable (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-enable-method-wmi +- MSBTS_ReceiveLocation.Disable (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-receivelocation-disable-method-wmi +- MSBTS_HostInstance (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-wmi +- MSBTS_HostInstance.Start (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-start-method-wmi +- MSBTS_HostInstance.Stop (WMI): https://learn.microsoft.com/en-us/biztalk/core/technical-reference/msbts-hostinstance-stop-method-wmi diff --git a/archive/powershell/BizTalkPlatformManagementTool.ps1 b/archive/powershell/BizTalkPlatformManagementTool.ps1 new file mode 100644 index 0000000..7303bd3 --- /dev/null +++ b/archive/powershell/BizTalkPlatformManagementTool.ps1 @@ -0,0 +1,1040 @@ + +<# +BizTalk Platform Management Tool – Refactored Level B (Single File) +Resilienz-Level 1 • HTML-Level B • PS 5.1 & 7.x kompatibel + +Beinhaltet: +- Snapshot: Applications (ReceiveLocations / SendPorts / Orchestrations) + Host Instances +- Exporte: JSON, CSV, HTML (modernisiert) +- Compare: Before/After (Artifacts + HostInstances) → JSON, CSV, HTML, Console +- Farbiges Console-Logging (wie Original) +- Stabilere WMI-Zugriffe (Fallback auf CIM für PS 7.x) +- Fix: HostInstance-Status (int ↔ text) robust +#> + +# ============================= PARAMETERS ============================= +param( + [switch]$Before, + [switch]$After, + [switch]$Compare, + [switch]$Diag, + [switch]$Shutdown, + [switch]$Restore, + [switch]$PlanOnly, + [string]$Server, + [string]$MgmtDbServer, + [string]$MgmtDbName, + [string]$OutDir = ".", + [string]$StateFile = "before.json", + [int]$WaitTimeoutSec = 300, + [int]$PollIntervalSec = 5 +) + +if (-not $Server) { $Server = $env:COMPUTERNAME } +$ErrorActionPreference = "Stop" +$Global:LogFile = Join-Path (Get-Location) "BizTalkPlatformManagementTool.log" + +# ============================= LOGGING ============================= +function Write-Log { + param([string]$Message,[ValidateSet('INFO','WARN','ERROR','DEBUG')]$Level='INFO') + $ts = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") + $line = "[$ts][$Level] $Message" + $fg = switch ($Level) { INFO{'Gray'} WARN{'Yellow'} ERROR{'Red'} DEBUG{'DarkCyan'} } + Write-Host $line -ForegroundColor $fg + Add-Content -Path $Global:LogFile -Value $line +} + +# ============================= UTILITIES ============================= +function Test-CommandExists { + param([Parameter(Mandatory)][string]$Name) + try { return [bool](Get-Command -Name $Name -ErrorAction Stop) } catch { return $false } +} + +function Invoke-BizTalkWmiQuery { + <# + Cross-PS-Version Wrapper. + - In PS 5.1 nutzt Get-WmiObject + - In PS 7.x (und falls WMI nicht verfügbar) nutzt Get-CimInstance + #> + param( + [Parameter(Mandatory)][string]$ClassName, + [Parameter()][string]$Namespace = 'root\MicrosoftBizTalkServer', + [Parameter()][string]$ComputerName = $env:COMPUTERNAME, + [switch]$ThrowOnError + ) + $useWmi = Test-CommandExists -Name 'Get-WmiObject' + try { + if ($useWmi) { + return Get-WmiObject -Class $ClassName -Namespace $Namespace -ComputerName $ComputerName -ErrorAction Stop + } else { + return Get-CimInstance -ClassName $ClassName -Namespace $Namespace -ComputerName $ComputerName -ErrorAction Stop + } + } + catch { + Write-Log "WMI/CIM query failed for ${ClassName}: $($_.Exception.Message)" 'ERROR' + if ($ThrowOnError) { throw } + return @() + } +} + +# ============================= STATUS MAPPING ============================= +function Convert-HostState { + param($Value) + if ($Value -is [int]) { return $Value } + switch ($Value.ToString().ToLower()) { + 'started' { return 4 } + 'startpending' { return 2 } + 'stoppending' { return 3 } + 'stopped' { return 1 } + default { return 0 } + } +} + +function Format-Status { + param([string]$ArtifactType, $Value) + + $r = [ordered]@{Text='Unknown'; Class='bad'; ConsoleColor='Red'} + + switch ($ArtifactType) { + 'ReceiveLocation' { + if ($Value -eq $true) { $r.Text='Enabled'; $r.Class='ok'; $r.ConsoleColor='Green' } + elseif ($Value -eq $false) { $r.Text='Disabled'; $r.Class='bad'; $r.ConsoleColor='Red' } + } + 'SendPort' { + switch ([int]$Value) { + 3 { $r.Text='Started'; $r.Class='ok'; $r.ConsoleColor='Green' } + 2 { $r.Text='Stopped'; $r.Class='bad'; $r.ConsoleColor='Red' } + 1 { $r.Text='Bound'; $r.Class='bad'; $r.ConsoleColor='Red' } + default { $r.Text='Unknown'; $r.Class='bad'; $r.ConsoleColor='Red' } + } + } + 'Orchestration' { + switch ([int]$Value) { + 4 { $r.Text='Started'; $r.Class='ok'; $r.ConsoleColor='Green' } + 3 { $r.Text='Stopped'; $r.Class='bad'; $r.ConsoleColor='Red' } + 2 { $r.Text='Bound'; $r.Class='bad'; $r.ConsoleColor='Red' } + 1 { $r.Text='Unbound'; $r.Class='bad'; $r.ConsoleColor='Red' } + default { $r.Text='Unknown'; $r.Class='bad'; $r.ConsoleColor='Red' } + } + } + 'HostInstance' { + $v = Convert-HostState $Value + switch ($v) { + 4 { $r.Text='Started'; $r.Class='ok'; $r.ConsoleColor='Green' } + 2 { $r.Text='StartPending'; $r.Class='warn'; $r.ConsoleColor='Yellow' } + 3 { $r.Text='StopPending'; $r.Class='warn'; $r.ConsoleColor='Yellow' } + 1 { $r.Text='Stopped'; $r.Class='bad'; $r.ConsoleColor='Red' } + default { $r.Text='Unknown'; $r.Class='bad'; $r.ConsoleColor='Red' } + } + } + } + return [pscustomobject]$r +} + +# ============================= MGMT DB ============================= +function Get-BizTalkMgmtDbInfo { + param($MgmtDbServer,$MgmtDbName) + try { + $p = Get-ItemProperty 'HKLM:SOFTWARE\\Microsoft\\BizTalk Server\\3.0\\Administration' + if (-not $MgmtDbServer) { $MgmtDbServer = $p.MgmtDBServer } + if (-not $MgmtDbName) { $MgmtDbName = $p.MgmtDBName } + } catch { + if (-not $MgmtDbServer) { $MgmtDbServer = 'localhost' } + if (-not $MgmtDbName) { $MgmtDbName = 'BizTalkMgmtDb' } + } + return @{ Server=$MgmtDbServer; Name=$MgmtDbName } +} + +# ============================= HOST INSTANCES ============================= +function Get-BizTalkHostInstances { + param([string]$Server) + + $list = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_HostInstance' -ComputerName $Server + + $result = foreach ($hi in $list) { + $st = Format-Status -ArtifactType 'HostInstance' -Value $hi.ServiceState + [pscustomobject]@{ + InstanceName = $hi.InstanceName + HostName = $hi.HostName + Server = $hi.RunningServer + RawState = $hi.ServiceState + StateText = $st.Text + StateClass = $st.Class + ConsoleColor = $st.ConsoleColor + } + } + + return $result +} + +# ============================= CONTROL HELPERS ============================= +function Get-BizTalkStateFile { + param([Parameter(Mandatory)][string]$Path) + if (-not (Test-Path $Path)) { + throw "State file not found: $Path" + } + return (Get-Content $Path -Raw | ConvertFrom-Json) +} + +function Get-ReturnCode { + param($Result) + if ($null -eq $Result) { return $null } + if ($Result.PSObject.Properties.Name -contains 'ReturnValue') { return [int]$Result.ReturnValue } + if ($Result -is [int] -or $Result -is [uint32]) { return [int]$Result } + return $null +} + +function Assert-BizTalkMethodResult { + param( + [Parameter(Mandatory)][string]$Action, + $Result + ) + + $code = Get-ReturnCode -Result $Result + if ($null -ne $code -and $code -ne 0) { + throw "$Action failed with HRESULT/ReturnValue $code" + } +} + +function Invoke-BizTalkObjectMethod { + param( + [Parameter(Mandatory)]$InputObject, + [Parameter(Mandatory)][string]$MethodName, + [hashtable]$CimArguments = @{}, + [object[]]$WmiArguments = @(), + [Parameter(Mandatory)][string]$Action, + [switch]$PlanOnly + ) + + if ($PlanOnly) { + Write-Log "PLAN: $Action" + return + } + + Write-Log $Action + + try { + if ($InputObject -is [Microsoft.Management.Infrastructure.CimInstance]) { + if ($CimArguments.Count -gt 0) { + $result = Invoke-CimMethod -InputObject $InputObject -MethodName $MethodName -Arguments $CimArguments -ErrorAction Stop + } else { + $result = Invoke-CimMethod -InputObject $InputObject -MethodName $MethodName -ErrorAction Stop + } + } else { + $args = if ($WmiArguments.Count -gt 0) { [object[]]$WmiArguments } else { $null } + $result = $InputObject.InvokeMethod($MethodName, $args) + } + + Assert-BizTalkMethodResult -Action $Action -Result $result + Write-Log "OK: $Action" + } + catch { + Write-Log "FAILED: $Action - $($_.Exception.Message)" 'ERROR' + throw + } +} + +function New-NameMap { + param( + [Parameter(Mandatory)][AllowEmptyCollection()]$Items, + [Parameter(Mandatory)][string]$KeyProperty + ) + + $map = @{} + foreach ($item in $Items) { + $key = $item.$KeyProperty + if (-not $key) { continue } + if ($map.ContainsKey($key)) { + Write-Log "Duplicate WMI key '$key' for property '$KeyProperty'; keeping first object." 'WARN' + continue + } + $map[$key] = $item + } + return $map +} + +function Wait-BizTalkCondition { + param( + [Parameter(Mandatory)][scriptblock]$Condition, + [Parameter(Mandatory)][string]$Description, + [int]$TimeoutSec = 300, + [int]$PollIntervalSec = 5, + [switch]$PlanOnly + ) + + if ($PlanOnly) { return } + + $deadline = (Get-Date).AddSeconds($TimeoutSec) + do { + if (& $Condition) { + Write-Log "Reached: $Description" + return + } + Start-Sleep -Seconds $PollIntervalSec + } while ((Get-Date) -lt $deadline) + + throw "Timeout after ${TimeoutSec}s while waiting for: $Description" +} + +function Get-CurrentBizTalkObjectMaps { + param([Parameter(Mandatory)][string]$Server) + + $receiveLocations = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_ReceiveLocation' -ComputerName $Server -ThrowOnError + $sendPorts = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_SendPort' -ComputerName $Server -ThrowOnError + $orchestrations = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_Orchestration' -ComputerName $Server -ThrowOnError + $hostInstances = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_HostInstance' -ComputerName $Server -ThrowOnError + + return [pscustomobject]@{ + ReceiveLocations = New-NameMap -Items $receiveLocations -KeyProperty 'Name' + SendPorts = New-NameMap -Items $sendPorts -KeyProperty 'Name' + Orchestrations = New-NameMap -Items $orchestrations -KeyProperty 'Name' + HostInstances = New-NameMap -Items $hostInstances -KeyProperty 'InstanceName' + } +} + +function Get-SnapshotArtifacts { + param([Parameter(Mandatory)]$Snapshot) + + $receiveLocations = @() + $sendPorts = @() + $orchestrations = @() + + foreach ($app in $Snapshot.Applications) { + foreach ($item in $app.ReceiveLocations) { + $receiveLocations += [pscustomobject]@{ Application=$app.Application; Name=$item.Name; Enabled=$item.Enabled } + } + foreach ($item in $app.SendPorts) { + $sendPorts += [pscustomobject]@{ Application=$app.Application; Name=$item.Name; Status=[int]$item.Status } + } + foreach ($item in $app.Orchestrations) { + $orchestrations += [pscustomobject]@{ Application=$app.Application; Name=$item.Name; Status=[int]$item.OrchestrationStatus } + } + } + + return [pscustomobject]@{ + ReceiveLocations = $receiveLocations + SendPorts = $sendPorts + Orchestrations = $orchestrations + HostInstances = @($Snapshot.HostInstances) + } +} + +function Export-BizTalkOperationPlan { + param( + [Parameter(Mandatory)][string]$Path, + [Parameter(Mandatory)][string]$Mode, + [Parameter(Mandatory)]$Artifacts + ) + + $plan = [pscustomobject]@{ + Mode = $Mode + CreatedAt = (Get-Date).ToString('s') + ReceiveLocations = @($Artifacts.ReceiveLocations) + SendPorts = @($Artifacts.SendPorts) + Orchestrations = @($Artifacts.Orchestrations) + HostInstances = @($Artifacts.HostInstances) + } + + $plan | ConvertTo-Json -Depth 8 | Out-File $Path -Encoding UTF8 + Write-Log "Operation plan exported: $Path" +} + +function Stop-BizTalkRuntimeFromSnapshot { + param( + [Parameter(Mandatory)]$Snapshot, + [Parameter(Mandatory)][string]$Server, + [int]$WaitTimeoutSec, + [int]$PollIntervalSec, + [switch]$PlanOnly + ) + + $artifacts = Get-SnapshotArtifacts -Snapshot $Snapshot + Export-BizTalkOperationPlan -Path 'shutdown-plan.json' -Mode 'Shutdown' -Artifacts $artifacts + + $maps = Get-CurrentBizTalkObjectMaps -Server $Server + + foreach ($rl in $artifacts.ReceiveLocations | Where-Object { $_.Enabled -eq $true }) { + if (-not $maps.ReceiveLocations.ContainsKey($rl.Name)) { Write-Log "Receive location not found: $($rl.Name)" 'WARN'; continue } + $obj = $maps.ReceiveLocations[$rl.Name] + Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Disable' -Action "Disable receive location '$($rl.Name)'" -PlanOnly:$PlanOnly + Wait-BizTalkCondition -Description "Receive location '$($rl.Name)' disabled" -TimeoutSec $WaitTimeoutSec -PollIntervalSec $PollIntervalSec -PlanOnly:$PlanOnly -Condition { + $current = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_ReceiveLocation' -ComputerName $Server | Where-Object { $_.Name -eq $rl.Name } | Select-Object -First 1 + $current -and $current.IsDisabled -eq $true + } + } + + foreach ($orch in $artifacts.Orchestrations | Where-Object { $_.Status -eq 4 }) { + if (-not $maps.Orchestrations.ContainsKey($orch.Name)) { Write-Log "Orchestration not found: $($orch.Name)" 'WARN'; continue } + $obj = $maps.Orchestrations[$orch.Name] + Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Stop' -CimArguments @{ AutoDisableReceiveLocationFlag=[uint32]1; AutoSuspendServiceInstanceFlag=[uint32]1 } -WmiArguments @(1,1) -Action "Stop orchestration '$($orch.Name)'" -PlanOnly:$PlanOnly + Wait-BizTalkCondition -Description "Orchestration '$($orch.Name)' stopped" -TimeoutSec $WaitTimeoutSec -PollIntervalSec $PollIntervalSec -PlanOnly:$PlanOnly -Condition { + $current = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_Orchestration' -ComputerName $Server | Where-Object { $_.Name -eq $orch.Name } | Select-Object -First 1 + $current -and [int]$current.OrchestrationStatus -eq 3 + } + } + + foreach ($sp in $artifacts.SendPorts | Where-Object { $_.Status -eq 3 }) { + if (-not $maps.SendPorts.ContainsKey($sp.Name)) { Write-Log "Send port not found: $($sp.Name)" 'WARN'; continue } + $obj = $maps.SendPorts[$sp.Name] + Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Stop' -Action "Stop send port '$($sp.Name)'" -PlanOnly:$PlanOnly + Wait-BizTalkCondition -Description "Send port '$($sp.Name)' stopped" -TimeoutSec $WaitTimeoutSec -PollIntervalSec $PollIntervalSec -PlanOnly:$PlanOnly -Condition { + $current = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_SendPort' -ComputerName $Server | Where-Object { $_.Name -eq $sp.Name } | Select-Object -First 1 + $current -and [int]$current.Status -eq 2 + } + } + + foreach ($hi in $artifacts.HostInstances | Where-Object { (Convert-HostState $_.RawState) -eq 4 }) { + if ($hi.Server -and $hi.Server -ne $Server) { + Write-Log "Skipping host instance '$($hi.InstanceName)' on server '$($hi.Server)'. Run the shutdown on that server to stop it safely." 'WARN' + continue + } + if (-not $maps.HostInstances.ContainsKey($hi.InstanceName)) { Write-Log "Host instance not found: $($hi.InstanceName)" 'WARN'; continue } + $obj = $maps.HostInstances[$hi.InstanceName] + Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Stop' -Action "Stop host instance '$($hi.InstanceName)'" -PlanOnly:$PlanOnly + Wait-BizTalkCondition -Description "Host instance '$($hi.InstanceName)' stopped" -TimeoutSec $WaitTimeoutSec -PollIntervalSec $PollIntervalSec -PlanOnly:$PlanOnly -Condition { + $current = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_HostInstance' -ComputerName $Server | Where-Object { $_.InstanceName -eq $hi.InstanceName } | Select-Object -First 1 + $current -and (Convert-HostState $current.ServiceState) -eq 1 + } + } +} + +function Restore-BizTalkRuntimeFromSnapshot { + param( + [Parameter(Mandatory)]$Snapshot, + [Parameter(Mandatory)][string]$Server, + [int]$WaitTimeoutSec, + [int]$PollIntervalSec, + [switch]$PlanOnly + ) + + $artifacts = Get-SnapshotArtifacts -Snapshot $Snapshot + Export-BizTalkOperationPlan -Path 'restore-plan.json' -Mode 'Restore' -Artifacts $artifacts + + $maps = Get-CurrentBizTalkObjectMaps -Server $Server + + foreach ($hi in $artifacts.HostInstances | Where-Object { (Convert-HostState $_.RawState) -eq 4 }) { + if ($hi.Server -and $hi.Server -ne $Server) { + Write-Log "Skipping host instance '$($hi.InstanceName)' on server '$($hi.Server)'. Run the restore on that server to start it safely." 'WARN' + continue + } + if (-not $maps.HostInstances.ContainsKey($hi.InstanceName)) { Write-Log "Host instance not found: $($hi.InstanceName)" 'WARN'; continue } + $obj = $maps.HostInstances[$hi.InstanceName] + Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Start' -Action "Start host instance '$($hi.InstanceName)'" -PlanOnly:$PlanOnly + Wait-BizTalkCondition -Description "Host instance '$($hi.InstanceName)' started" -TimeoutSec $WaitTimeoutSec -PollIntervalSec $PollIntervalSec -PlanOnly:$PlanOnly -Condition { + $current = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_HostInstance' -ComputerName $Server | Where-Object { $_.InstanceName -eq $hi.InstanceName } | Select-Object -First 1 + $current -and (Convert-HostState $current.ServiceState) -eq 4 + } + } + + foreach ($sp in $artifacts.SendPorts) { + if (-not $maps.SendPorts.ContainsKey($sp.Name)) { Write-Log "Send port not found: $($sp.Name)" 'WARN'; continue } + $obj = $maps.SendPorts[$sp.Name] + $currentStatus = [int]$obj.Status + switch ($sp.Status) { + 3 { + Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Start' -Action "Start send port '$($sp.Name)'" -PlanOnly:$PlanOnly + } + 2 { + if ($currentStatus -eq 1) { + Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Enlist' -Action "Enlist send port '$($sp.Name)' to stopped state" -PlanOnly:$PlanOnly + } elseif ($currentStatus -eq 3) { + Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Stop' -Action "Stop send port '$($sp.Name)'" -PlanOnly:$PlanOnly + } else { + Write-Log "Send port already stopped: $($sp.Name)" + } + } + 1 { + Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'UnEnlist' -Action "Unenlist send port '$($sp.Name)'" -PlanOnly:$PlanOnly + } + } + Wait-BizTalkCondition -Description "Send port '$($sp.Name)' restored to status $($sp.Status)" -TimeoutSec $WaitTimeoutSec -PollIntervalSec $PollIntervalSec -PlanOnly:$PlanOnly -Condition { + $current = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_SendPort' -ComputerName $Server | Where-Object { $_.Name -eq $sp.Name } | Select-Object -First 1 + $current -and [int]$current.Status -eq $sp.Status + } + } + + foreach ($orch in $artifacts.Orchestrations) { + if (-not $maps.Orchestrations.ContainsKey($orch.Name)) { Write-Log "Orchestration not found: $($orch.Name)" 'WARN'; continue } + $obj = $maps.Orchestrations[$orch.Name] + $currentStatus = [int]$obj.OrchestrationStatus + switch ($orch.Status) { + 4 { + if ($currentStatus -eq 4) { + Write-Log "Orchestration already started: $($orch.Name)" + } else { + Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Start' -CimArguments @{ AutoEnableReceiveLocationFlag=[uint32]1; AutoResumeOrchestrationInstanceFlag=[uint32]1; AutoStartSendPortsFlag=[uint32]1 } -WmiArguments @(1,1,1) -Action "Start orchestration '$($orch.Name)'" -PlanOnly:$PlanOnly + } + } + 3 { + if ($currentStatus -eq 4) { + Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Stop' -CimArguments @{ AutoDisableReceiveLocationFlag=[uint32]1; AutoSuspendServiceInstanceFlag=[uint32]1 } -WmiArguments @(1,1) -Action "Stop orchestration '$($orch.Name)'" -PlanOnly:$PlanOnly + } elseif ($currentStatus -eq 3) { + Write-Log "Orchestration already stopped: $($orch.Name)" + } else { + Write-Log "Orchestration '$($orch.Name)' target is Stopped, current status is $currentStatus. Leaving unchanged." 'WARN' + } + } + 2 { + Write-Log "Orchestration '$($orch.Name)' was Bound in the snapshot. Bound restore is left unchanged to avoid accidentally changing it to Unbound." 'WARN' + } + 1 { + if ($currentStatus -eq 1) { + Write-Log "Orchestration already unbound: $($orch.Name)" + } else { + Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'UnenlistService' -CimArguments @{ AutoTerminateOrchestrationInstanceFlag=[uint32]1 } -WmiArguments @(1) -Action "Unenlist orchestration '$($orch.Name)'" -PlanOnly:$PlanOnly + } + } + } + if ($orch.Status -ne 2) { + Wait-BizTalkCondition -Description "Orchestration '$($orch.Name)' restored to status $($orch.Status)" -TimeoutSec $WaitTimeoutSec -PollIntervalSec $PollIntervalSec -PlanOnly:$PlanOnly -Condition { + $current = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_Orchestration' -ComputerName $Server | Where-Object { $_.Name -eq $orch.Name } | Select-Object -First 1 + $current -and [int]$current.OrchestrationStatus -eq $orch.Status + } + } + } + + foreach ($rl in $artifacts.ReceiveLocations) { + if (-not $maps.ReceiveLocations.ContainsKey($rl.Name)) { Write-Log "Receive location not found: $($rl.Name)" 'WARN'; continue } + $obj = $maps.ReceiveLocations[$rl.Name] + if ($rl.Enabled -eq $true) { + Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Enable' -Action "Enable receive location '$($rl.Name)'" -PlanOnly:$PlanOnly + } else { + Invoke-BizTalkObjectMethod -InputObject $obj -MethodName 'Disable' -Action "Keep receive location disabled '$($rl.Name)'" -PlanOnly:$PlanOnly + } + Wait-BizTalkCondition -Description "Receive location '$($rl.Name)' restored" -TimeoutSec $WaitTimeoutSec -PollIntervalSec $PollIntervalSec -PlanOnly:$PlanOnly -Condition { + $current = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_ReceiveLocation' -ComputerName $Server | Where-Object { $_.Name -eq $rl.Name } | Select-Object -First 1 + $current -and ((-not $current.IsDisabled) -eq [bool]$rl.Enabled) + } + } +} + +# ============================= HTML HELPERS ============================= +function New-Html-Header { + param([string]$Title) +@" + + + + +$Title + + +

$Title

+"@ +} + +function New-Html-Footer { "" } + +# ============================= SNAPSHOT ============================= +function Get-BizTalkSnapshot { + param( + [string]$OutputFile, + [string]$Server, + [string]$MgmtDbServer, + [string]$MgmtDbName + ) + + Write-Log "Snapshot: Server=$Server File=$OutputFile" + + [void][System.Reflection.Assembly]::LoadWithPartialName('Microsoft.BizTalk.ExplorerOM') + $db = Get-BizTalkMgmtDbInfo -MgmtDbServer $MgmtDbServer -MgmtDbName $MgmtDbName + + $cat = New-Object Microsoft.BizTalk.ExplorerOM.BtsCatalogExplorer + $cat.ConnectionString = "Server={0};Database={1};Integrated Security=SSPI" -f $db.Server,$db.Name + + $apps = $cat.Applications + + # WMI lookup tables + $rlW = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_ReceiveLocation' -ComputerName $Server | + Select-Object Name, @{n='Enabled';e={-not $_.IsDisabled}}, AdapterName, @{n='Address';e={$_.InboundTransportURL}} + + $spW = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_SendPort' -ComputerName $Server | + Select-Object Name, @{n='Status';e={$_.Status}}, @{n='PrimaryTransportType';e={$_.PTTransportType}}, @{n='PrimaryTransportAddress';e={$_.PTAddress}} + + $orW = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_Orchestration' -ComputerName $Server | + Select-Object Name, @{n='OrchestrationStatus';e={$_.OrchestrationStatus}} + + $mapRL=@{}; foreach($i in $rlW){$mapRL[$i.Name]=$i} + $mapSP=@{}; foreach($i in $spW){$mapSP[$i.Name]=$i} + $mapOR=@{}; foreach($i in $orW){$mapOR[$i.Name]=$i} + + $snapshot=@() + + foreach ($app in $apps) { + $rlItems=@() + foreach ($rp in $app.ReceivePorts) { + foreach ($rl in $rp.ReceiveLocations) { + if ($mapRL.ContainsKey($rl.Name)) { + $w = $mapRL[$rl.Name] + $rlItems += [pscustomobject]@{ + Name=$rl.Name + Enabled=$w.Enabled + AdapterName=$w.AdapterName + Address=$w.Address + } + } + } + } + + $spItems=@() + foreach ($sp in $app.SendPorts) { + if ($mapSP.ContainsKey($sp.Name)) { + $w = $mapSP[$sp.Name] + $spItems += [pscustomobject]@{ + Name=$sp.Name + Status=$w.Status + PrimaryTransportType=$w.PrimaryTransportType + PrimaryTransportAddress=$w.PrimaryTransportAddress + } + } + } + + $orItems=@() + foreach ($o in $app.Orchestrations) { + $key = if ($mapOR.ContainsKey($o.FullName)) { $o.FullName } else { $o.Name } + if ($mapOR.ContainsKey($key)) { + $w = $mapOR[$key] + $orItems += [pscustomobject]@{ + Name=$o.FullName + OrchestrationStatus=$w.OrchestrationStatus + } + } + } + + $snapshot += [pscustomobject]@{ + Application=$app.Name + ReceiveLocations=$rlItems + SendPorts=$spItems + Orchestrations=$orItems + } + } + + # Host Instances + $hostInstances = Get-BizTalkHostInstances -Server $Server + + # Export JSON (Applications + HostInstances) + $full = [pscustomobject]@{ + Applications = $snapshot + HostInstances = $hostInstances + } + + $full | ConvertTo-Json -Depth 8 | Out-File $OutputFile -Encoding UTF8 + + # Export CSV (Artifacts) + $csvArtifact = foreach ($app in $snapshot) { + foreach ($rl in $app.ReceiveLocations) { + $st = Format-Status -ArtifactType 'ReceiveLocation' -Value $rl.Enabled + [pscustomobject]@{ + Application=$app.Application + Type='ReceiveLocation' + Name=$rl.Name + Status=$st.Text + } + } + foreach ($sp in $app.SendPorts) { + $st = Format-Status -ArtifactType 'SendPort' -Value $sp.Status + [pscustomobject]@{ + Application=$app.Application + Type='SendPort' + Name=$sp.Name + Status=$st.Text + } + } + foreach ($o in $app.Orchestrations) { + $st = Format-Status -ArtifactType 'Orchestration' -Value $o.OrchestrationStatus + [pscustomobject]@{ + Application=$app.Application + Type='Orchestration' + Name=$o.Name + Status=$st.Text + } + } + } + + $csvArtifact | + Export-Csv -Encoding UTF8 -NoTypeInformation -Path ($OutputFile + '.csv') + + # Export CSV (HostInstances) + $hostInstances | + Select-Object InstanceName,HostName,Server,StateText | + Export-Csv -NoTypeInformation -Encoding UTF8 -Path ($OutputFile + '.hosts.csv') + + # HTML Snapshot (modernisiert) + $html = @() + $html += New-Html-Header -Title 'BizTalk Snapshot' + + # Summary + $totalApps = ($snapshot | Measure-Object).Count + $totalRL = ($snapshot.ReceiveLocations | Measure-Object).Count + $totalSP = ($snapshot.SendPorts | Measure-Object).Count + $totalOR = ($snapshot.Orchestrations | Measure-Object).Count + $totalHI = ($hostInstances | Measure-Object).Count + + $html += "
" + $html += "
Applications
$totalApps
" + $html += "
Receive Locations
$totalRL
" + $html += "
Send Ports
$totalSP
" + $html += "
Orchestrations
$totalOR
" + $html += "
Host Instances
$totalHI
" + $html += "
" + + foreach ($app in $snapshot) { + $html += "
" + $html += "

Application: $($app.Application)

" + + if ($app.ReceiveLocations.Count -gt 0) { + $html += "

Receive Locations

" + foreach ($rl in $app.ReceiveLocations) { + $st = Format-Status -ArtifactType 'ReceiveLocation' -Value $rl.Enabled + $html += "" + } + $html += "
NameStatusAdapterAddress
$($rl.Name)$($st.Text)$($rl.AdapterName)$($rl.Address)
" + } + + if ($app.SendPorts.Count -gt 0) { + $html += "

Send Ports

" + foreach ($sp in $app.SendPorts) { + $st = Format-Status -ArtifactType 'SendPort' -Value $sp.Status + $html += "" + } + $html += "
NameStatusPrimaryTransportTypePrimaryTransportAddress
$($sp.Name)$($st.Text)$($sp.PrimaryTransportType)$($sp.PrimaryTransportAddress)
" + } + + if ($app.Orchestrations.Count -gt 0) { + $html += "

Orchestrations

" + foreach ($o in $app.Orchestrations) { + $st = Format-Status -ArtifactType 'Orchestration' -Value $o.OrchestrationStatus + $html += "" + } + $html += "
NameStatus
$($o.Name)$($st.Text)
" + } + $html += "
" + } + + # Host Instances + $html += "
" + $html += "

Host Instances

" + $html += "" + foreach ($hi in $hostInstances) { + $html += "" + } + $html += "
InstanceHostServerStatus
$($hi.InstanceName)$($hi.HostName)$($hi.Server)$($hi.StateText)
" + $html += "
" + + $html += New-Html-Footer + $html -join "`n" | Out-File ($OutputFile + '.html') -Encoding UTF8 + + # Console Output (wie Original) + Write-Host "`n===== Snapshot =====" -ForegroundColor Yellow + foreach ($app in $snapshot) { + Write-Host "`nApplication: $($app.Application)" -ForegroundColor Cyan + + foreach ($rl in $app.ReceiveLocations) { + $st = Format-Status -ArtifactType 'ReceiveLocation' -Value $rl.Enabled + Write-Host (" RL {0,-60} {1}" -f $rl.Name,$st.Text) -ForegroundColor $st.ConsoleColor + } + + foreach ($sp in $app.SendPorts) { + $st = Format-Status -ArtifactType 'SendPort' -Value $sp.Status + Write-Host (" SP {0,-60} {1}" -f $sp.Name,$st.Text) -ForegroundColor $st.ConsoleColor + } + + foreach ($o in $app.Orchestrations) { + $st = Format-Status -ArtifactType 'Orchestration' -Value $o.OrchestrationStatus + Write-Host (" OR {0,-60} {1}" -f $o.Name,$st.Text) -ForegroundColor $st.ConsoleColor + } + } + + # Host Instances (Konsole) + Write-Host "`n=== Host Instances ===" -ForegroundColor Yellow + foreach ($hi in $hostInstances) { + Write-Host ("{0,-35} {1,-20} {2,-10}" -f $hi.InstanceName,$hi.HostName,$hi.StateText) -ForegroundColor $hi.ConsoleColor + } + + Write-Host "Snapshot erstellt: $(Resolve-Path $OutputFile)" -ForegroundColor Green +} + +# ============================= COMPARE ============================= +function Compare-BizTalkSnapshots { + param( + [string]$Before, + [string]$After, + [string]$HtmlReport = 'BizTalkDiff.html' + ) + + Write-Log "Vergleiche Snapshots: $Before ↔ $After" + + $b = Get-Content $Before | ConvertFrom-Json + $a = Get-Content $After | ConvertFrom-Json + + $diff = New-Object System.Collections.ArrayList + + function AddDiff { + param($app,$type,$name,$before,$after) + $bf = Format-Status -ArtifactType $type -Value $before + $af = Format-Status -ArtifactType $type -Value $after + [void]$diff.Add([pscustomobject]@{ + Application = $app + ArtifactType= $type + Name = $name + BeforeText = $bf.Text + BeforeClass = $bf.Class + AfterText = $af.Text + AfterClass = $af.Class + }) + } + + foreach ($appAfter in $a.Applications) { + $appBefore = $b.Applications | Where-Object {$_.Application -eq $appAfter.Application} + + # RECEIVE LOCATIONS + foreach ($x in $appAfter.ReceiveLocations) { + $old = $appBefore.ReceiveLocations | Where-Object {$_.Name -eq $x.Name} + if (-not $old) { AddDiff $appAfter.Application 'ReceiveLocation' $x.Name $null $x.Enabled; continue } + if ($old.Enabled -ne $x.Enabled) { AddDiff $appAfter.Application 'ReceiveLocation' $x.Name $old.Enabled $x.Enabled } + } + foreach ($x in $appBefore.ReceiveLocations) { + if (-not ($appAfter.ReceiveLocations | Where-Object {$_.Name -eq $x.Name})) { + AddDiff $appBefore.Application 'ReceiveLocation' $x.Name $x.Enabled $null + } + } + + # SEND PORTS + foreach ($x in $appAfter.SendPorts) { + $old = $appBefore.SendPorts | Where-Object {$_.Name -eq $x.Name} + if (-not $old) { AddDiff $appAfter.Application 'SendPort' $x.Name $null $x.Status; continue } + if ($old.Status -ne $x.Status) { AddDiff $appAfter.Application 'SendPort' $x.Name $old.Status $x.Status } + } + foreach ($x in $appBefore.SendPorts) { + if (-not ($appAfter.SendPorts | Where-Object {$_.Name -eq $x.Name})) { + AddDiff $appBefore.Application 'SendPort' $x.Name $x.Status $null + } + } + + # ORCHESTRATIONS + foreach ($x in $appAfter.Orchestrations) { + $old = $appBefore.Orchestrations | Where-Object {$_.Name -eq $x.Name} + if (-not $old) { AddDiff $appAfter.Application 'Orchestration' $x.Name $null $x.OrchestrationStatus; continue } + if ($old.OrchestrationStatus -ne $x.OrchestrationStatus) { + AddDiff $appAfter.Application 'Orchestration' $x.Name $old.OrchestrationStatus $x.OrchestrationStatus + } + } + foreach ($x in $appBefore.Orchestrations) { + if (-not ($appAfter.Orchestrations | Where-Object {$_.Name -eq $x.Name})) { + AddDiff $appBefore.Application 'Orchestration' $x.Name $x.OrchestrationStatus $null + } + } + } + + # ======================== HOST INSTANCE DIFF ======================== + $beforeHI = $b.HostInstances + $afterHI = $a.HostInstances + + $hiDiff_New=@() + $hiDiff_Removed=@() + $hiDiff_Changed=@() + + foreach ($hi in $afterHI) { + if (-not ($beforeHI | Where-Object {$_.InstanceName -eq $hi.InstanceName})) { + $hiDiff_New += $hi + } + } + + foreach ($hi in $beforeHI) { + if (-not ($afterHI | Where-Object {$_.InstanceName -eq $hi.InstanceName})) { + $hiDiff_Removed += $hi + } + } + + foreach ($hiAfter in $afterHI) { + $hiBefore = $beforeHI | Where-Object {$_.InstanceName -eq $hiAfter.InstanceName} + if ($hiBefore) { + if ($hiBefore.StateText -ne $hiAfter.StateText) { + $hiDiff_Changed += [pscustomobject]@{ + Instance = $hiAfter.InstanceName + Before = $hiBefore.StateText + After = $hiAfter.StateText + } + } + } + } + + # ======================== JSON DIFF EXPORT ======================== + $jsonDiff = [pscustomobject]@{ + ArtifactsDiff = $diff + HostInstances = [pscustomobject]@{ + New = $hiDiff_New + Removed = $hiDiff_Removed + Changed = $hiDiff_Changed + } + } + + $jsonDiff | ConvertTo-Json -Depth 8 | Out-File 'diff.json' -Encoding UTF8 + + # ======================== CSV DIFF EXPORT ========================= + $diff | Export-Csv -Path 'diff.csv' -NoTypeInformation -Encoding UTF8 + # HostInstance CSVs separat bereitstellen + $hiDiff_New | Export-Csv -Path 'diff.hosts.new.csv' -NoTypeInformation -Encoding UTF8 + $hiDiff_Removed | Export-Csv -Path 'diff.hosts.removed.csv' -NoTypeInformation -Encoding UTF8 + $hiDiff_Changed | Export-Csv -Path 'diff.hosts.changed.csv' -NoTypeInformation -Encoding UTF8 + + # ======================== HTML DIFF EXPORT ======================== + $html=@() + $html+= New-Html-Header -Title 'BizTalk Differences' + + # Summary-Karten + $artCount = ($diff | Measure-Object).Count + $hiNew = ($hiDiff_New | Measure-Object).Count + $hiRem = ($hiDiff_Removed | Measure-Object).Count + $hiChg = ($hiDiff_Changed | Measure-Object).Count + + $html += "
" + $html += "
Artifact Differences
$artCount
" + $html += "
Hosts New
$hiNew
" + $html += "
Hosts Removed
$hiRem
" + $html += "
Hosts Changed
$hiChg
" + $html += "
" + + # Artifacts + $html += "
" + $html += "

Artifact Differences

" + $html += "" + foreach ($d in $diff) { + $html += "" + } + $html += "
ApplicationTypeNameBeforeAfter
$($d.Application)$($d.ArtifactType)$($d.Name)$($d.BeforeText)$($d.AfterText)
" + $html += "
" + + # HostInstance: NEW + $html += "
" + $html += "

Host Instance Changes

" + $html += "

New

" + foreach ($hi in $hiDiff_New) { + $html += "" + } + $html += "
InstanceHostServerStatus
$($hi.InstanceName)$($hi.HostName)$($hi.Server)$($hi.StateText)
" + + # HostInstance: REMOVED + $html += "

Removed

" + foreach ($hi in $hiDiff_Removed) { + $html += "" + } + $html += "
InstanceHostServerLast Status
$($hi.InstanceName)$($hi.HostName)$($hi.Server)$($hi.StateText)
" + + # HostInstance: CHANGED + $html += "

Changed

" + foreach ($hi in $hiDiff_Changed) { + $before = Format-Status -ArtifactType 'HostInstance' -Value $hi.Before + $after = Format-Status -ArtifactType 'HostInstance' -Value $hi.After + $html += "" + } + $html += "
InstanceBeforeAfter
$($hi.Instance)$($hi.Before)$($hi.After)
" + $html += "
" + + $html += New-Html-Footer + $html -join "`n" | Out-File $HtmlReport -Encoding UTF8 + + # ======================== CONSOLE OUTPUT ========================= + Write-Host "`n===== BizTalk DIFF =====" -ForegroundColor Yellow + + if ($diff.Count -eq 0 -and $hiDiff_New.Count -eq 0 -and $hiDiff_Removed.Count -eq 0 -and $hiDiff_Changed.Count -eq 0) { + Write-Host "Keine Unterschiede." -ForegroundColor Green + } + else { + Write-Host "`n--- Artifacts ---" -ForegroundColor Cyan + foreach ($d in $diff) { + Write-Host "`nApplication: $($d.Application)" -ForegroundColor Cyan + Write-Host "Artifact: $($d.ArtifactType)" + Write-Host "Name: $($d.Name)" + Write-Host ("Before: {0}" -f $d.BeforeText) -ForegroundColor $(if ($d.BeforeClass -eq 'ok') {'Green'} elseif ($d.BeforeClass -eq 'warn') {'Yellow'} else {'Red'}) + Write-Host ("After: {0}" -f $d.AfterText) -ForegroundColor $(if ($d.AfterClass -eq 'ok') {'Green'} elseif ($d.AfterClass -eq 'warn') {'Yellow'} else {'Red'}) + } + + Write-Host "`n--- Host Instances (New) ---" -ForegroundColor Cyan + foreach ($hi in $hiDiff_New) { + Write-Host ("NEW: {0} {1}" -f $hi.InstanceName,$hi.StateText) -ForegroundColor $hi.ConsoleColor + } + + Write-Host "`n--- Host Instances (Removed) ---" -ForegroundColor Cyan + foreach ($hi in $hiDiff_Removed) { + Write-Host ("REMOVED: {0} {1}" -f $hi.InstanceName,$hi.StateText) -ForegroundColor Red + } + + Write-Host "`n--- Host Instances (Changed) ---" -ForegroundColor Cyan + foreach ($hi in $hiDiff_Changed) { + Write-Host ("CHANGED: {0} {1} -> {2}" -f $hi.Instance,$hi.Before,$hi.After) -ForegroundColor Yellow + } + } + + Write-Host "HTML Diff exportiert: $HtmlReport" -ForegroundColor Green +} + +# ============================= DISPATCH ============================= +try { + if (-not (Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir | Out-Null } + Set-Location $OutDir + Write-Log "Working dir: $(Get-Location)" + + if ($Diag) { + Write-Host "DIAG → Server: $Server" -ForegroundColor Yellow + try { + $sp = Invoke-BizTalkWmiQuery -ClassName 'MSBTS_SendPort' -ComputerName $Server | Select-Object -First 3 Name,Status + Write-Host "WMI/CIM OK:" -ForegroundColor Green + $sp | Format-Table + } catch { + Write-Host "ERROR: $($_.Exception.Message)" -ForegroundColor Red + } + exit + } + + if ($Shutdown) { + Get-BizTalkSnapshot -OutputFile $StateFile -Server $Server -MgmtDbServer $MgmtDbServer -MgmtDbName $MgmtDbName + $snapshot = Get-BizTalkStateFile -Path $StateFile + Stop-BizTalkRuntimeFromSnapshot -Snapshot $snapshot -Server $Server -WaitTimeoutSec $WaitTimeoutSec -PollIntervalSec $PollIntervalSec -PlanOnly:$PlanOnly + if (-not $PlanOnly) { + Get-BizTalkSnapshot -OutputFile 'shutdown-after.json' -Server $Server -MgmtDbServer $MgmtDbServer -MgmtDbName $MgmtDbName + } + exit + } + + if ($Restore) { + $snapshot = Get-BizTalkStateFile -Path $StateFile + Restore-BizTalkRuntimeFromSnapshot -Snapshot $snapshot -Server $Server -WaitTimeoutSec $WaitTimeoutSec -PollIntervalSec $PollIntervalSec -PlanOnly:$PlanOnly + if (-not $PlanOnly) { + Get-BizTalkSnapshot -OutputFile 'restore-after.json' -Server $Server -MgmtDbServer $MgmtDbServer -MgmtDbName $MgmtDbName + } + exit + } + + if ($Before) { Get-BizTalkSnapshot -OutputFile 'before.json' -Server $Server -MgmtDbServer $MgmtDbServer -MgmtDbName $MgmtDbName; exit } + if ($After) { Get-BizTalkSnapshot -OutputFile 'after.json' -Server $Server -MgmtDbServer $MgmtDbServer -MgmtDbName $MgmtDbName; exit } + if ($Compare){ Compare-BizTalkSnapshots -Before 'before.json' -After 'after.json'; exit } + + Write-Host "Usage:" + Write-Host " .\\BizTalkPlatformManagementTool.ps1 -Before" + Write-Host " .\\BizTalkPlatformManagementTool.ps1 -After" + Write-Host " .\\BizTalkPlatformManagementTool.ps1 -Compare" + Write-Host " .\\BizTalkPlatformManagementTool.ps1 -Diag" + Write-Host " .\\BizTalkPlatformManagementTool.ps1 -Shutdown [-PlanOnly] [-StateFile before.json]" + Write-Host " .\\BizTalkPlatformManagementTool.ps1 -Restore [-PlanOnly] [-StateFile before.json]" +} +catch { + Write-Log "Fatal: $($_.Exception.Message)" 'ERROR' + throw +} diff --git a/src/BizTalkPlatformManagementTool/App.config b/src/BizTalkPlatformManagementTool/App.config new file mode 100644 index 0000000..f401e47 --- /dev/null +++ b/src/BizTalkPlatformManagementTool/App.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj b/src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj new file mode 100644 index 0000000..47ecbce --- /dev/null +++ b/src/BizTalkPlatformManagementTool/BizTalkPlatformManagementTool.csproj @@ -0,0 +1,67 @@ + + + + + Debug + AnyCPU + {2C5B2C0A-F407-46C2-9E3B-1FA09FA8445A} + WinExe + BizTalkPlatformManagementTool + BizTalkPlatformManagementTool + v4.7.1 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/BizTalkPlatformManagementTool/Models/ArtifactStates.cs b/src/BizTalkPlatformManagementTool/Models/ArtifactStates.cs new file mode 100644 index 0000000..6809fa3 --- /dev/null +++ b/src/BizTalkPlatformManagementTool/Models/ArtifactStates.cs @@ -0,0 +1,59 @@ +namespace BizTalkPlatformManagementTool.Models +{ + public static class ArtifactStates + { + public const int SendPortBound = 1; + public const int SendPortStopped = 2; + public const int SendPortStarted = 3; + + public const int OrchestrationUnbound = 1; + public const int OrchestrationBound = 2; + public const int OrchestrationStopped = 3; + public const int OrchestrationStarted = 4; + + public const int HostStopped = 1; + public const int HostStartPending = 2; + public const int HostStopPending = 3; + public const int HostStarted = 4; + + public static string FormatReceiveLocation(bool enabled) + { + return enabled ? "Enabled" : "Disabled"; + } + + public static string FormatSendPort(int status) + { + switch (status) + { + case SendPortStarted: return "Started"; + case SendPortStopped: return "Stopped"; + case SendPortBound: return "Bound"; + default: return "Unknown (" + status + ")"; + } + } + + public static string FormatOrchestration(int status) + { + switch (status) + { + case OrchestrationStarted: return "Started"; + case OrchestrationStopped: return "Stopped"; + case OrchestrationBound: return "Bound"; + case OrchestrationUnbound: return "Unbound"; + default: return "Unknown (" + status + ")"; + } + } + + public static string FormatHostInstance(int state) + { + switch (state) + { + case HostStarted: return "Started"; + case HostStartPending: return "StartPending"; + case HostStopPending: return "StopPending"; + case HostStopped: return "Stopped"; + default: return "Unknown (" + state + ")"; + } + } + } +} diff --git a/src/BizTalkPlatformManagementTool/Models/BizTalkSnapshot.cs b/src/BizTalkPlatformManagementTool/Models/BizTalkSnapshot.cs new file mode 100644 index 0000000..d769252 --- /dev/null +++ b/src/BizTalkPlatformManagementTool/Models/BizTalkSnapshot.cs @@ -0,0 +1,126 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace BizTalkPlatformManagementTool.Models +{ + [DataContract] + public sealed class BizTalkSnapshot + { + public BizTalkSnapshot() + { + Applications = new List(); + HostInstances = new List(); + } + + [DataMember(Order = 1)] + public string ToolVersion { get; set; } + + [DataMember(Order = 2)] + public string CreatedAt { get; set; } + + [DataMember(Order = 3)] + public string Server { get; set; } + + [DataMember(Order = 4)] + public List Applications { get; set; } + + [DataMember(Order = 5)] + public List HostInstances { get; set; } + } + + [DataContract] + public sealed class ApplicationSnapshot + { + public ApplicationSnapshot() + { + ReceiveLocations = new List(); + SendPorts = new List(); + Orchestrations = new List(); + } + + [DataMember(Order = 1)] + public string Application { get; set; } + + [DataMember(Order = 2)] + public List ReceiveLocations { get; set; } + + [DataMember(Order = 3)] + public List SendPorts { get; set; } + + [DataMember(Order = 4)] + public List Orchestrations { get; set; } + } + + [DataContract] + public sealed class ReceiveLocationState + { + [DataMember(Order = 1)] + public string Application { get; set; } + + [DataMember(Order = 2)] + public string Name { get; set; } + + [DataMember(Order = 3)] + public string ReceivePortName { get; set; } + + [DataMember(Order = 4)] + public bool Enabled { get; set; } + + [DataMember(Order = 5)] + public string AdapterName { get; set; } + + [DataMember(Order = 6)] + public string Address { get; set; } + } + + [DataContract] + public sealed class SendPortState + { + [DataMember(Order = 1)] + public string Application { get; set; } + + [DataMember(Order = 2)] + public string Name { get; set; } + + [DataMember(Order = 3)] + public int Status { get; set; } + + [DataMember(Order = 4)] + public string PrimaryTransportType { get; set; } + + [DataMember(Order = 5)] + public string PrimaryTransportAddress { get; set; } + } + + [DataContract] + public sealed class OrchestrationState + { + [DataMember(Order = 1)] + public string Application { get; set; } + + [DataMember(Order = 2)] + public string Name { get; set; } + + [DataMember(Order = 3)] + public int OrchestrationStatus { get; set; } + } + + [DataContract] + public sealed class HostInstanceState + { + [DataMember(Order = 1)] + public string InstanceName { get; set; } + + [DataMember(Order = 2)] + public string HostName { get; set; } + + [DataMember(Order = 3)] + public string Server { get; set; } + + [DataMember(Order = 4)] + public int RawState { get; set; } + + [DataMember(Order = 5)] + public string StateText { get; set; } + } +} diff --git a/src/BizTalkPlatformManagementTool/Models/DiffModels.cs b/src/BizTalkPlatformManagementTool/Models/DiffModels.cs new file mode 100644 index 0000000..f4a19d4 --- /dev/null +++ b/src/BizTalkPlatformManagementTool/Models/DiffModels.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace BizTalkPlatformManagementTool.Models +{ + [DataContract] + public sealed class SnapshotDiff + { + public SnapshotDiff() + { + ArtifactDifferences = new List(); + HostInstanceDifferences = new List(); + } + + [DataMember(Order = 1)] + public List ArtifactDifferences { get; set; } + + [DataMember(Order = 2)] + public List HostInstanceDifferences { get; set; } + } + + [DataContract] + public sealed class ArtifactDiffEntry + { + [DataMember(Order = 1)] + public string Application { get; set; } + + [DataMember(Order = 2)] + public string ArtifactType { get; set; } + + [DataMember(Order = 3)] + public string Name { get; set; } + + [DataMember(Order = 4)] + public string Before { get; set; } + + [DataMember(Order = 5)] + public string After { get; set; } + } + + [DataContract] + public sealed class HostInstanceDiffEntry + { + [DataMember(Order = 1)] + public string InstanceName { get; set; } + + [DataMember(Order = 2)] + public string HostName { get; set; } + + [DataMember(Order = 3)] + public string Server { get; set; } + + [DataMember(Order = 4)] + public string Before { get; set; } + + [DataMember(Order = 5)] + public string After { get; set; } + } +} diff --git a/src/BizTalkPlatformManagementTool/Models/OperationModels.cs b/src/BizTalkPlatformManagementTool/Models/OperationModels.cs new file mode 100644 index 0000000..0ff62a4 --- /dev/null +++ b/src/BizTalkPlatformManagementTool/Models/OperationModels.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace BizTalkPlatformManagementTool.Models +{ + public enum OperationMode + { + Shutdown, + Restore + } + + public enum OperationStepKind + { + ReceiveLocation, + SendPort, + Orchestration, + HostInstance, + Note + } + + [DataContract] + public sealed class OperationPlan + { + public OperationPlan() + { + Steps = new List(); + } + + [DataMember(Order = 1)] + public string Mode { get; set; } + + [DataMember(Order = 2)] + public string CreatedAt { get; set; } + + [DataMember(Order = 3)] + public string Server { get; set; } + + [DataMember(Order = 4)] + public List Steps { get; set; } + } + + [DataContract] + public sealed class OperationStep + { + [DataMember(Order = 1)] + public string Kind { get; set; } + + [DataMember(Order = 2)] + public string Application { get; set; } + + [DataMember(Order = 3)] + public string Name { get; set; } + + [DataMember(Order = 4)] + public string Server { get; set; } + + [DataMember(Order = 5)] + public string Action { get; set; } + + [DataMember(Order = 6)] + public string WmiClass { get; set; } + + [DataMember(Order = 7)] + public string KeyProperty { get; set; } + + [DataMember(Order = 8)] + public string KeyValue { get; set; } + + [DataMember(Order = 9)] + public string MethodName { get; set; } + + [DataMember(Order = 10)] + public int[] Arguments { get; set; } + + [DataMember(Order = 11)] + public int? TargetState { get; set; } + + [DataMember(Order = 12)] + public bool Execute { get; set; } + + [DataMember(Order = 13)] + public string Warning { get; set; } + } + + public sealed class OperationOptions + { + public string Server { get; set; } + public string OutputDirectory { get; set; } + public string StateFile { get; set; } + public bool DryRun { get; set; } + public int WaitTimeoutSeconds { get; set; } + public int PollIntervalSeconds { get; set; } + } +} diff --git a/src/BizTalkPlatformManagementTool/Program.cs b/src/BizTalkPlatformManagementTool/Program.cs new file mode 100644 index 0000000..e25e27a --- /dev/null +++ b/src/BizTalkPlatformManagementTool/Program.cs @@ -0,0 +1,17 @@ +using System; +using System.Windows.Forms; +using BizTalkPlatformManagementTool.Ui; + +namespace BizTalkPlatformManagementTool +{ + internal static class Program + { + [STAThread] + private static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new MainForm()); + } + } +} diff --git a/src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs b/src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs new file mode 100644 index 0000000..5667699 --- /dev/null +++ b/src/BizTalkPlatformManagementTool/Services/BizTalkOperationService.cs @@ -0,0 +1,415 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management; +using BizTalkPlatformManagementTool.Models; + +namespace BizTalkPlatformManagementTool.Services +{ + public sealed class BizTalkOperationService + { + public const string Version = "2.0.0-net471"; + private const string UnknownApplication = "(Unknown Application)"; + + private readonly OperationLogger _logger; + + public BizTalkOperationService(OperationLogger logger) + { + _logger = logger; + } + + public void Diagnose(string server) + { + using (var client = CreateClient(server)) + { + var ports = client.Query("MSBTS_SendPort"); + _logger.Success("WMI/CIM diagnostic succeeded. Send ports visible: " + ports.Count); + } + } + + public BizTalkSnapshot CreateSnapshot(string server) + { + using (var client = CreateClient(server)) + { + var snapshot = new BizTalkSnapshot + { + ToolVersion = Version, + CreatedAt = DateTime.Now.ToString("s"), + Server = client.Server + }; + + var apps = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var item in client.Query("MSBTS_ReceiveLocation")) + { + var appName = GetApplicationName(item); + var app = GetApplication(apps, appName); + app.ReceiveLocations.Add(new ReceiveLocationState + { + Application = app.Application, + Name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty), + ReceivePortName = BizTalkWmiClient.SafeGetString(item, "ReceivePortName", string.Empty), + Enabled = !BizTalkWmiClient.SafeGetBoolean(item, "IsDisabled", true), + AdapterName = BizTalkWmiClient.SafeGetString(item, "AdapterName", string.Empty), + Address = BizTalkWmiClient.SafeGetString(item, "InboundTransportURL", string.Empty) + }); + } + + foreach (var item in client.Query("MSBTS_SendPort")) + { + var appName = GetApplicationName(item); + var app = GetApplication(apps, appName); + app.SendPorts.Add(new SendPortState + { + Application = app.Application, + Name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty), + Status = BizTalkWmiClient.SafeGetInt32(item, "Status", 0), + PrimaryTransportType = BizTalkWmiClient.SafeGetString(item, "PTTransportType", string.Empty), + PrimaryTransportAddress = BizTalkWmiClient.SafeGetString(item, "PTAddress", string.Empty) + }); + } + + foreach (var item in client.Query("MSBTS_Orchestration")) + { + var appName = GetApplicationName(item); + var app = GetApplication(apps, appName); + app.Orchestrations.Add(new OrchestrationState + { + Application = app.Application, + Name = BizTalkWmiClient.SafeGetString(item, "Name", string.Empty), + OrchestrationStatus = BizTalkWmiClient.SafeGetInt32(item, "OrchestrationStatus", 0) + }); + } + + foreach (var item in client.Query("MSBTS_HostInstance")) + { + var state = BizTalkWmiClient.SafeGetInt32(item, "ServiceState", 0); + snapshot.HostInstances.Add(new HostInstanceState + { + InstanceName = BizTalkWmiClient.SafeGetString(item, "InstanceName", BizTalkWmiClient.SafeGetString(item, "Name", string.Empty)), + HostName = BizTalkWmiClient.SafeGetString(item, "HostName", string.Empty), + Server = BizTalkWmiClient.SafeGetString(item, "RunningServer", string.Empty), + RawState = state, + StateText = ArtifactStates.FormatHostInstance(state) + }); + } + + foreach (var app in apps.Values.OrderBy(a => a.Application)) + { + app.ReceiveLocations = app.ReceiveLocations.OrderBy(x => x.Name).ToList(); + app.SendPorts = app.SendPorts.OrderBy(x => x.Name).ToList(); + app.Orchestrations = app.Orchestrations.OrderBy(x => x.Name).ToList(); + snapshot.Applications.Add(app); + } + snapshot.HostInstances = snapshot.HostInstances.OrderBy(x => x.Server).ThenBy(x => x.InstanceName).ToList(); + + _logger.Success("Snapshot created. Applications: " + snapshot.Applications.Count + ", host instances: " + snapshot.HostInstances.Count); + return snapshot; + } + } + + public OperationPlan CreateShutdownPlan(BizTalkSnapshot snapshot, string server) + { + var plan = NewPlan(OperationMode.Shutdown, server); + + foreach (var app in snapshot.Applications) + { + foreach (var item in app.ReceiveLocations.Where(x => x.Enabled)) + { + plan.Steps.Add(Step("ReceiveLocation", app.Application, item.Name, null, "Disable receive location", "MSBTS_ReceiveLocation", "Name", item.Name, "Disable", null, false)); + } + + foreach (var item in app.Orchestrations.Where(x => x.OrchestrationStatus == ArtifactStates.OrchestrationStarted)) + { + plan.Steps.Add(Step("Orchestration", app.Application, item.Name, null, "Stop orchestration", "MSBTS_Orchestration", "Name", item.Name, "Stop", new[] { 1, 1 }, ArtifactStates.OrchestrationStopped)); + } + + foreach (var item in app.SendPorts.Where(x => x.Status == ArtifactStates.SendPortStarted)) + { + plan.Steps.Add(Step("SendPort", app.Application, item.Name, null, "Stop send port", "MSBTS_SendPort", "Name", item.Name, "Stop", null, ArtifactStates.SendPortStopped)); + } + } + + foreach (var item in snapshot.HostInstances.Where(x => x.RawState == ArtifactStates.HostStarted)) + { + var step = Step("HostInstance", string.Empty, item.InstanceName, item.Server, "Stop host instance", "MSBTS_HostInstance", "InstanceName", item.InstanceName, "Stop", null, ArtifactStates.HostStopped); + if (!string.IsNullOrEmpty(item.Server) && !string.Equals(item.Server, server, StringComparison.OrdinalIgnoreCase)) + { + step.Execute = false; + step.Warning = "Host instance belongs to server '" + item.Server + "'. Run this action on that server."; + } + plan.Steps.Add(step); + } + + return plan; + } + + public OperationPlan CreateRestorePlan(BizTalkSnapshot snapshot, string server) + { + var plan = NewPlan(OperationMode.Restore, server); + + foreach (var item in snapshot.HostInstances.Where(x => x.RawState == ArtifactStates.HostStarted)) + { + var step = Step("HostInstance", string.Empty, item.InstanceName, item.Server, "Start host instance", "MSBTS_HostInstance", "InstanceName", item.InstanceName, "Start", null, ArtifactStates.HostStarted); + if (!string.IsNullOrEmpty(item.Server) && !string.Equals(item.Server, server, StringComparison.OrdinalIgnoreCase)) + { + step.Execute = false; + step.Warning = "Host instance belongs to server '" + item.Server + "'. Run this action on that server."; + } + plan.Steps.Add(step); + } + + foreach (var app in snapshot.Applications) + { + foreach (var item in app.SendPorts) + { + if (item.Status == ArtifactStates.SendPortStarted) + { + plan.Steps.Add(Step("SendPort", app.Application, item.Name, null, "Start send port", "MSBTS_SendPort", "Name", item.Name, "Start", null, ArtifactStates.SendPortStarted)); + } + else if (item.Status == ArtifactStates.SendPortStopped) + { + plan.Steps.Add(Step("SendPort", app.Application, item.Name, null, "Ensure send port is stopped", "MSBTS_SendPort", "Name", item.Name, "StopOrEnlist", null, ArtifactStates.SendPortStopped)); + } + else if (item.Status == ArtifactStates.SendPortBound) + { + plan.Steps.Add(Step("SendPort", app.Application, item.Name, null, "Ensure send port is bound", "MSBTS_SendPort", "Name", item.Name, "UnEnlist", null, ArtifactStates.SendPortBound)); + } + } + + foreach (var item in app.Orchestrations) + { + if (item.OrchestrationStatus == ArtifactStates.OrchestrationStarted) + { + plan.Steps.Add(Step("Orchestration", app.Application, item.Name, null, "Start orchestration", "MSBTS_Orchestration", "Name", item.Name, "Start", new[] { 1, 1, 1 }, ArtifactStates.OrchestrationStarted)); + } + else if (item.OrchestrationStatus == ArtifactStates.OrchestrationStopped) + { + plan.Steps.Add(Step("Orchestration", app.Application, item.Name, null, "Ensure orchestration is stopped", "MSBTS_Orchestration", "Name", item.Name, "StopIfStarted", new[] { 1, 1 }, ArtifactStates.OrchestrationStopped)); + } + else if (item.OrchestrationStatus == ArtifactStates.OrchestrationBound) + { + plan.Steps.Add(new OperationStep + { + Kind = "Note", + Application = app.Application, + Name = item.Name, + Action = "Orchestration was Bound in the snapshot; leaving unchanged to avoid making it Unbound.", + Execute = false, + Warning = "Manual check recommended." + }); + } + else if (item.OrchestrationStatus == ArtifactStates.OrchestrationUnbound) + { + plan.Steps.Add(Step("Orchestration", app.Application, item.Name, null, "Unenlist orchestration", "MSBTS_Orchestration", "Name", item.Name, "UnenlistService", new[] { 1 }, ArtifactStates.OrchestrationUnbound)); + } + } + + foreach (var item in app.ReceiveLocations) + { + plan.Steps.Add(Step("ReceiveLocation", app.Application, item.Name, null, item.Enabled ? "Enable receive location" : "Disable receive location", "MSBTS_ReceiveLocation", "Name", item.Name, item.Enabled ? "Enable" : "Disable", null, null)); + } + } + + return plan; + } + + public void ExecutePlan(OperationPlan plan, OperationOptions options) + { + using (var client = CreateClient(options.Server)) + { + foreach (var step in plan.Steps) + { + if (!step.Execute) + { + _logger.Warning(step.Action + (string.IsNullOrEmpty(step.Warning) ? string.Empty : " " + step.Warning)); + continue; + } + + if (options.DryRun) + { + _logger.Info("DRY RUN: " + step.Action + " '" + step.Name + "'"); + continue; + } + + using (var instance = client.FindByProperty(step.WmiClass, step.KeyProperty, step.KeyValue)) + { + if (instance == null) + { + throw new InvalidOperationException(step.Kind + " not found: " + step.Name); + } + + ExecuteStep(client, instance, step, options); + } + } + } + } + + public string SaveSnapshot(string outputDirectory, string fileName, BizTalkSnapshot snapshot) + { + Directory.CreateDirectory(outputDirectory); + var path = Path.Combine(outputDirectory, fileName); + SnapshotStore.SaveSnapshotSet(path, snapshot); + _logger.Success("Saved snapshot: " + path); + return path; + } + + public string SavePlan(string outputDirectory, string fileName, OperationPlan plan) + { + Directory.CreateDirectory(outputDirectory); + var path = Path.Combine(outputDirectory, fileName); + JsonFileStore.Save(path, plan); + _logger.Success("Saved operation plan: " + path); + return path; + } + + public string SaveDiff(string outputDirectory, string fileName, SnapshotDiff diff) + { + Directory.CreateDirectory(outputDirectory); + var path = Path.Combine(outputDirectory, fileName); + SnapshotStore.SaveDiffSet(path, diff); + _logger.Success("Saved diff: " + path); + return path; + } + + private void ExecuteStep(BizTalkWmiClient client, ManagementObject instance, OperationStep step, OperationOptions options) + { + if (string.Equals(step.MethodName, "StopOrEnlist", StringComparison.OrdinalIgnoreCase)) + { + var status = BizTalkWmiClient.SafeGetInt32(instance, "Status", 0); + if (status == ArtifactStates.SendPortBound) + { + client.InvokeMethod(instance, "Enlist"); + } + else if (status == ArtifactStates.SendPortStarted) + { + client.InvokeMethod(instance, "Stop"); + } + else + { + _logger.Info("Already stopped: " + step.Name); + } + } + else if (string.Equals(step.MethodName, "StopIfStarted", StringComparison.OrdinalIgnoreCase)) + { + var status = BizTalkWmiClient.SafeGetInt32(instance, "OrchestrationStatus", 0); + if (status == ArtifactStates.OrchestrationStarted) + { + client.InvokeMethod(instance, "Stop", ToObjects(step.Arguments)); + } + else + { + _logger.Info("No stop required: " + step.Name); + } + } + else + { + client.InvokeMethod(instance, step.MethodName, ToObjects(step.Arguments)); + } + + WaitForTarget(client, step, options); + } + + private void WaitForTarget(BizTalkWmiClient client, OperationStep step, OperationOptions options) + { + if (!step.TargetState.HasValue) + { + if (step.Kind == "ReceiveLocation") + { + var shouldBeEnabled = string.Equals(step.MethodName, "Enable", StringComparison.OrdinalIgnoreCase); + client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => !BizTalkWmiClient.SafeGetBoolean(o, "IsDisabled", true) == shouldBeEnabled, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds); + } + return; + } + + if (step.Kind == "SendPort") + { + client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => BizTalkWmiClient.SafeGetInt32(o, "Status", 0) == step.TargetState.Value, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds); + } + else if (step.Kind == "Orchestration") + { + client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => BizTalkWmiClient.SafeGetInt32(o, "OrchestrationStatus", 0) == step.TargetState.Value, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds); + } + else if (step.Kind == "HostInstance") + { + client.WaitForState(step.WmiClass, step.KeyProperty, step.KeyValue, o => BizTalkWmiClient.SafeGetInt32(o, "ServiceState", 0) == step.TargetState.Value, step.Action + " completed", options.WaitTimeoutSeconds, options.PollIntervalSeconds); + } + } + + private BizTalkWmiClient CreateClient(string server) + { + var client = new BizTalkWmiClient(server, _logger); + client.Connect(); + return client; + } + + private static OperationPlan NewPlan(OperationMode mode, string server) + { + return new OperationPlan + { + Mode = mode.ToString(), + CreatedAt = DateTime.Now.ToString("s"), + Server = server + }; + } + + private static OperationStep Step(string kind, string application, string name, string server, string action, string wmiClass, string keyProperty, string keyValue, string methodName, int[] arguments, int? targetState) + { + return new OperationStep + { + Kind = kind, + Application = application, + Name = name, + Server = server, + Action = action, + WmiClass = wmiClass, + KeyProperty = keyProperty, + KeyValue = keyValue, + MethodName = methodName, + Arguments = arguments, + TargetState = targetState, + Execute = true + }; + } + + private static object[] ToObjects(int[] values) + { + if (values == null || values.Length == 0) + { + return new object[0]; + } + + var result = new object[values.Length]; + for (int i = 0; i < values.Length; i++) + { + result[i] = values[i]; + } + return result; + } + + private static ApplicationSnapshot GetApplication(Dictionary apps, string appName) + { + ApplicationSnapshot app; + if (!apps.TryGetValue(appName, out app)) + { + app = new ApplicationSnapshot { Application = appName }; + apps.Add(appName, app); + } + return app; + } + + private static string GetApplicationName(ManagementObject item) + { + var names = new[] { "ApplicationName", "Application", "BizTalkApplication" }; + foreach (var name in names) + { + var value = BizTalkWmiClient.SafeGetString(item, name, null); + if (!string.IsNullOrWhiteSpace(value)) + { + return value; + } + } + return UnknownApplication; + } + } +} diff --git a/src/BizTalkPlatformManagementTool/Services/BizTalkWmiClient.cs b/src/BizTalkPlatformManagementTool/Services/BizTalkWmiClient.cs new file mode 100644 index 0000000..3523704 --- /dev/null +++ b/src/BizTalkPlatformManagementTool/Services/BizTalkWmiClient.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Management; +using System.Threading; + +namespace BizTalkPlatformManagementTool.Services +{ + public sealed class BizTalkWmiClient : IDisposable + { + private const string NamespacePath = "root\\MicrosoftBizTalkServer"; + private readonly string _server; + private readonly OperationLogger _logger; + private ManagementScope _scope; + + public BizTalkWmiClient(string server, OperationLogger logger) + { + _server = string.IsNullOrWhiteSpace(server) ? Environment.MachineName : server.Trim(); + _logger = logger; + } + + public string Server + { + get { return _server; } + } + + public void Connect() + { + var path = "\\\\" + _server + "\\" + NamespacePath; + _logger.Info("Connecting to WMI namespace " + path); + _scope = new ManagementScope(path); + _scope.Connect(); + _logger.Success("WMI connection established."); + } + + public List Query(string className) + { + EnsureConnected(); + + var result = new List(); + var query = new ObjectQuery("SELECT * FROM " + className); + using (var searcher = new ManagementObjectSearcher(_scope, query)) + using (var collection = searcher.Get()) + { + foreach (ManagementObject item in collection) + { + result.Add(item); + } + } + + return result; + } + + public ManagementObject FindByProperty(string className, string propertyName, string value) + { + EnsureConnected(); + + var query = string.Format( + CultureInfo.InvariantCulture, + "SELECT * FROM {0} WHERE {1} = \"{2}\"", + className, + propertyName, + EscapeWql(value)); + + using (var searcher = new ManagementObjectSearcher(_scope, new ObjectQuery(query))) + using (var collection = searcher.Get()) + { + foreach (ManagementObject item in collection) + { + return item; + } + } + + return null; + } + + public uint InvokeMethod(ManagementObject instance, string methodName, params object[] arguments) + { + if (instance == null) + { + throw new ArgumentNullException("instance"); + } + + var methodToCall = ResolveMethodName(instance, methodName); + _logger.Info("Calling " + instance.Path.ClassName + "." + methodToCall + " on " + SafeGetString(instance, "Name", SafeGetString(instance, "InstanceName", ""))); + + object result = instance.InvokeMethod(methodToCall, arguments == null || arguments.Length == 0 ? null : arguments); + var returnCode = ExtractReturnCode(result); + if (returnCode != 0) + { + throw new InvalidOperationException(methodToCall + " returned HRESULT/ReturnValue " + returnCode.ToString(CultureInfo.InvariantCulture)); + } + + return returnCode; + } + + public void WaitForState(string className, string keyProperty, string keyValue, Func isReached, string description, int timeoutSeconds, int pollIntervalSeconds) + { + var deadline = DateTime.UtcNow.AddSeconds(Math.Max(1, timeoutSeconds)); + var delay = Math.Max(1, pollIntervalSeconds); + + while (DateTime.UtcNow <= deadline) + { + using (var current = FindByProperty(className, keyProperty, keyValue)) + { + if (current != null && isReached(current)) + { + _logger.Success("Reached: " + description); + return; + } + } + + Thread.Sleep(TimeSpan.FromSeconds(delay)); + } + + throw new TimeoutException("Timeout while waiting for " + description); + } + + public static string SafeGetString(ManagementBaseObject item, string propertyName, string fallback) + { + try + { + if (!HasProperty(item, propertyName)) + { + return fallback; + } + + var value = item[propertyName]; + return value == null ? fallback : Convert.ToString(value, CultureInfo.InvariantCulture); + } + catch (ManagementException) + { + return fallback; + } + } + + public static int SafeGetInt32(ManagementBaseObject item, string propertyName, int fallback) + { + try + { + if (!HasProperty(item, propertyName)) + { + return fallback; + } + + var value = item[propertyName]; + return value == null ? fallback : Convert.ToInt32(value, CultureInfo.InvariantCulture); + } + catch + { + return fallback; + } + } + + public static bool SafeGetBoolean(ManagementBaseObject item, string propertyName, bool fallback) + { + try + { + if (!HasProperty(item, propertyName)) + { + return fallback; + } + + var value = item[propertyName]; + return value == null ? fallback : Convert.ToBoolean(value, CultureInfo.InvariantCulture); + } + catch + { + return fallback; + } + } + + private void EnsureConnected() + { + if (_scope == null || !_scope.IsConnected) + { + Connect(); + } + } + + private static bool HasProperty(ManagementBaseObject item, string propertyName) + { + foreach (PropertyData property in item.Properties) + { + if (string.Equals(property.Name, propertyName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + private static string ResolveMethodName(ManagementObject instance, string requestedName) + { + foreach (MethodData method in instance.Methods) + { + if (string.Equals(method.Name, requestedName, StringComparison.OrdinalIgnoreCase)) + { + return method.Name; + } + } + + throw new MissingMethodException(instance.Path.ClassName, requestedName); + } + + private static uint ExtractReturnCode(object result) + { + if (result == null) + { + return 0; + } + + var outParams = result as ManagementBaseObject; + if (outParams != null && HasProperty(outParams, "ReturnValue")) + { + return Convert.ToUInt32(outParams["ReturnValue"], CultureInfo.InvariantCulture); + } + + return Convert.ToUInt32(result, CultureInfo.InvariantCulture); + } + + private static string EscapeWql(string value) + { + return (value ?? string.Empty).Replace("\\", "\\\\").Replace("\"", "\\\""); + } + + public void Dispose() + { + } + } +} diff --git a/src/BizTalkPlatformManagementTool/Services/CsvWriter.cs b/src/BizTalkPlatformManagementTool/Services/CsvWriter.cs new file mode 100644 index 0000000..ec8b08c --- /dev/null +++ b/src/BizTalkPlatformManagementTool/Services/CsvWriter.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using System.IO; +using System.Text; +using BizTalkPlatformManagementTool.Models; + +namespace BizTalkPlatformManagementTool.Services +{ + public static class CsvWriter + { + public static void WriteSnapshotArtifacts(string path, BizTalkSnapshot snapshot) + { + var lines = new List { "Application,Type,Name,Status" }; + foreach (var app in snapshot.Applications) + { + foreach (var item in app.ReceiveLocations) + { + lines.Add(Row(app.Application, "ReceiveLocation", item.Name, ArtifactStates.FormatReceiveLocation(item.Enabled))); + } + foreach (var item in app.SendPorts) + { + lines.Add(Row(app.Application, "SendPort", item.Name, ArtifactStates.FormatSendPort(item.Status))); + } + foreach (var item in app.Orchestrations) + { + lines.Add(Row(app.Application, "Orchestration", item.Name, ArtifactStates.FormatOrchestration(item.OrchestrationStatus))); + } + } + File.WriteAllLines(path, lines, Encoding.UTF8); + } + + public static void WriteSnapshotHosts(string path, BizTalkSnapshot snapshot) + { + var lines = new List { "InstanceName,HostName,Server,State" }; + foreach (var item in snapshot.HostInstances) + { + lines.Add(Row(item.InstanceName, item.HostName, item.Server, item.StateText)); + } + File.WriteAllLines(path, lines, Encoding.UTF8); + } + + public static void WriteDiff(string path, SnapshotDiff diff) + { + var lines = new List { "Scope,Application,Type,Name,Server,Before,After" }; + foreach (var item in diff.ArtifactDifferences) + { + lines.Add(Row("Artifact", item.Application, item.ArtifactType, item.Name, string.Empty, item.Before, item.After)); + } + foreach (var item in diff.HostInstanceDifferences) + { + lines.Add(Row("HostInstance", string.Empty, "HostInstance", item.InstanceName, item.Server, item.Before, item.After)); + } + File.WriteAllLines(path, lines, Encoding.UTF8); + } + + private static string Row(params string[] values) + { + var escaped = new string[values.Length]; + for (int i = 0; i < values.Length; i++) + { + escaped[i] = Escape(values[i]); + } + return string.Join(",", escaped); + } + + private static string Escape(string value) + { + value = value ?? string.Empty; + if (value.IndexOfAny(new[] { ',', '"', '\r', '\n' }) < 0) + { + return value; + } + return "\"" + value.Replace("\"", "\"\"") + "\""; + } + } +} diff --git a/src/BizTalkPlatformManagementTool/Services/HtmlReportWriter.cs b/src/BizTalkPlatformManagementTool/Services/HtmlReportWriter.cs new file mode 100644 index 0000000..89bb520 --- /dev/null +++ b/src/BizTalkPlatformManagementTool/Services/HtmlReportWriter.cs @@ -0,0 +1,100 @@ +using System.Net; +using System.Text; +using BizTalkPlatformManagementTool.Models; + +namespace BizTalkPlatformManagementTool.Services +{ + public static class HtmlReportWriter + { + public static void WriteSnapshot(string path, BizTalkSnapshot snapshot) + { + var html = new StringBuilder(); + Header(html, "BizTalk Snapshot"); + html.Append("

Server: ").Append(Encode(snapshot.Server)).Append(" Created: ").Append(Encode(snapshot.CreatedAt)).Append("

"); + + foreach (var app in snapshot.Applications) + { + html.Append("

").Append(Encode(app.Application)).Append("

"); + html.Append("

Receive Locations

"); + foreach (var item in app.ReceiveLocations) + { + StatusRow(html, item.Name, ArtifactStates.FormatReceiveLocation(item.Enabled), item.Enabled, item.AdapterName, item.Address); + } + html.Append("
NameStatusAdapterAddress
"); + + html.Append("

Send Ports

"); + foreach (var item in app.SendPorts) + { + StatusRow(html, item.Name, ArtifactStates.FormatSendPort(item.Status), item.Status == ArtifactStates.SendPortStarted, item.PrimaryTransportType, item.PrimaryTransportAddress); + } + html.Append("
NameStatusTransportAddress
"); + + html.Append("

Orchestrations

"); + foreach (var item in app.Orchestrations) + { + html.Append(""); + } + html.Append("
NameStatus
").Append(Encode(item.Name)).Append("") + .Append(Encode(ArtifactStates.FormatOrchestration(item.OrchestrationStatus))).Append("
"); + } + + html.Append("

Host Instances

"); + foreach (var item in snapshot.HostInstances) + { + html.Append(""); + } + html.Append("
InstanceHostServerStatus
").Append(Encode(item.InstanceName)).Append("").Append(Encode(item.HostName)).Append("") + .Append(Encode(item.Server)).Append("") + .Append(Encode(item.StateText)).Append("
"); + Footer(html); + System.IO.File.WriteAllText(path, html.ToString(), Encoding.UTF8); + } + + public static void WriteDiff(string path, SnapshotDiff diff) + { + var html = new StringBuilder(); + Header(html, "BizTalk Differences"); + + html.Append("

Artifacts

"); + foreach (var item in diff.ArtifactDifferences) + { + html.Append(""); + } + html.Append("
ApplicationTypeNameBeforeAfter
").Append(Encode(item.Application)).Append("").Append(Encode(item.ArtifactType)).Append("") + .Append(Encode(item.Name)).Append("").Append(Encode(item.Before)).Append("").Append(Encode(item.After)).Append("
"); + + html.Append("

Host Instances

"); + foreach (var item in diff.HostInstanceDifferences) + { + html.Append(""); + } + html.Append("
InstanceHostServerBeforeAfter
").Append(Encode(item.InstanceName)).Append("").Append(Encode(item.HostName)).Append("") + .Append(Encode(item.Server)).Append("").Append(Encode(item.Before)).Append("").Append(Encode(item.After)).Append("
"); + Footer(html); + System.IO.File.WriteAllText(path, html.ToString(), Encoding.UTF8); + } + + private static void StatusRow(StringBuilder html, string name, string status, bool ok, string detail1, string detail2) + { + html.Append("").Append(Encode(name)).Append("").Append(Encode(status)) + .Append("").Append(Encode(detail1)).Append("").Append(Encode(detail2)).Append(""); + } + + private static void Header(StringBuilder html, string title) + { + html.Append("").Append(Encode(title)).Append("

").Append(Encode(title)).Append("

"); + } + + private static void Footer(StringBuilder html) + { + html.Append(""); + } + + private static string Encode(string value) + { + return WebUtility.HtmlEncode(value ?? string.Empty); + } + } +} diff --git a/src/BizTalkPlatformManagementTool/Services/JsonFileStore.cs b/src/BizTalkPlatformManagementTool/Services/JsonFileStore.cs new file mode 100644 index 0000000..0c1e93a --- /dev/null +++ b/src/BizTalkPlatformManagementTool/Services/JsonFileStore.cs @@ -0,0 +1,38 @@ +using System.IO; +using System.Runtime.Serialization.Json; +using System.Text; + +namespace BizTalkPlatformManagementTool.Services +{ + public static class JsonFileStore + { + public static void Save(string path, T value) + { + Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))); + var serializer = new DataContractJsonSerializer(typeof(T), new DataContractJsonSerializerSettings + { + UseSimpleDictionaryFormat = true + }); + + using (var stream = new MemoryStream()) + { + serializer.WriteObject(stream, value); + var json = Encoding.UTF8.GetString(stream.ToArray()); + File.WriteAllText(path, json, Encoding.UTF8); + } + } + + public static T Load(string path) + { + var serializer = new DataContractJsonSerializer(typeof(T), new DataContractJsonSerializerSettings + { + UseSimpleDictionaryFormat = true + }); + + using (var stream = File.OpenRead(path)) + { + return (T)serializer.ReadObject(stream); + } + } + } +} diff --git a/src/BizTalkPlatformManagementTool/Services/OperationLogger.cs b/src/BizTalkPlatformManagementTool/Services/OperationLogger.cs new file mode 100644 index 0000000..3dd2ecb --- /dev/null +++ b/src/BizTalkPlatformManagementTool/Services/OperationLogger.cs @@ -0,0 +1,64 @@ +using System; + +namespace BizTalkPlatformManagementTool.Services +{ + public enum LogLevel + { + Info, + Warning, + Error, + Success + } + + public sealed class LogEntry + { + public DateTime Timestamp { get; set; } + public LogLevel Level { get; set; } + public string Message { get; set; } + } + + public sealed class OperationLogger + { + private readonly Action _sink; + + public OperationLogger(Action sink) + { + _sink = sink; + } + + public void Info(string message) + { + Write(LogLevel.Info, message); + } + + public void Warning(string message) + { + Write(LogLevel.Warning, message); + } + + public void Error(string message) + { + Write(LogLevel.Error, message); + } + + public void Success(string message) + { + Write(LogLevel.Success, message); + } + + private void Write(LogLevel level, string message) + { + if (_sink == null) + { + return; + } + + _sink(new LogEntry + { + Timestamp = DateTime.Now, + Level = level, + Message = message + }); + } + } +} diff --git a/src/BizTalkPlatformManagementTool/Services/SnapshotComparer.cs b/src/BizTalkPlatformManagementTool/Services/SnapshotComparer.cs new file mode 100644 index 0000000..9ee419d --- /dev/null +++ b/src/BizTalkPlatformManagementTool/Services/SnapshotComparer.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using BizTalkPlatformManagementTool.Models; + +namespace BizTalkPlatformManagementTool.Services +{ + public static class SnapshotComparer + { + public static SnapshotDiff Compare(BizTalkSnapshot before, BizTalkSnapshot after) + { + var diff = new SnapshotDiff(); + + CompareReceiveLocations(diff, FlattenReceiveLocations(before), FlattenReceiveLocations(after)); + CompareSendPorts(diff, FlattenSendPorts(before), FlattenSendPorts(after)); + CompareOrchestrations(diff, FlattenOrchestrations(before), FlattenOrchestrations(after)); + CompareHostInstances(diff, before.HostInstances, after.HostInstances); + + return diff; + } + + private static void CompareReceiveLocations(SnapshotDiff diff, Dictionary before, Dictionary after) + { + foreach (var pair in after) + { + ReceiveLocationState old; + if (!before.TryGetValue(pair.Key, out old)) + { + AddArtifact(diff, pair.Value.Application, "ReceiveLocation", pair.Value.Name, "", ArtifactStates.FormatReceiveLocation(pair.Value.Enabled)); + } + else if (old.Enabled != pair.Value.Enabled) + { + AddArtifact(diff, pair.Value.Application, "ReceiveLocation", pair.Value.Name, ArtifactStates.FormatReceiveLocation(old.Enabled), ArtifactStates.FormatReceiveLocation(pair.Value.Enabled)); + } + } + + foreach (var pair in before) + { + if (!after.ContainsKey(pair.Key)) + { + AddArtifact(diff, pair.Value.Application, "ReceiveLocation", pair.Value.Name, ArtifactStates.FormatReceiveLocation(pair.Value.Enabled), ""); + } + } + } + + private static void CompareSendPorts(SnapshotDiff diff, Dictionary before, Dictionary after) + { + foreach (var pair in after) + { + SendPortState old; + if (!before.TryGetValue(pair.Key, out old)) + { + AddArtifact(diff, pair.Value.Application, "SendPort", pair.Value.Name, "", ArtifactStates.FormatSendPort(pair.Value.Status)); + } + else if (old.Status != pair.Value.Status) + { + AddArtifact(diff, pair.Value.Application, "SendPort", pair.Value.Name, ArtifactStates.FormatSendPort(old.Status), ArtifactStates.FormatSendPort(pair.Value.Status)); + } + } + + foreach (var pair in before) + { + if (!after.ContainsKey(pair.Key)) + { + AddArtifact(diff, pair.Value.Application, "SendPort", pair.Value.Name, ArtifactStates.FormatSendPort(pair.Value.Status), ""); + } + } + } + + private static void CompareOrchestrations(SnapshotDiff diff, Dictionary before, Dictionary after) + { + foreach (var pair in after) + { + OrchestrationState old; + if (!before.TryGetValue(pair.Key, out old)) + { + AddArtifact(diff, pair.Value.Application, "Orchestration", pair.Value.Name, "", ArtifactStates.FormatOrchestration(pair.Value.OrchestrationStatus)); + } + else if (old.OrchestrationStatus != pair.Value.OrchestrationStatus) + { + AddArtifact(diff, pair.Value.Application, "Orchestration", pair.Value.Name, ArtifactStates.FormatOrchestration(old.OrchestrationStatus), ArtifactStates.FormatOrchestration(pair.Value.OrchestrationStatus)); + } + } + + foreach (var pair in before) + { + if (!after.ContainsKey(pair.Key)) + { + AddArtifact(diff, pair.Value.Application, "Orchestration", pair.Value.Name, ArtifactStates.FormatOrchestration(pair.Value.OrchestrationStatus), ""); + } + } + } + + private static void CompareHostInstances(SnapshotDiff diff, List before, List after) + { + var beforeMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + var afterMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var item in before) + { + beforeMap[item.InstanceName ?? string.Empty] = item; + } + foreach (var item in after) + { + afterMap[item.InstanceName ?? string.Empty] = item; + } + + foreach (var pair in afterMap) + { + HostInstanceState old; + if (!beforeMap.TryGetValue(pair.Key, out old)) + { + diff.HostInstanceDifferences.Add(new HostInstanceDiffEntry { InstanceName = pair.Value.InstanceName, HostName = pair.Value.HostName, Server = pair.Value.Server, Before = "", After = pair.Value.StateText }); + } + else if (old.RawState != pair.Value.RawState) + { + diff.HostInstanceDifferences.Add(new HostInstanceDiffEntry { InstanceName = pair.Value.InstanceName, HostName = pair.Value.HostName, Server = pair.Value.Server, Before = old.StateText, After = pair.Value.StateText }); + } + } + + foreach (var pair in beforeMap) + { + if (!afterMap.ContainsKey(pair.Key)) + { + diff.HostInstanceDifferences.Add(new HostInstanceDiffEntry { InstanceName = pair.Value.InstanceName, HostName = pair.Value.HostName, Server = pair.Value.Server, Before = pair.Value.StateText, After = "" }); + } + } + } + + private static void AddArtifact(SnapshotDiff diff, string application, string type, string name, string before, string after) + { + diff.ArtifactDifferences.Add(new ArtifactDiffEntry + { + Application = application, + ArtifactType = type, + Name = name, + Before = before, + After = after + }); + } + + private static Dictionary FlattenReceiveLocations(BizTalkSnapshot snapshot) + { + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var app in snapshot.Applications) + { + foreach (var item in app.ReceiveLocations) + { + map[item.Name ?? string.Empty] = item; + } + } + return map; + } + + private static Dictionary FlattenSendPorts(BizTalkSnapshot snapshot) + { + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var app in snapshot.Applications) + { + foreach (var item in app.SendPorts) + { + map[item.Name ?? string.Empty] = item; + } + } + return map; + } + + private static Dictionary FlattenOrchestrations(BizTalkSnapshot snapshot) + { + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var app in snapshot.Applications) + { + foreach (var item in app.Orchestrations) + { + map[item.Name ?? string.Empty] = item; + } + } + return map; + } + } +} diff --git a/src/BizTalkPlatformManagementTool/Services/SnapshotStore.cs b/src/BizTalkPlatformManagementTool/Services/SnapshotStore.cs new file mode 100644 index 0000000..817fc88 --- /dev/null +++ b/src/BizTalkPlatformManagementTool/Services/SnapshotStore.cs @@ -0,0 +1,25 @@ +using System.IO; +using BizTalkPlatformManagementTool.Models; + +namespace BizTalkPlatformManagementTool.Services +{ + public static class SnapshotStore + { + public static void SaveSnapshotSet(string jsonPath, BizTalkSnapshot snapshot) + { + Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(jsonPath))); + JsonFileStore.Save(jsonPath, snapshot); + CsvWriter.WriteSnapshotArtifacts(jsonPath + ".csv", snapshot); + CsvWriter.WriteSnapshotHosts(jsonPath + ".hosts.csv", snapshot); + HtmlReportWriter.WriteSnapshot(jsonPath + ".html", snapshot); + } + + public static void SaveDiffSet(string jsonPath, SnapshotDiff diff) + { + Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(jsonPath))); + JsonFileStore.Save(jsonPath, diff); + CsvWriter.WriteDiff(Path.ChangeExtension(jsonPath, ".csv"), diff); + HtmlReportWriter.WriteDiff(Path.ChangeExtension(jsonPath, ".html"), diff); + } + } +} diff --git a/src/BizTalkPlatformManagementTool/Ui/MainForm.cs b/src/BizTalkPlatformManagementTool/Ui/MainForm.cs new file mode 100644 index 0000000..8428cd2 --- /dev/null +++ b/src/BizTalkPlatformManagementTool/Ui/MainForm.cs @@ -0,0 +1,462 @@ +using System; +using System.Drawing; +using System.IO; +using System.Threading.Tasks; +using System.Windows.Forms; +using BizTalkPlatformManagementTool.Models; +using BizTalkPlatformManagementTool.Services; + +namespace BizTalkPlatformManagementTool.Ui +{ + public sealed class MainForm : Form + { + private readonly TextBox _serverTextBox = new TextBox(); + private readonly TextBox _outputTextBox = new TextBox(); + private readonly TextBox _stateFileTextBox = new TextBox(); + private readonly NumericUpDown _timeoutInput = new NumericUpDown(); + private readonly NumericUpDown _pollInput = new NumericUpDown(); + private readonly CheckBox _dryRunCheckBox = new CheckBox(); + private readonly DataGridView _statusGrid = new DataGridView(); + private readonly DataGridView _logGrid = new DataGridView(); + private readonly StatusStrip _statusStrip = new StatusStrip(); + private readonly ToolStripStatusLabel _statusLabel = new ToolStripStatusLabel(); + private readonly BizTalkOperationService _service; + + private Button _diagnoseButton; + private Button _beforeButton; + private Button _afterButton; + private Button _compareButton; + private Button _shutdownButton; + private Button _restoreButton; + + public MainForm() + { + Text = "BizTalk Platform Management Tool"; + Width = 1180; + Height = 760; + MinimumSize = new Size(980, 640); + StartPosition = FormStartPosition.CenterScreen; + + _service = new BizTalkOperationService(new OperationLogger(AppendLog)); + BuildUi(); + } + + private void BuildUi() + { + var root = new TableLayoutPanel + { + Dock = DockStyle.Fill, + ColumnCount = 1, + RowCount = 4, + Padding = new Padding(10) + }; + root.RowStyles.Add(new RowStyle(SizeType.Absolute, 88)); + root.RowStyles.Add(new RowStyle(SizeType.Absolute, 54)); + root.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); + root.RowStyles.Add(new RowStyle(SizeType.Absolute, 24)); + Controls.Add(root); + + root.Controls.Add(BuildSettingsPanel(), 0, 0); + root.Controls.Add(BuildActionPanel(), 0, 1); + root.Controls.Add(BuildTabs(), 0, 2); + + _statusStrip.Items.Add(_statusLabel); + _statusLabel.Text = "Ready. Dry run is enabled."; + root.Controls.Add(_statusStrip, 0, 3); + } + + private Control BuildSettingsPanel() + { + var panel = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 8, RowCount = 2 }; + panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 70)); + panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 30)); + panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 76)); + panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 35)); + panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 88)); + panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 82)); + panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 72)); + panel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 88)); + panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 36)); + panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 36)); + + _serverTextBox.Text = Environment.MachineName; + _serverTextBox.Dock = DockStyle.Fill; + _outputTextBox.Text = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "BizTalkPlatformManagementTool"); + _outputTextBox.Dock = DockStyle.Fill; + _stateFileTextBox.Text = "before.json"; + _stateFileTextBox.Dock = DockStyle.Fill; + + _timeoutInput.Minimum = 30; + _timeoutInput.Maximum = 3600; + _timeoutInput.Value = 300; + _timeoutInput.Dock = DockStyle.Fill; + + _pollInput.Minimum = 1; + _pollInput.Maximum = 60; + _pollInput.Value = 5; + _pollInput.Dock = DockStyle.Fill; + + _dryRunCheckBox.Text = "Dry run"; + _dryRunCheckBox.Checked = true; + _dryRunCheckBox.Dock = DockStyle.Fill; + + var browseButton = new Button { Text = "Browse...", Dock = DockStyle.Fill }; + browseButton.Click += BrowseButtonClick; + + panel.Controls.Add(Label("Server"), 0, 0); + panel.Controls.Add(_serverTextBox, 1, 0); + panel.Controls.Add(Label("Output"), 2, 0); + panel.Controls.Add(_outputTextBox, 3, 0); + panel.Controls.Add(browseButton, 4, 0); + panel.Controls.Add(Label("Timeout"), 5, 0); + panel.Controls.Add(_timeoutInput, 6, 0); + + panel.Controls.Add(Label("State"), 0, 1); + panel.Controls.Add(_stateFileTextBox, 1, 1); + panel.SetColumnSpan(_stateFileTextBox, 3); + panel.Controls.Add(_dryRunCheckBox, 4, 1); + panel.Controls.Add(Label("Poll sec."), 5, 1); + panel.Controls.Add(_pollInput, 6, 1); + + return panel; + } + + private Control BuildActionPanel() + { + var panel = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.LeftToRight, Padding = new Padding(0, 8, 0, 0) }; + _diagnoseButton = ActionButton("Diagnose", DiagnoseClick); + _beforeButton = ActionButton("Snapshot Before", BeforeClick); + _afterButton = ActionButton("Snapshot After", AfterClick); + _compareButton = ActionButton("Compare", CompareClick); + _shutdownButton = ActionButton("Shutdown", ShutdownClick); + _restoreButton = ActionButton("Restore", RestoreClick); + + panel.Controls.Add(_diagnoseButton); + panel.Controls.Add(_beforeButton); + panel.Controls.Add(_afterButton); + panel.Controls.Add(_compareButton); + panel.Controls.Add(_shutdownButton); + panel.Controls.Add(_restoreButton); + return panel; + } + + private Control BuildTabs() + { + var tabs = new TabControl { Dock = DockStyle.Fill }; + var statusPage = new TabPage("Status / Results"); + var logPage = new TabPage("Operation Log"); + + ConfigureGrid(_statusGrid); + _statusGrid.Columns.Add("Scope", "Scope"); + _statusGrid.Columns.Add("Application", "Application"); + _statusGrid.Columns.Add("Name", "Name"); + _statusGrid.Columns.Add("Status", "Status"); + _statusGrid.Columns.Add("Details", "Details"); + statusPage.Controls.Add(_statusGrid); + + ConfigureGrid(_logGrid); + _logGrid.Columns.Add("Time", "Time"); + _logGrid.Columns.Add("Level", "Level"); + _logGrid.Columns.Add("Message", "Message"); + _logGrid.Columns[2].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill; + logPage.Controls.Add(_logGrid); + + tabs.TabPages.Add(statusPage); + tabs.TabPages.Add(logPage); + return tabs; + } + + private void DiagnoseClick(object sender, EventArgs e) + { + var server = _serverTextBox.Text.Trim(); + RunAsync("Diagnosing WMI access...", () => _service.Diagnose(server)); + } + + private void BeforeClick(object sender, EventArgs e) + { + var options = GetOptions(); + RunAsync("Creating before snapshot...", () => + { + var snapshot = _service.CreateSnapshot(options.Server); + _service.SaveSnapshot(options.OutputDirectory, "before.json", snapshot); + ShowSnapshot(snapshot); + }); + } + + private void AfterClick(object sender, EventArgs e) + { + var options = GetOptions(); + RunAsync("Creating after snapshot...", () => + { + var snapshot = _service.CreateSnapshot(options.Server); + _service.SaveSnapshot(options.OutputDirectory, "after.json", snapshot); + ShowSnapshot(snapshot); + }); + } + + private void CompareClick(object sender, EventArgs e) + { + var options = GetOptions(); + RunAsync("Comparing before and after snapshots...", () => + { + var before = JsonFileStore.Load(Path.Combine(options.OutputDirectory, "before.json")); + var after = JsonFileStore.Load(Path.Combine(options.OutputDirectory, "after.json")); + var diff = SnapshotComparer.Compare(before, after); + _service.SaveDiff(options.OutputDirectory, "diff.json", diff); + ShowDiff(diff); + }); + } + + private void ShutdownClick(object sender, EventArgs e) + { + if (!ConfirmDangerousAction("Shutdown")) + { + return; + } + + var options = GetOptions(); + RunAsync("Preparing shutdown...", () => + { + var snapshot = _service.CreateSnapshot(options.Server); + _service.SaveSnapshot(options.OutputDirectory, "before.json", snapshot); + var plan = _service.CreateShutdownPlan(snapshot, options.Server); + _service.SavePlan(options.OutputDirectory, "shutdown-plan.json", plan); + ShowPlan(plan); + _service.ExecutePlan(plan, options); + if (!options.DryRun) + { + var after = _service.CreateSnapshot(options.Server); + _service.SaveSnapshot(options.OutputDirectory, "shutdown-after.json", after); + ShowSnapshot(after); + } + }); + } + + private void RestoreClick(object sender, EventArgs e) + { + if (!ConfirmDangerousAction("Restore")) + { + return; + } + + var options = GetOptions(); + RunAsync("Preparing restore...", () => + { + var snapshot = JsonFileStore.Load(ResolveStateFile(options)); + var plan = _service.CreateRestorePlan(snapshot, options.Server); + _service.SavePlan(options.OutputDirectory, "restore-plan.json", plan); + ShowPlan(plan); + _service.ExecutePlan(plan, options); + if (!options.DryRun) + { + var after = _service.CreateSnapshot(options.Server); + _service.SaveSnapshot(options.OutputDirectory, "restore-after.json", after); + ShowSnapshot(after); + } + }); + } + + private void RunAsync(string status, Action work) + { + SetBusy(true, status); + Task.Run(() => + { + try + { + work(); + AppendLog(new LogEntry { Timestamp = DateTime.Now, Level = LogLevel.Success, Message = "Operation completed." }); + SetBusy(false, "Ready."); + } + catch (Exception ex) + { + AppendLog(new LogEntry { Timestamp = DateTime.Now, Level = LogLevel.Error, Message = ex.Message }); + SetBusy(false, "Failed: " + ex.Message); + } + }); + } + + private OperationOptions GetOptions() + { + var server = string.IsNullOrWhiteSpace(_serverTextBox.Text) ? Environment.MachineName : _serverTextBox.Text.Trim(); + var output = string.IsNullOrWhiteSpace(_outputTextBox.Text) + ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "BizTalkPlatformManagementTool") + : _outputTextBox.Text.Trim(); + var stateFile = string.IsNullOrWhiteSpace(_stateFileTextBox.Text) ? "before.json" : _stateFileTextBox.Text.Trim(); + + return new OperationOptions + { + Server = server, + OutputDirectory = output, + StateFile = stateFile, + DryRun = _dryRunCheckBox.Checked, + WaitTimeoutSeconds = (int)_timeoutInput.Value, + PollIntervalSeconds = (int)_pollInput.Value + }; + } + + private string ResolveStateFile(OperationOptions options) + { + return Path.IsPathRooted(options.StateFile) ? options.StateFile : Path.Combine(options.OutputDirectory, options.StateFile); + } + + private bool ConfirmDangerousAction(string actionName) + { + if (_dryRunCheckBox.Checked) + { + return true; + } + + var result = MessageBox.Show( + actionName + " will change the BizTalk runtime state on server '" + _serverTextBox.Text + "'. Continue?", + "Confirm BizTalk Runtime Change", + MessageBoxButtons.YesNo, + MessageBoxIcon.Warning, + MessageBoxDefaultButton.Button2); + return result == DialogResult.Yes; + } + + private void ShowSnapshot(BizTalkSnapshot snapshot) + { + InvokeIfRequired(() => + { + _statusGrid.Rows.Clear(); + foreach (var app in snapshot.Applications) + { + foreach (var item in app.ReceiveLocations) + { + _statusGrid.Rows.Add("ReceiveLocation", app.Application, item.Name, ArtifactStates.FormatReceiveLocation(item.Enabled), item.AdapterName + " " + item.Address); + } + foreach (var item in app.SendPorts) + { + _statusGrid.Rows.Add("SendPort", app.Application, item.Name, ArtifactStates.FormatSendPort(item.Status), item.PrimaryTransportType + " " + item.PrimaryTransportAddress); + } + foreach (var item in app.Orchestrations) + { + _statusGrid.Rows.Add("Orchestration", app.Application, item.Name, ArtifactStates.FormatOrchestration(item.OrchestrationStatus), string.Empty); + } + } + foreach (var item in snapshot.HostInstances) + { + _statusGrid.Rows.Add("HostInstance", string.Empty, item.InstanceName, item.StateText, item.HostName + " on " + item.Server); + } + }); + } + + private void ShowDiff(SnapshotDiff diff) + { + InvokeIfRequired(() => + { + _statusGrid.Rows.Clear(); + foreach (var item in diff.ArtifactDifferences) + { + _statusGrid.Rows.Add("Diff", item.Application, item.Name, item.Before + " -> " + item.After, item.ArtifactType); + } + foreach (var item in diff.HostInstanceDifferences) + { + _statusGrid.Rows.Add("HostDiff", string.Empty, item.InstanceName, item.Before + " -> " + item.After, item.Server); + } + }); + } + + private void ShowPlan(OperationPlan plan) + { + InvokeIfRequired(() => + { + _statusGrid.Rows.Clear(); + foreach (var step in plan.Steps) + { + _statusGrid.Rows.Add(step.Kind, step.Application, step.Name, step.Execute ? step.Action : "Skipped", step.Warning ?? step.MethodName); + } + }); + } + + private void AppendLog(LogEntry entry) + { + InvokeIfRequired(() => + { + var index = _logGrid.Rows.Add(entry.Timestamp.ToString("HH:mm:ss"), entry.Level.ToString(), entry.Message); + var row = _logGrid.Rows[index]; + if (entry.Level == LogLevel.Error) + { + row.DefaultCellStyle.ForeColor = Color.DarkRed; + } + else if (entry.Level == LogLevel.Warning) + { + row.DefaultCellStyle.ForeColor = Color.DarkOrange; + } + else if (entry.Level == LogLevel.Success) + { + row.DefaultCellStyle.ForeColor = Color.DarkGreen; + } + _logGrid.FirstDisplayedScrollingRowIndex = Math.Max(0, _logGrid.Rows.Count - 1); + }); + } + + private void SetBusy(bool busy, string status) + { + InvokeIfRequired(() => + { + _diagnoseButton.Enabled = !busy; + _beforeButton.Enabled = !busy; + _afterButton.Enabled = !busy; + _compareButton.Enabled = !busy; + _shutdownButton.Enabled = !busy; + _restoreButton.Enabled = !busy; + _statusLabel.Text = status; + }); + } + + private void BrowseButtonClick(object sender, EventArgs e) + { + using (var dialog = new FolderBrowserDialog()) + { + dialog.SelectedPath = Directory.Exists(_outputTextBox.Text) ? _outputTextBox.Text : Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory); + if (dialog.ShowDialog(this) == DialogResult.OK) + { + _outputTextBox.Text = dialog.SelectedPath; + } + } + } + + private void InvokeIfRequired(Action action) + { + if (IsDisposed) + { + return; + } + + if (InvokeRequired) + { + BeginInvoke(action); + } + else + { + action(); + } + } + + private static Label Label(string text) + { + return new Label { Text = text, Dock = DockStyle.Fill, TextAlign = ContentAlignment.MiddleLeft }; + } + + private static Button ActionButton(string text, EventHandler handler) + { + var button = new Button { Text = text, Width = 148, Height = 34, Margin = new Padding(0, 0, 8, 0) }; + button.Click += handler; + return button; + } + + private static void ConfigureGrid(DataGridView grid) + { + grid.Dock = DockStyle.Fill; + grid.AllowUserToAddRows = false; + grid.AllowUserToDeleteRows = false; + grid.ReadOnly = true; + grid.RowHeadersVisible = false; + grid.SelectionMode = DataGridViewSelectionMode.FullRowSelect; + grid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells; + grid.BackgroundColor = SystemColors.Window; + } + } +} diff --git a/src/BizTalkStatusTool.ps1 b/src/BizTalkStatusTool.ps1 deleted file mode 100644 index 388aec2..0000000 --- a/src/BizTalkStatusTool.ps1 +++ /dev/null @@ -1,676 +0,0 @@ -<# -BizTalk Status Tool – Extended Version -Includes: -- Applications Snapshot (RL/SP/ORCH) -- Host Instances Snapshot -- JSON / CSV / HTML / Console -- Compare (Before / After) -- HostInstance Diff (New / Removed / Changed) -#> - -# ====================================================== -# PARAMETERS -# ====================================================== -param( - [switch]$Before, - [switch]$After, - [switch]$Compare, - [switch]$Diag, - [string]$Server, - [string]$MgmtDbServer, - [string]$MgmtDbName, - [string]$OutDir -) - -if (-not $Server) { $Server = $env:COMPUTERNAME } -if (-not $OutDir) { $OutDir = "." } - -$ErrorActionPreference = "Stop" -$Global:LogFile = Join-Path (Get-Location) "BizTalkStatusTool.log" - -# ====================================================== -# LOG -# ====================================================== -function Write-Log { - param([string]$Message,[ValidateSet('INFO','WARN','ERROR','DEBUG')]$Level='INFO') - - $ts = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") - $line = "[$ts][$Level] $Message" - $fg = switch ($Level) { - INFO {"Gray"} WARN {"Yellow"} ERROR {"Red"} DEBUG {"DarkCyan"} - } - Write-Host $line -ForegroundColor $fg - Add-Content -Path $Global:LogFile -Value $line -} - -# ====================================================== -# STATUS MAPPING -# ====================================================== -function Format-Status { - param([string]$ArtifactType, $Value) - - $r = [ordered]@{ - Text = "" - Class = "neutral" - ConsoleColor = "Yellow" - } - - switch ($ArtifactType) { - - "ReceiveLocation" { - if ($Value -eq $true) { - $r.Text="Enabled"; $r.Class="ok"; $r.ConsoleColor="Green" - } - elseif ($Value -eq $false) { - $r.Text="Disabled"; $r.Class="bad"; $r.ConsoleColor="Red" - } - } - - "SendPort" { - switch ([int]$Value) { - 3 { $r.Text="Started"; $r.Class="ok"; $r.ConsoleColor="Green" } - 2 { $r.Text="Stopped"; $r.Class="bad"; $r.ConsoleColor="Red" } - 1 { $r.Text="Bound"; $r.Class="bad"; $r.ConsoleColor="Red" } - default { $r.Text="Unknown"; $r.Class="bad"; $r.ConsoleColor="Red" } - } - } - - "Orchestration" { - switch ([int]$Value) { - 4 { $r.Text="Started"; $r.Class="ok"; $r.ConsoleColor="Green" } - 3 { $r.Text="Stopped"; $r.Class="bad"; $r.ConsoleColor="Red" } - 2 { $r.Text="Bound"; $r.Class="bad"; $r.ConsoleColor="Red" } - default { $r.Text="Unknown"; $r.Class="bad"; $r.ConsoleColor="Red" } - } - } - - "HostInstance" { - switch ([int]$Value) { - 4 { $r.Text="Started"; $r.Class="ok"; $r.ConsoleColor="Green" } - 2 { $r.Text="StartPending"; $r.Class="warn"; $r.ConsoleColor="Yellow" } - 3 { $r.Text="StopPending"; $r.Class="warn"; $r.ConsoleColor="Yellow" } - 1 { $r.Text="Stopped"; $r.Class="bad"; $r.ConsoleColor="Red" } - default { $r.Text="Unknown"; $r.Class="bad"; $r.ConsoleColor="Red" } - } - } - } - - return [pscustomobject]$r -} - -# ====================================================== -# MGMT DB -# ====================================================== -function Get-BizTalkMgmtDbInfo { - param([string]$MgmtDbServer,[string]$MgmtDbName) - - $reg = "HKLM:\SOFTWARE\Microsoft\BizTalk Server\3.0\Administration" - - try { - $p = Get-ItemProperty $reg - if (-not $MgmtDbServer) { $MgmtDbServer = $p.MgmtDBServer } - if (-not $MgmtDbName) { $MgmtDbName = $p.MgmtDBName } - } - catch { - if (-not $MgmtDbServer) { $MgmtDbServer = "localhost" } - if (-not $MgmtDbName) { $MgmtDbName = "BizTalkMgmtDb" } - } - - return @{ Server=$MgmtDbServer; Name=$MgmtDbName } -} - -# ====================================================== -# HOST INSTANCES -# ====================================================== -function Get-BizTalkHostInstances { - param([string]$Server) - - $wmi = @{ - Namespace = "root\MicrosoftBizTalkServer" - ComputerName = $Server - ErrorAction = 'Stop' - } - - $list = Get-WmiObject MSBTS_HostInstance @wmi - - $result = foreach ($hi in $list) { - $st = Format-Status -ArtifactType "HostInstance" -Value $hi.ServiceState - [pscustomobject]@{ - InstanceName = $hi.InstanceName - HostName = $hi.HostName - Server = $hi.RunningServer - RawState = $hi.ServiceState - StateText = $st.Text - StateClass = $st.Class - ConsoleColor = $st.ConsoleColor - } - } - - return $result -} - -function Show-HostInstancesConsole { - param($HostInstances) - Write-Host "`n=== Host Instances ===" -ForegroundColor Yellow - foreach ($hi in $HostInstances) { - Write-Host ("{0,-35} {1,-20} {2,-10}" -f $hi.InstanceName,$hi.HostName,$hi.StateText) ` - -ForegroundColor $hi.ConsoleColor - } -} - -function Export-HostInstancesHtml { - param($HostInstances) - - $html = @() - $html += "

Host Instances

" - $html += "" - $html += "" - - foreach ($hi in $HostInstances) { - $html += " - - - - - " - } - - $html += "
InstanceHostServerStatus
$($hi.InstanceName)$($hi.HostName)$($hi.Server)$($hi.StateText)
" - return ($html -join "`n") -} - -function Export-HostInstancesCsv { - param($HostInstances, [string]$CsvFile) - $HostInstances | - Select InstanceName,HostName,Server,StateText | - Export-Csv -NoTypeInformation -Encoding UTF8 -Path $CsvFile -} - -# ====================================================== -# SNAPSHOT -# ====================================================== -function Get-BizTalkSnapshot { - param( - [string]$OutputFile, - [string]$Server, - [string]$MgmtDbServer, - [string]$MgmtDbName - ) - - Write-Log "Snapshot: Server=$Server File=$OutputFile" - - [void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.BizTalk.ExplorerOM") - $db = Get-BizTalkMgmtDbInfo -MgmtDbServer $MgmtDbServer -MgmtDbName $MgmtDbName - - $cat = New-Object Microsoft.BizTalk.ExplorerOM.BtsCatalogExplorer - $cat.ConnectionString = "Server={0};Database={1};Integrated Security=SSPI" -f $db.Server,$db.Name - - $apps = $cat.Applications - - # WMI lookup tables - $wmi = @{ Namespace='root\MicrosoftBizTalkServer'; ComputerName=$Server; ErrorAction='Stop' } - - $rlW = Get-WmiObject MSBTS_ReceiveLocation @wmi | - Select Name, @{n='Enabled';e={-not $_.IsDisabled}}, AdapterName, @{n='Address';e={$_.InboundTransportURL}} - - $spW = Get-WmiObject MSBTS_SendPort @wmi | - Select Name, @{n='Status';e={$_.Status}}, @{n='PrimaryTransportType';e={$_.PTTransportType}}, - @{n='PrimaryTransportAddress';e={$_.PTAddress}} - - $orW = Get-WmiObject MSBTS_Orchestration @wmi | - Select Name, @{n='OrchestrationStatus';e={$_.OrchestrationStatus}} - - $mapRL=@{}; foreach($i in $rlW){$mapRL[$i.Name]=$i} - $mapSP=@{}; foreach($i in $spW){$mapSP[$i.Name]=$i} - $mapOR=@{}; foreach($i in $orW){$mapOR[$i.Name]=$i} - - $snapshot=@() - - foreach ($app in $apps) { - - $rlItems=@() - foreach ($rp in $app.ReceivePorts) { - foreach ($rl in $rp.ReceiveLocations) { - if ($mapRL.ContainsKey($rl.Name)) { - $w = $mapRL[$rl.Name] - $rlItems += [pscustomobject]@{ - Name=$rl.Name - Enabled=$w.Enabled - AdapterName=$w.AdapterName - Address=$w.Address - } - } - } - } - - $spItems=@() - foreach ($sp in $app.SendPorts) { - if ($mapSP.ContainsKey($sp.Name)) { - $w = $mapSP[$sp.Name] - $spItems += [pscustomobject]@{ - Name=$sp.Name - Status=$w.Status - PrimaryTransportType=$w.PrimaryTransportType - PrimaryTransportAddress=$w.PrimaryTransportAddress - } - } - } - - $orItems=@() - foreach ($o in $app.Orchestrations) { - $key = if ($mapOR.ContainsKey($o.FullName)) { $o.FullName } else { $o.Name } - if ($mapOR.ContainsKey($key)) { - $w = $mapOR[$key] - $orItems += [pscustomobject]@{ - Name=$o.FullName - OrchestrationStatus=$w.OrchestrationStatus - } - } - } - - $snapshot += [pscustomobject]@{ - Application=$app.Name - ReceiveLocations=$rlItems - SendPorts=$spItems - Orchestrations=$orItems - } - } - - # Host Instances - $hostInstances = Get-BizTalkHostInstances -Server $Server - - # Export JSON (Applications + HostInstances) - $full = [pscustomobject]@{ - Applications = $snapshot - HostInstances = $hostInstances - } - - $full | ConvertTo-Json -Depth 8 | Out-File $OutputFile -Encoding UTF8 - - # Export CSV - $csvArtifact = foreach ($app in $snapshot) { - foreach ($rl in $app.ReceiveLocations) { - $st = Format-Status -ArtifactType "ReceiveLocation" -Value $rl.Enabled - [pscustomobject]@{ - Application=$app.Application - Type="ReceiveLocation" - Name=$rl.Name - Status=$st.Text - } - } - foreach ($sp in $app.SendPorts) { - $st = Format-Status -ArtifactType "SendPort" -Value $sp.Status - [pscustomobject]@{ - Application=$app.Application - Type="SendPort" - Name=$sp.Name - Status=$st.Text - } - } - foreach ($o in $app.Orchestrations) { - $st = Format-Status -ArtifactType "Orchestration" -Value $o.OrchestrationStatus - [pscustomobject]@{ - Application=$app.Application - Type="Orchestration" - Name=$o.Name - Status=$st.Text - } - } - } - - $csvArtifact | - Export-Csv -Encoding UTF8 -NoTypeInformation -Path ($OutputFile + ".csv") - - Export-HostInstancesCsv -HostInstances $hostInstances -CsvFile ($OutputFile + ".hosts.csv") - - # HTML - $html=@() - $html+="" - $html+="

BizTalk Snapshot

" - - foreach ($app in $snapshot) { - $html+="

Application: $($app.Application)

" - - if ($app.ReceiveLocations.Count -gt 0) { - $html+="

Receive Locations

" - foreach ($rl in $app.ReceiveLocations) { - $st = Format-Status -ArtifactType "ReceiveLocation" -Value $rl.Enabled - $html+="" - } - $html+="
NameStatus
$($rl.Name)$($st.Text)
" - } - - if ($app.SendPorts.Count -gt 0) { - $html+="

Send Ports

" - foreach ($sp in $app.SendPorts) { - $st = Format-Status -ArtifactType "SendPort" -Value $sp.Status - $html+="" - } - $html+="
NameStatus
$($sp.Name)$($st.Text)
" - } - - if ($app.Orchestrations.Count -gt 0) { - $html+="

Orchestrations

" - foreach ($o in $app.Orchestrations) { - $st = Format-Status -ArtifactType "Orchestration" -Value $o.OrchestrationStatus - $html+="" - } - $html+="
NameStatus
$($o.Name)$($st.Text)
" - } - } - - # Add Host Instances - $html += Export-HostInstancesHtml -HostInstances $hostInstances - - $html+="" - $html -join "`n" | Out-File ($OutputFile + ".html") -Encoding UTF8 - - # Console Output - Write-Host "`n===== Snapshot =====" -ForegroundColor Yellow - foreach ($app in $snapshot) { - Write-Host "`nApplication: $($app.Application)" -ForegroundColor Cyan - - foreach ($rl in $app.ReceiveLocations) { - $st = Format-Status -ArtifactType "ReceiveLocation" -Value $rl.Enabled - Write-Host (" RL {0,-60} {1}" -f $rl.Name,$st.Text) -ForegroundColor $st.ConsoleColor - } - - foreach ($sp in $app.SendPorts) { - $st = Format-Status -ArtifactType "SendPort" -Value $sp.Status - Write-Host (" SP {0,-60} {1}" -f $sp.Name,$st.Text) -ForegroundColor $st.ConsoleColor - } - - foreach ($o in $app.Orchestrations) { - $st = Format-Status -ArtifactType "Orchestration" -Value $o.OrchestrationStatus - Write-Host (" OR {0,-60} {1}" -f $o.Name,$st.Text) -ForegroundColor $st.ConsoleColor - } - } - - Show-HostInstancesConsole -HostInstances $hostInstances - - Write-Host "Snapshot erstellt: $(Resolve-Path $OutputFile)" -ForegroundColor Green -} - -# ====================================================== -# COMPARE (Before/After) -# ====================================================== -function Compare-BizTalkSnapshots { - param( - [string]$Before, - [string]$After, - [string]$HtmlReport = "BizTalkDiff.html" - ) - - $b = Get-Content $Before | ConvertFrom-Json - $a = Get-Content $After | ConvertFrom-Json - - $diff = @() - - function AddDiff { - param($app,$type,$name,$before,$after) - - $bf = Format-Status -ArtifactType $type -Value $before - $af = Format-Status -ArtifactType $type -Value $after - - $script:diff += [pscustomobject]@{ - Application = $app - ArtifactType= $type - Name = $name - BeforeText = $bf.Text - BeforeClass = $bf.Class - AfterText = $af.Text - AfterClass = $af.Class - } - } - - foreach ($appAfter in $a.Applications) { - $appBefore = $b.Applications | Where-Object {$_.Application -eq $appAfter.Application} - - # RECEIVE LOCATIONS - foreach ($x in $appAfter.ReceiveLocations) { - $old = $appBefore.ReceiveLocations | Where-Object {$_.Name -eq $x.Name} - if (-not $old) { AddDiff $appAfter.Application "ReceiveLocation" $x.Name $null $x.Enabled; continue } - if ($old.Enabled -ne $x.Enabled) { AddDiff $appAfter.Application "ReceiveLocation" $x.Name $old.Enabled $x.Enabled } - } - foreach ($x in $appBefore.ReceiveLocations) { - if (-not ($appAfter.ReceiveLocations | Where-Object {$_.Name -eq $x.Name})) { - AddDiff $appBefore.Application "ReceiveLocation" $x.Name $x.Enabled $null - } - } - - # SEND PORTS - foreach ($x in $appAfter.SendPorts) { - $old = $appBefore.SendPorts | Where-Object {$_.Name -eq $x.Name} - if (-not $old) { AddDiff $appAfter.Application "SendPort" $x.Name $null $x.Status; continue } - if ($old.Status -ne $x.Status) { AddDiff $appAfter.Application "SendPort" $x.Name $old.Status $x.Status } - } - foreach ($x in $appBefore.SendPorts) { - if (-not ($appAfter.SendPorts | Where-Object {$_.Name -eq $x.Name})) { - AddDiff $appBefore.Application "SendPort" $x.Name $x.Status $null - } - } - - # ORCHESTRATIONS - foreach ($x in $appAfter.Orchestrations) { - $old = $appBefore.Orchestrations | Where-Object {$_.Name -eq $x.Name} - if (-not $old) { AddDiff $appAfter.Application "Orchestration" $x.Name $null $x.OrchestrationStatus; continue } - if ($old.OrchestrationStatus -ne $x.OrchestrationStatus) { - AddDiff $appAfter.Application "Orchestration" $x.Name $old.OrchestrationStatus $x.OrchestrationStatus - } - } - foreach ($x in $appBefore.Orchestrations) { - if (-not ($appAfter.Orchestrations | Where-Object {$_.Name -eq $x.Name})) { - AddDiff $appBefore.Application "Orchestration" $x.Name $x.OrchestrationStatus $null - } - } - } - - # ====================================================== - # HOST INSTANCE DIFF (Option H-DIFF2) - # ====================================================== - - $beforeHI = $b.HostInstances - $afterHI = $a.HostInstances - - $hiDiff_New=@() - $hiDiff_Removed=@() - $hiDiff_Changed=@() - - # New - foreach ($hi in $afterHI) { - if (-not ($beforeHI | Where-Object {$_.InstanceName -eq $hi.InstanceName})) { - $hiDiff_New += $hi - } - } - - # Removed - foreach ($hi in $beforeHI) { - if (-not ($afterHI | Where-Object {$_.InstanceName -eq $hi.InstanceName})) { - $hiDiff_Removed += $hi - } - } - - # Changed - foreach ($hiAfter in $afterHI) { - $hiBefore = $beforeHI | Where-Object {$_.InstanceName -eq $hiAfter.InstanceName} - if ($hiBefore) { - if ($hiBefore.StateText -ne $hiAfter.StateText) { - $hiDiff_Changed += [pscustomobject]@{ - Instance = $hiAfter.InstanceName - Before = $hiBefore.StateText - After = $hiAfter.StateText - } - } - } - } - - # ====================================================== - # JSON DIFF EXPORT - # ====================================================== - $jsonDiff = [pscustomobject]@{ - ArtifactsDiff = $diff - HostInstances = [pscustomobject]@{ - New = $hiDiff_New - Removed = $hiDiff_Removed - Changed = $hiDiff_Changed - } - } - - $jsonDiff | ConvertTo-Json -Depth 8 | Out-File "diff.json" -Encoding UTF8 - - # ====================================================== - # CSV DIFF EXPORT - # ====================================================== - $diff | Export-Csv -Path "diff.csv" -NoTypeInformation -Encoding UTF8 - - # ====================================================== - # HTML DIFF EXPORT - # ====================================================== - $html=@() - $html+="" - $html+="

BizTalk Differences

" - - # Artifacts - $html+="

Artifact Differences

" - $html+="" - foreach ($d in $diff) { - $html+=" - - - - - - " - } - $html+="
ApplicationTypeNameBeforeAfter
$($d.Application)$($d.ArtifactType)$($d.Name)$($d.BeforeText)$($d.AfterText)
" - - # HostInstance DIFF - $html+="

Host Instance Changes

" - - # NEW - $html+="

New

" - foreach ($hi in $hiDiff_New) { - $html+=" - - - - - " - } - $html+="
InstanceHostServerStatus
$($hi.InstanceName)$($hi.HostName)$($hi.Server)$($hi.StateText)
" - - # REMOVED - $html+="

Removed

" - foreach ($hi in $hiDiff_Removed) { - $html+=" - - - - - " - } - $html+="
InstanceHostServerStatus
$($hi.InstanceName)$($hi.HostName)$($hi.Server)$($hi.StateText)
" - - # CHANGED - $html+="

Changed

" - foreach ($hi in $hiDiff_Changed) { - $before = Format-Status -ArtifactType "HostInstance" -Value $hi.Before - $after = Format-Status -ArtifactType "HostInstance" -Value $hi.After - $html+=" - - - - " - } - $html+="
InstanceBeforeAfter
$($hi.Instance)$($hi.Before)$($hi.After)
" - - $html+="" - $html -join "`n" | Out-File $HtmlReport -Encoding UTF8 - - # ====================================================== - # CONSOLE OUTPUT - # ====================================================== - Write-Host "`n===== BizTalk DIFF =====" -ForegroundColor Yellow - - if ($diff.Count -eq 0 -and - $hiDiff_New.Count -eq 0 -and - $hiDiff_Removed.Count -eq 0 -and - $hiDiff_Changed.Count -eq 0) - { - Write-Host "Keine Unterschiede." -ForegroundColor Green - } - else { - Write-Host "`n--- Artifacts ---" -ForegroundColor Cyan - foreach ($d in $diff) { - Write-Host "`nApplication: $($d.Application)" -ForegroundColor Cyan - Write-Host "Artifact: $($d.ArtifactType)" - Write-Host "Name: $($d.Name)" - Write-Host ("Before: {0}" -f $d.BeforeText) -ForegroundColor $( - if ($d.BeforeClass -eq "ok") {"Green"} elseif ($d.BeforeClass -eq "warn") {"Yellow"} else {"Red"}) - Write-Host ("After: {0}" -f $d.AfterText) -ForegroundColor $( - if ($d.AfterClass -eq "ok") {"Green"} elseif ($d.AfterClass -eq "warn") {"Yellow"} else {"Red"}) - } - - Write-Host "`n--- Host Instances (New) ---" -ForegroundColor Cyan - foreach ($hi in $hiDiff_New) { - Write-Host ("NEW: {0} {1}" -f $hi.InstanceName,$hi.StateText) -ForegroundColor $hi.ConsoleColor - } - - Write-Host "`n--- Host Instances (Removed) ---" -ForegroundColor Cyan - foreach ($hi in $hiDiff_Removed) { - Write-Host ("REMOVED: {0} {1}" -f $hi.InstanceName,$hi.StateText) -ForegroundColor Red - } - - Write-Host "`n--- Host Instances (Changed) ---" -ForegroundColor Cyan - foreach ($hi in $hiDiff_Changed) { - Write-Host ("CHANGED: {0} {1} -> {2}" -f $hi.Instance,$hi.Before,$hi.After) -ForegroundColor Yellow - } - } - - Write-Host "HTML Diff exportiert: $HtmlReport" -ForegroundColor Green -} - -# ====================================================== -# DISPATCH -# ====================================================== -if (-not (Test-Path $OutDir)) { - New-Item -ItemType Directory -Path $OutDir | Out-Null -} -Set-Location $OutDir -Write-Log "Working dir: $(Get-Location)" - -if ($Diag) { - Write-Host "DIAG → Server: $Server" -ForegroundColor Yellow - try { - $sp = Get-WmiObject MSBTS_SendPort -Namespace root\MicrosoftBizTalkServer -ComputerName $Server | - Select -First 3 Name,Status - Write-Host "WMI OK:" -ForegroundColor Green - $sp | Format-Table - } - catch { - Write-Host "ERROR: $($_.Exception.Message)" -ForegroundColor Red - } - exit -} - -if ($Before) { Get-BizTalkSnapshot -OutputFile "before.json" -Server $Server -MgmtDbServer $MgmtDbServer -MgmtDbName $MgmtDbName; exit } -if ($After) { Get-BizTalkSnapshot -OutputFile "after.json" -Server $Server -MgmtDbServer $MgmtDbServer -MgmtDbName $MgmtDbName; exit } -if ($Compare){ Compare-BizTalkSnapshots -Before "before.json" -After "after.json"; exit } - -Write-Host "Usage:" -Write-Host " .\BizTalkStatusTool.ps1 -Before" -Write-Host " .\BizTalkStatusTool.ps1 -After" -Write-Host " .\BizTalkStatusTool.ps1 -Compare" -Write-Host " .\BizTalkStatusTool.ps1 -Diag" \ No newline at end of file