294 lines
13 KiB
PowerShell
294 lines
13 KiB
PowerShell
# ============================================================
|
|
# Write-MigrationReport.ps1
|
|
# ============================================================
|
|
# Writes a unified CSV report for all processed servers
|
|
# (Windows + Linux) and optionally a rollback JSON file.
|
|
# Also contains Write-RollbackFile (Task 10).
|
|
# ============================================================
|
|
|
|
function Write-MigrationReport {
|
|
<#
|
|
.SYNOPSIS
|
|
Writes a unified CSV migration report for all processed servers.
|
|
.DESCRIPTION
|
|
Accepts an array of result objects from Set-WindowsDns and
|
|
Invoke-LinuxDns. Writes to CSV with -Append so re-runs don't
|
|
overwrite prior results. Prints a summary table to the console.
|
|
.PARAMETER Results
|
|
Array of PSCustomObjects returned by the DNS change functions.
|
|
.PARAMETER OutputPath
|
|
Full path to the CSV file. Created if it does not exist.
|
|
.PARAMETER Append
|
|
If set, appends to an existing CSV. Default: $true
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[PSCustomObject[]]$Results,
|
|
|
|
[Parameter(Mandatory)]
|
|
[string]$OutputPath,
|
|
|
|
[switch]$NoAppend
|
|
)
|
|
|
|
# Ensure output directory exists
|
|
$outputDir = Split-Path $OutputPath -Parent
|
|
if (-not (Test-Path $outputDir)) {
|
|
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
|
|
}
|
|
|
|
# Normalise results to CSV-safe objects (strip internal _RollbackData field)
|
|
$csvRows = foreach ($r in $Results) {
|
|
[PSCustomObject]@{
|
|
Hostname = $r.Hostname
|
|
ResolvedIP = $r.ResolvedIP
|
|
OS = $r.OS
|
|
Distro = $r.Distro
|
|
OldDNS = $r.OldDNS
|
|
NewDNS = $r.NewDNS
|
|
ConnectionMethod = $r.ConnectionMethod
|
|
ChangeStatus = $r.ChangeStatus
|
|
ValidationStatus = $r.ValidationStatus
|
|
ModifiedFiles = $r.ModifiedFiles
|
|
ServiceRestarted = $r.ServiceRestarted
|
|
ErrorMessage = $r.ErrorMessage
|
|
DryRun = $r.DryRun
|
|
Timestamp = $r.Timestamp
|
|
}
|
|
}
|
|
|
|
# Write CSV
|
|
if ($NoAppend -or -not (Test-Path $OutputPath)) {
|
|
$csvRows | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
|
|
}
|
|
else {
|
|
$csvRows | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8 -Append
|
|
}
|
|
|
|
# ── Console summary table ─────────────────────────────────────────────────
|
|
$total = $Results.Count
|
|
$success = @($Results | Where-Object { $_.ChangeStatus -eq "Success" }).Count
|
|
$dryRun = @($Results | Where-Object { $_.ChangeStatus -eq "DryRun" }).Count
|
|
$failed = @($Results | Where-Object { $_.ChangeStatus -eq "Failed" }).Count
|
|
$connFailed = @($Results | Where-Object { $_.ChangeStatus -eq "ConnectionFailed" }).Count
|
|
$skipped = @($Results | Where-Object { $_.ChangeStatus -eq "Skipped" }).Count
|
|
$valSuccess = @($Results | Where-Object { $_.ValidationStatus -eq "Success" }).Count
|
|
$valFailed = @($Results | Where-Object { $_.ValidationStatus -eq "ValidationFailed" }).Count
|
|
|
|
Write-Host ""
|
|
Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Cyan
|
|
Write-Host " DNS MIGRATION REPORT SUMMARY" -ForegroundColor Cyan
|
|
Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Cyan
|
|
Write-Host (" Total servers processed : {0}" -f $total)
|
|
Write-Host (" DNS change success : {0}" -f $success) -ForegroundColor Green
|
|
Write-Host (" DNS change failed : {0}" -f $failed) -ForegroundColor $(if ($failed -gt 0) { "Red" } else { "White" })
|
|
Write-Host (" Connection failed : {0}" -f $connFailed) -ForegroundColor $(if ($connFailed -gt 0) { "Red" } else { "White" })
|
|
Write-Host (" Skipped (unresolvable) : {0}" -f $skipped) -ForegroundColor $(if ($skipped -gt 0) { "Yellow" } else { "White" })
|
|
Write-Host (" Dry run (no changes) : {0}" -f $dryRun) -ForegroundColor $(if ($dryRun -gt 0) { "Cyan" } else { "White" })
|
|
Write-Host "───────────────────────────────────────────────────────────────" -ForegroundColor Cyan
|
|
Write-Host (" Validation success : {0}" -f $valSuccess) -ForegroundColor Green
|
|
Write-Host (" Validation failed : {0}" -f $valFailed) -ForegroundColor $(if ($valFailed -gt 0) { "Yellow" } else { "White" })
|
|
Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Cyan
|
|
Write-Host (" Report written to: {0}" -f $OutputPath) -ForegroundColor Gray
|
|
Write-Host ""
|
|
|
|
# ── Per-server table (truncated to 80 chars per row for readability) ───────
|
|
$colWidth = @{ H=32; S=18; V=18; E=30 }
|
|
$header = "{0,-$($colWidth.H)} {1,-$($colWidth.S)} {2,-$($colWidth.V)} {3}" `
|
|
-f "Hostname", "ChangeStatus", "ValidationStatus", "Error/Note"
|
|
$divider = "-" * ($colWidth.H + $colWidth.S + $colWidth.V + $colWidth.E + 3)
|
|
|
|
Write-Host $divider
|
|
Write-Host $header
|
|
Write-Host $divider
|
|
|
|
foreach ($r in $Results | Sort-Object OS, Hostname) {
|
|
$changeColor = switch ($r.ChangeStatus) {
|
|
"Success" { "Green" }
|
|
"DryRun" { "Cyan" }
|
|
"Failed" { "Red" }
|
|
"ConnectionFailed"{ "Red" }
|
|
"Skipped" { "Yellow" }
|
|
default { "White" }
|
|
}
|
|
$valColor = switch ($r.ValidationStatus) {
|
|
"Success" { "Green" }
|
|
"ValidationFailed"{ "Yellow" }
|
|
"DryRun_OK" { "Cyan" }
|
|
"DryRun_NoResolve"{ "Yellow" }
|
|
default { "Gray" }
|
|
}
|
|
|
|
$hn = $r.Hostname.PadRight($colWidth.H).Substring(0, [Math]::Min($r.Hostname.Length, $colWidth.H)).PadRight($colWidth.H)
|
|
$st = $r.ChangeStatus.PadRight($colWidth.S)
|
|
$vl = $r.ValidationStatus.PadRight($colWidth.V)
|
|
$err = if ($r.ErrorMessage) {
|
|
$r.ErrorMessage.Substring(0, [Math]::Min($r.ErrorMessage.Length, $colWidth.E))
|
|
} else { "" }
|
|
|
|
Write-Host ("{0} " -f $hn) -NoNewline
|
|
Write-Host ("{0} " -f $st) -NoNewline -ForegroundColor $changeColor
|
|
Write-Host ("{0} " -f $vl) -NoNewline -ForegroundColor $valColor
|
|
Write-Host $err
|
|
}
|
|
|
|
Write-Host $divider
|
|
Write-Host ""
|
|
}
|
|
|
|
|
|
function Write-RollbackFile {
|
|
<#
|
|
.SYNOPSIS
|
|
Serializes pre-change DNS state to a timestamped JSON rollback file.
|
|
.DESCRIPTION
|
|
Called BEFORE any DNS changes are made. Captures:
|
|
- Windows: NIC name + old DNS IP list
|
|
- Linux: modified file paths + base64 ContentBefore + service name
|
|
The rollback file is written atomically covering all discovered
|
|
servers, even if the migration script is interrupted mid-run.
|
|
.PARAMETER Results
|
|
Array of result objects. For pre-change capture, these should have
|
|
OldDNS populated (from read-only probe) and _RollbackData for Linux.
|
|
.PARAMETER Results
|
|
Array of result objects from Set-WindowsDns / Invoke-LinuxDns.
|
|
Each entry's _RollbackData property carries the Linux file content.
|
|
For Windows entries, the NICName is extracted from the ModifiedFiles field.
|
|
.PARAMETER OutputDir
|
|
Directory to write the rollback JSON file.
|
|
.OUTPUTS
|
|
Full path to the written rollback JSON file.
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
[PSCustomObject[]]$Results,
|
|
|
|
[string]$OutputDir = ".\logs"
|
|
)
|
|
|
|
if (-not (Test-Path $OutputDir)) {
|
|
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
|
}
|
|
|
|
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
|
$filePath = Join-Path $OutputDir "dns-rollback-$timestamp.json"
|
|
|
|
$entries = [System.Collections.Generic.List[object]]::new()
|
|
|
|
foreach ($r in $Results) {
|
|
if ($r.OS -eq "Windows") {
|
|
# Windows rollback: NIC name + old DNS IPs
|
|
$nicName = if ($r.ModifiedFiles -match "NIC:(.+)") { $Matches[1] } else { "" }
|
|
|
|
$entries.Add([ordered]@{
|
|
Hostname = $r.Hostname
|
|
OS = "Windows"
|
|
Distro = $r.Distro
|
|
ConnectionMethod = $r.ConnectionMethod
|
|
OldDNS = @($r.OldDNS -split ";" | Where-Object { $_ -ne "" })
|
|
NewDNS = @($r.NewDNS -split ";" | Where-Object { $_ -ne "" })
|
|
NICName = $nicName
|
|
ModifiedFiles = @()
|
|
ServiceRestarted = ""
|
|
ResolvConfBefore = ""
|
|
ResolvConfAfter = ""
|
|
Timestamp = $r.Timestamp
|
|
})
|
|
}
|
|
elseif ($r.OS -eq "Linux") {
|
|
# Linux rollback: full file contents from _RollbackData
|
|
$rd = $r._RollbackData
|
|
|
|
if ($null -ne $rd) {
|
|
# Normalise ModifiedFiles to serialisable format
|
|
$mf = @($rd.ModifiedFiles | ForEach-Object {
|
|
[ordered]@{
|
|
Path = $_.Path
|
|
ContentBefore = $_.ContentBefore
|
|
ContentAfter = $_.ContentAfter
|
|
}
|
|
})
|
|
|
|
$entries.Add([ordered]@{
|
|
Hostname = $rd.Hostname
|
|
OS = "Linux"
|
|
Distro = $rd.Distro
|
|
ConnectionMethod = $rd.ConnectionMethod
|
|
OldDNS = @($rd.OldDNS | Where-Object { $_ -ne "" })
|
|
NewDNS = @($rd.NewDNS | Where-Object { $_ -ne "" })
|
|
NICName = $rd.NICName
|
|
ModifiedFiles = $mf
|
|
ServiceRestarted = $rd.ServiceRestarted
|
|
ResolvConfBefore = $rd.ResolvConfBefore
|
|
ResolvConfAfter = $rd.ResolvConfAfter
|
|
Timestamp = $rd.Timestamp
|
|
})
|
|
}
|
|
else {
|
|
# No rollback data — record minimal entry so it appears in rollback file
|
|
$entries.Add([ordered]@{
|
|
Hostname = $r.Hostname
|
|
OS = "Linux"
|
|
Distro = $r.Distro
|
|
ConnectionMethod = "SSH"
|
|
OldDNS = @($r.OldDNS -split ";" | Where-Object { $_ -ne "" })
|
|
NewDNS = @($r.NewDNS -split ";" | Where-Object { $_ -ne "" })
|
|
NICName = ""
|
|
ModifiedFiles = @()
|
|
ServiceRestarted = $r.ServiceRestarted
|
|
ResolvConfBefore = ""
|
|
ResolvConfAfter = ""
|
|
Timestamp = $r.Timestamp
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
$rollbackData = [ordered]@{
|
|
SchemaVersion = "1.0"
|
|
CreatedAt = (Get-Date -Format "yyyy-MM-ddTHH:mm:ssZ")
|
|
TotalServers = $entries.Count
|
|
Servers = $entries.ToArray()
|
|
}
|
|
|
|
$rollbackData | ConvertTo-Json -Depth 10 | Set-Content -Path $filePath -Encoding UTF8
|
|
|
|
Write-Host " Rollback file written: $filePath" -ForegroundColor Green
|
|
return $filePath
|
|
}
|
|
|
|
|
|
function Read-RollbackFile {
|
|
<#
|
|
.SYNOPSIS
|
|
Reads and validates a rollback JSON file. Returns the parsed object.
|
|
.PARAMETER Path
|
|
Path to the dns-rollback-<timestamp>.json file.
|
|
#>
|
|
param(
|
|
[Parameter(Mandatory)]
|
|
[string]$Path
|
|
)
|
|
|
|
if (-not (Test-Path $Path)) {
|
|
throw "Rollback file not found: $Path"
|
|
}
|
|
|
|
try {
|
|
$data = Get-Content $Path -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
}
|
|
catch {
|
|
throw "Failed to parse rollback file '$Path': $($_.Exception.Message)"
|
|
}
|
|
|
|
if ($null -eq $data.Servers) {
|
|
throw "Rollback file has no 'Servers' array: $Path"
|
|
}
|
|
|
|
Write-Host " Rollback file loaded: $Path" -ForegroundColor Cyan
|
|
Write-Host (" Contains {0} server entries (created {1})" -f $data.TotalServers, $data.CreatedAt) -ForegroundColor Gray
|
|
|
|
return $data
|
|
}
|