<# 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 }