<# .SYNOPSIS Compares two baselines taken by Get-MigrationBaseline.ps1 (before / after a migration). .DESCRIPTION Prints what changed in the areas that usually break after a hypervisor change: IP configuration, offline or read-only disks, missing volumes, ports no longer listening, automatic services no longer running, ghost adapters, secure channel, activation and new error sources. Exit code 1 when at least one check fails, so the script can gate a runbook. .EXAMPLE .\Compare-MigrationBaseline.ps1 -Before .\before.json -After .\after.json .NOTES Source: ANYSYS ICT, https://www.anysys.be/articles/ #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$Before, [Parameter(Mandatory)][string]$After ) $b = Get-Content -Path $Before -Raw | ConvertFrom-Json $a = Get-Content -Path $After -Raw | ConvertFrom-Json foreach ($f in @{ Before = $b; After = $a }.GetEnumerator()) { if (-not $f.Value.CapturedAt) { throw "$($f.Key) is not a file produced by Get-MigrationBaseline.ps1" } } $failed = 0 function Test-SameSet { # Compare-Object refuses empty input, so empty sets are handled explicitly. param($Reference, $Difference) $r = @($Reference | Where-Object { $_ }); $d = @($Difference | Where-Object { $_ }) if (-not $r.Count -and -not $d.Count) { return $true } if (-not $r.Count -or -not $d.Count) { return $false } return -not (Compare-Object $r $d) } function Write-Check { param([string]$Name, [bool]$Ok, [string]$Detail = '') if ($Ok) { Write-Host ("[ OK ] {0}" -f $Name) -ForegroundColor Green } else { Write-Host ("[FAIL] {0} {1}" -f $Name, $Detail) -ForegroundColor Red; $script:failed++ } } Write-Host ("Platform: {0} {1} -> {2} {3}" -f $b.Manufacturer, $b.Model, $a.Manufacturer, $a.Model) # Network: compare addressing, not adapter names (they change with the virtual NIC model). $ipB = @($b.Network | ForEach-Object { $_.IPv4 }) | Sort-Object $ipA = @($a.Network | ForEach-Object { $_.IPv4 }) | Sort-Object Write-Check 'IPv4 addresses' (Test-SameSet $ipB $ipA) ("before: {0} / after: {1}" -f ($ipB -join ','), ($ipA -join ',')) $gwB = @($b.Network | ForEach-Object { $_.Gateway }) | Sort-Object -Unique $gwA = @($a.Network | ForEach-Object { $_.Gateway }) | Sort-Object -Unique Write-Check 'Default gateway' (Test-SameSet $gwB $gwA) ("before: {0} / after: {1}" -f ($gwB -join ','), ($gwA -join ',')) $dnsB = @($b.Network | ForEach-Object { $_.Dns }) | Sort-Object -Unique $dnsA = @($a.Network | ForEach-Object { $_.Dns }) | Sort-Object -Unique Write-Check 'DNS servers' (Test-SameSet $dnsB $dnsA) ("before: {0} / after: {1}" -f ($dnsB -join ','), ($dnsA -join ',')) $ghost = @($a.GhostNics | Where-Object { $_ }) Write-Check 'No ghost network adapter' ($ghost.Count -eq 0) ("{0} non-present adapter(s): {1}" -f $ghost.Count, (($ghost.FriendlyName) -join ', ')) # Storage $badDisks = @($a.Disks | Where-Object { $_ -and ($_.IsOffline -or $_.IsReadOnly) }) Write-Check 'All disks online and writable' ($badDisks.Count -eq 0) ("disk(s): {0}" -f (($badDisks.Number) -join ',')) $missingVol = @($b.Volumes.DriveLetter | Where-Object { $_ -notin $a.Volumes.DriveLetter }) Write-Check 'All drive letters present' ($missingVol.Count -eq 0) ("missing: {0}" -f ($missingVol -join ',')) # Services and ports $runningB = @($b.AutoServices | Where-Object State -eq 'Running' | ForEach-Object Name) $runningA = @($a.AutoServices | Where-Object State -eq 'Running' | ForEach-Object Name) $stopped = @($runningB | Where-Object { $_ -notin $runningA -and $_ -notmatch '^VM|^vm3|^VGAuth' }) Write-Check 'Automatic services still running' ($stopped.Count -eq 0) ("not running: {0}" -f ($stopped -join ', ')) $ports = @($b.ListeningPorts | Where-Object { $_ -notin $a.ListeningPorts -and $_ -notmatch '/vm' }) Write-Check 'Listening ports preserved' ($ports.Count -eq 0) ("missing: {0}" -f ($ports -join ', ')) # Identity and licensing if ($null -ne $b.SecureChannel) { Write-Check 'Domain secure channel' ($a.SecureChannel -eq $true) ("value: {0}" -f $a.SecureChannel) } $licB = @($b.Activation | Where-Object LicenseStatus -eq 1).Count $licA = @($a.Activation | Where-Object LicenseStatus -eq 1).Count Write-Check 'Windows activation' ($licA -ge $licB) ("licensed products before: {0} / after: {1}" -f $licB, $licA) # Leftovers and new errors are reported, the operator decides. $vmw = @($a.VirtTools | Where-Object { $_.DisplayName -match 'VMware' }) if ($vmw.Count) { Write-Host ("[INFO] VMware components still installed: {0}" -f (($vmw.DisplayName) -join ', ')) -ForegroundColor Yellow } $keysB = @($b.RecentErrors | Where-Object { $_ } | ForEach-Object { '{0}/{1}' -f $_.Source, $_.Id }) $newErr = @($a.RecentErrors | Where-Object { $_ -and ('{0}/{1}' -f $_.Source, $_.Id) -notin $keysB }) foreach ($e in $newErr) { Write-Host ("[INFO] New error source: {0} {1} id {2} (x{3})" -f $e.Log, $e.Source, $e.Id, $e.Count) -ForegroundColor Yellow } if ($failed) { Write-Host "$failed check(s) failed." -ForegroundColor Red; exit 1 } Write-Host 'All checks passed.' -ForegroundColor Green