Files

1041 lines
45 KiB
PowerShell
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<#
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)
@"
<html>
<head>
<meta charset='utf-8'/>
<style>
body{font-family:Segoe UI,Arial,sans-serif;font-size:13px;color:#222;margin:20px;}
h2{margin-top:0}
section{margin-bottom:24px}
table{border-collapse:collapse;width:100%;margin:8px 0 16px 0}
th,td{border:1px solid #ccc;padding:6px 8px;text-align:left}
th{background:#f8f8f8}
tr:nth-child(even){background:#fcfcfc}
.ok{color:#0a7a0a;font-weight:600}
.bad{color:#c12727;font-weight:600}
.warn{color:#c88719;font-weight:600}
.summary{display:flex;gap:16px;margin:8px 0 12px 0}
.summary .card{border:1px solid #ddd;border-radius:6px;padding:8px 12px;background:#fafafa}
.summary .value{font-size:18px;font-weight:700;margin-top:4px}
.app{margin-top:18px}
.small{color:#666}
</style>
<title>$Title</title>
</head>
<body>
<h2>$Title</h2>
"@
}
function New-Html-Footer { "</body></html>" }
# ============================= 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 += "<section class='summary'>"
$html += "<div class='card'><div>Applications</div><div class='value'>$totalApps</div></div>"
$html += "<div class='card'><div>Receive Locations</div><div class='value'>$totalRL</div></div>"
$html += "<div class='card'><div>Send Ports</div><div class='value'>$totalSP</div></div>"
$html += "<div class='card'><div>Orchestrations</div><div class='value'>$totalOR</div></div>"
$html += "<div class='card'><div>Host Instances</div><div class='value'>$totalHI</div></div>"
$html += "</section>"
foreach ($app in $snapshot) {
$html += "<section class='app'>"
$html += "<h3>Application: $($app.Application)</h3>"
if ($app.ReceiveLocations.Count -gt 0) {
$html += "<h4>Receive Locations</h4><table><tr><th>Name</th><th>Status</th><th class='small'>Adapter</th><th class='small'>Address</th></tr>"
foreach ($rl in $app.ReceiveLocations) {
$st = Format-Status -ArtifactType 'ReceiveLocation' -Value $rl.Enabled
$html += "<tr><td>$($rl.Name)</td><td class='$($st.Class)'>$($st.Text)</td><td class='small'>$($rl.AdapterName)</td><td class='small'>$($rl.Address)</td></tr>"
}
$html += "</table>"
}
if ($app.SendPorts.Count -gt 0) {
$html += "<h4>Send Ports</h4><table><tr><th>Name</th><th>Status</th><th class='small'>PrimaryTransportType</th><th class='small'>PrimaryTransportAddress</th></tr>"
foreach ($sp in $app.SendPorts) {
$st = Format-Status -ArtifactType 'SendPort' -Value $sp.Status
$html += "<tr><td>$($sp.Name)</td><td class='$($st.Class)'>$($st.Text)</td><td class='small'>$($sp.PrimaryTransportType)</td><td class='small'>$($sp.PrimaryTransportAddress)</td></tr>"
}
$html += "</table>"
}
if ($app.Orchestrations.Count -gt 0) {
$html += "<h4>Orchestrations</h4><table><tr><th>Name</th><th>Status</th></tr>"
foreach ($o in $app.Orchestrations) {
$st = Format-Status -ArtifactType 'Orchestration' -Value $o.OrchestrationStatus
$html += "<tr><td>$($o.Name)</td><td class='$($st.Class)'>$($st.Text)</td></tr>"
}
$html += "</table>"
}
$html += "</section>"
}
# Host Instances
$html += "<section>"
$html += "<h3>Host Instances</h3>"
$html += "<table><tr><th>Instance</th><th>Host</th><th>Server</th><th>Status</th></tr>"
foreach ($hi in $hostInstances) {
$html += "<tr><td>$($hi.InstanceName)</td><td>$($hi.HostName)</td><td>$($hi.Server)</td><td class='$($hi.StateClass)'>$($hi.StateText)</td></tr>"
}
$html += "</table>"
$html += "</section>"
$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 += "<section class='summary'>"
$html += "<div class='card'><div>Artifact Differences</div><div class='value'>$artCount</div></div>"
$html += "<div class='card'><div>Hosts New</div><div class='value'>$hiNew</div></div>"
$html += "<div class='card'><div>Hosts Removed</div><div class='value'>$hiRem</div></div>"
$html += "<div class='card'><div>Hosts Changed</div><div class='value'>$hiChg</div></div>"
$html += "</section>"
# Artifacts
$html += "<section>"
$html += "<h3>Artifact Differences</h3>"
$html += "<table><tr><th>Application</th><th>Type</th><th>Name</th><th>Before</th><th>After</th></tr>"
foreach ($d in $diff) {
$html += "<tr><td>$($d.Application)</td><td>$($d.ArtifactType)</td><td>$($d.Name)</td><td class='$($d.BeforeClass)'>$($d.BeforeText)</td><td class='$($d.AfterClass)'>$($d.AfterText)</td></tr>"
}
$html += "</table>"
$html += "</section>"
# HostInstance: NEW
$html += "<section>"
$html += "<h3>Host Instance Changes</h3>"
$html += "<h4>New</h4><table><tr><th>Instance</th><th>Host</th><th>Server</th><th>Status</th></tr>"
foreach ($hi in $hiDiff_New) {
$html += "<tr><td>$($hi.InstanceName)</td><td>$($hi.HostName)</td><td>$($hi.Server)</td><td class='$($hi.StateClass)'>$($hi.StateText)</td></tr>"
}
$html += "</table>"
# HostInstance: REMOVED
$html += "<h4>Removed</h4><table><tr><th>Instance</th><th>Host</th><th>Server</th><th>Last Status</th></tr>"
foreach ($hi in $hiDiff_Removed) {
$html += "<tr><td>$($hi.InstanceName)</td><td>$($hi.HostName)</td><td>$($hi.Server)</td><td class='bad'>$($hi.StateText)</td></tr>"
}
$html += "</table>"
# HostInstance: CHANGED
$html += "<h4>Changed</h4><table><tr><th>Instance</th><th>Before</th><th>After</th></tr>"
foreach ($hi in $hiDiff_Changed) {
$before = Format-Status -ArtifactType 'HostInstance' -Value $hi.Before
$after = Format-Status -ArtifactType 'HostInstance' -Value $hi.After
$html += "<tr><td>$($hi.Instance)</td><td class='$($before.Class)'>$($hi.Before)</td><td class='$($after.Class)'>$($hi.After)</td></tr>"
}
$html += "</table>"
$html += "</section>"
$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
}