Public/Move-DnsZones.ps1
function Move-DnsZones { <# .SYNOPSIS Migrates DNS zones from one server to another with comprehensive backup. .DESCRIPTION This function performs a complete DNS zone migration including: - Creates backup of DNS zones, settings, and configurations - Exports DNS zones from source server - Imports DNS zones to destination server - Migrates DNS server settings and configurations - Validates migration success - Comprehensive logging of all operations .PARAMETER SourceServer The source DNS server hostname or IP address to migrate from. .PARAMETER DestinationServer The destination DNS server hostname or IP address to migrate to. .PARAMETER BackupPath Path where backups will be stored. Defaults to C:\Backups\DNS_Migration. .PARAMETER ZoneNames Optional array of specific zone names to migrate. If not specified, all zones will be migrated. .PARAMETER SkipValidation Skip post-migration validation checks. .PARAMETER Credential Credential object for accessing remote servers. .PARAMETER WhatIf Shows what would be done without actually performing the migration. .EXAMPLE Move-DnsZones -SourceServer "dns01.contoso.com" -DestinationServer "dns02.contoso.com" Migrates all DNS zones and settings from dns01 to dns02. .EXAMPLE Move-DnsZones -SourceServer "192.168.1.10" -DestinationServer "192.168.1.20" -ZoneNames @("contoso.com", "internal.local") -WhatIf Shows what would be migrated for specific zones without executing. .NOTES Author: Forthencho Module Requires: DNS Server PowerShell module, Administrative privileges Version: 1.0 #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$SourceServer, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$DestinationServer, [Parameter()] [ValidateScript({ $parentPath = Split-Path $_ -Parent if (-not (Test-Path $parentPath)) { try { New-Item -Path $parentPath -ItemType Directory -Force -ErrorAction Stop | Out-Null return $true } catch { throw "Cannot create backup directory: $parentPath" } } return $true })] [string]$BackupPath = "C:\Backups\DNS_Migration", [Parameter()] [string[]]$ZoneNames = @(), [Parameter()] [switch]$SkipValidation, [Parameter()] [System.Management.Automation.PSCredential]$Credential ) begin { # Initialize logging and backup directories $timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss" $migrationBackupPath = Join-Path $BackupPath $timestamp if (-not (Test-Path $migrationBackupPath)) { try { New-Item -Path $migrationBackupPath -ItemType Directory -Force | Out-Null Write-Verbose "Created backup directory: $migrationBackupPath" } catch { throw "Failed to create backup directory '$migrationBackupPath': $($_.Exception.Message)" } } $logFile = Join-Path $migrationBackupPath "DNS_Migration_$timestamp.log" # Helper function for logging function Write-MigrationLog { param( [Parameter(Mandatory)] [string]$Message, [Parameter()] [ValidateSet('INFO', 'WARNING', 'ERROR', 'SUCCESS')] [string]$Level = 'INFO' ) $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $logEntry = "[$timestamp] [$Level] $Message" # Write to console with colors switch ($Level) { 'INFO' { Write-Host $logEntry -ForegroundColor White } 'WARNING' { Write-Warning $logEntry } 'ERROR' { Write-Host $logEntry -ForegroundColor Red } 'SUCCESS' { Write-Host $logEntry -ForegroundColor Green } } # Write to log file try { Add-Content -Path $logFile -Value $logEntry -ErrorAction Stop } catch { Write-Warning "Failed to write to log file: $($_.Exception.Message)" } } # Validate DNS Server module availability if (-not (Get-Module -ListAvailable -Name DnsServer)) { throw "DNS Server PowerShell module is not available. Please install DNS Server role and management tools." } Import-Module DnsServer -ErrorAction Stop Write-MigrationLog "Starting DNS migration from $SourceServer to $DestinationServer" -Level 'INFO' Write-MigrationLog "Backup location: $migrationBackupPath" -Level 'INFO' Write-MigrationLog "WhatIf mode: $WhatIf" -Level 'INFO' } process { try { # Step 1: Backup DNS configuration from source server Write-MigrationLog "=== Step 1: Creating backup of source DNS configuration ===" -Level 'INFO' $sourceBackupPath = Join-Path $migrationBackupPath "Source_$SourceServer" New-Item -Path $sourceBackupPath -ItemType Directory -Force | Out-Null # Get DNS zones from source server Write-MigrationLog "Retrieving DNS zones from source server: $SourceServer" -Level 'INFO' $sessionParams = @{ ComputerName = $SourceServer ErrorAction = 'Stop' } if ($Credential) { $sessionParams.Credential = $Credential } try { $sourceZones = Invoke-Command @sessionParams -ScriptBlock { Import-Module DnsServer -Force Get-DnsServerZone | Where-Object { $_.ZoneType -ne 'Cache' } } Write-MigrationLog "Found $($sourceZones.Count) zones on source server" -Level 'SUCCESS' # Filter zones if specific zones were requested if ($ZoneNames.Count -gt 0) { $sourceZones = $sourceZones | Where-Object { $_.ZoneName -in $ZoneNames } Write-MigrationLog "Filtered to $($sourceZones.Count) specified zones" -Level 'INFO' } # Export zone information $zonesBackupFile = Join-Path $sourceBackupPath "DNS_Zones_Info.json" $sourceZones | ConvertTo-Json -Depth 10 | Out-File -FilePath $zonesBackupFile -Encoding UTF8 Write-MigrationLog "Backed up zone information to: $zonesBackupFile" -Level 'SUCCESS' } catch { Write-MigrationLog "Failed to retrieve zones from source server: $($_.Exception.Message)" -Level 'ERROR' throw } # Step 2: Export DNS zones and records Write-MigrationLog "=== Step 2: Exporting DNS zones and records ===" -Level 'INFO' foreach ($zone in $sourceZones) { try { Write-MigrationLog "Exporting zone: $($zone.ZoneName)" -Level 'INFO' if ($WhatIf) { Write-MigrationLog "WHATIF: Would export zone $($zone.ZoneName)" -Level 'INFO' } else { # Get all records for the zone $zoneRecords = Invoke-Command @sessionParams -ScriptBlock { param($zoneName) Get-DnsServerResourceRecord -ZoneName $zoneName } -ArgumentList $zone.ZoneName # Save records to JSON file $recordsBackupFile = Join-Path $sourceBackupPath "$($zone.ZoneName)_Records.json" $zoneRecords | ConvertTo-Json -Depth 10 | Out-File -FilePath $recordsBackupFile -Encoding UTF8 # Export zone to DNS file format try { $zoneExportResult = Invoke-Command @sessionParams -ScriptBlock { param($zoneName) $tempFile = "$env:TEMP\$zoneName.dns" Export-DnsServerZone -Name $zoneName -FileName (Split-Path $tempFile -Leaf) return "$env:SystemRoot\System32\dns\$zoneName.dns" } -ArgumentList $zone.ZoneName # Copy the exported file to backup location $localZoneFile = Join-Path $sourceBackupPath "$($zone.ZoneName).dns" $remoteZoneFile = "\\$SourceServer\C$\Windows\System32\dns\$($zone.ZoneName).dns" if (Test-Path $remoteZoneFile) { Copy-Item -Path $remoteZoneFile -Destination $localZoneFile -Force -ErrorAction SilentlyContinue } Write-MigrationLog "Exported zone $($zone.ZoneName) successfully" -Level 'SUCCESS' } catch { Write-MigrationLog "Could not export DNS file for $($zone.ZoneName), using JSON backup: $($_.Exception.Message)" -Level 'WARNING' } } } catch { Write-MigrationLog "Failed to export zone $($zone.ZoneName): $($_.Exception.Message)" -Level 'ERROR' } } # Step 3: Backup DNS server settings Write-MigrationLog "=== Step 3: Backing up DNS server settings ===" -Level 'INFO' try { if ($WhatIf) { Write-MigrationLog "WHATIF: Would backup DNS server settings" -Level 'INFO' } else { $serverSettings = Invoke-Command @sessionParams -ScriptBlock { Import-Module DnsServer -Force @{ ServerSettings = Get-DnsServerSetting Forwarders = Get-DnsServerForwarder RootHints = Get-DnsServerRootHint Scavenging = Get-DnsServerScavenging } } $settingsBackupFile = Join-Path $sourceBackupPath "DNS_Server_Settings.json" $serverSettings | ConvertTo-Json -Depth 10 | Out-File -FilePath $settingsBackupFile -Encoding UTF8 Write-MigrationLog "Backed up DNS server settings" -Level 'SUCCESS' } } catch { Write-MigrationLog "Failed to backup server settings: $($_.Exception.Message)" -Level 'WARNING' } # Step 4: Prepare destination server Write-MigrationLog "=== Step 4: Preparing destination server ===" -Level 'INFO' $destSessionParams = @{ ComputerName = $DestinationServer ErrorAction = 'Stop' } if ($Credential) { $destSessionParams.Credential = $Credential } try { # Verify destination server is accessible and has DNS role $destDnsInfo = Invoke-Command @destSessionParams -ScriptBlock { Import-Module DnsServer -Force Get-DnsServer } Write-MigrationLog "Destination server $DestinationServer is accessible and has DNS role installed" -Level 'SUCCESS' # Create backup of destination server (before migration) $destBackupPath = Join-Path $migrationBackupPath "Destination_$DestinationServer" New-Item -Path $destBackupPath -ItemType Directory -Force | Out-Null if (-not $WhatIf) { $destZones = Invoke-Command @destSessionParams -ScriptBlock { Get-DnsServerZone | Where-Object { $_.ZoneType -ne 'Cache' } } $destZones | ConvertTo-Json -Depth 10 | Out-File -FilePath (Join-Path $destBackupPath "Pre_Migration_Zones.json") -Encoding UTF8 Write-MigrationLog "Created pre-migration backup of destination server" -Level 'SUCCESS' } } catch { Write-MigrationLog "Failed to prepare destination server: $($_.Exception.Message)" -Level 'ERROR' throw } # Step 5: Import zones to destination server Write-MigrationLog "=== Step 5: Importing zones to destination server ===" -Level 'INFO' foreach ($zone in $sourceZones) { try { Write-MigrationLog "Processing zone: $($zone.ZoneName) for destination server" -Level 'INFO' if ($WhatIf) { Write-MigrationLog "WHATIF: Would import zone $($zone.ZoneName) to $DestinationServer" -Level 'INFO' continue } # Check if zone already exists on destination $existingZone = Invoke-Command @destSessionParams -ScriptBlock { param($zoneName) Get-DnsServerZone -Name $zoneName -ErrorAction SilentlyContinue } -ArgumentList $zone.ZoneName if ($existingZone) { Write-MigrationLog "Zone $($zone.ZoneName) already exists on destination. Creating backup..." -Level 'WARNING' # Backup existing zone records $existingZoneBackup = Join-Path $destBackupPath "$($zone.ZoneName)_Existing_Records.json" $existingZoneRecords = Invoke-Command @destSessionParams -ScriptBlock { param($zoneName) Get-DnsServerResourceRecord -ZoneName $zoneName } -ArgumentList $zone.ZoneName $existingZoneRecords | ConvertTo-Json -Depth 10 | Out-File -FilePath $existingZoneBackup -Encoding UTF8 Write-MigrationLog "Backed up existing zone $($zone.ZoneName)" -Level 'SUCCESS' } # Import the zone using JSON backup (more reliable than DNS files) $recordsBackupFile = Join-Path $sourceBackupPath "$($zone.ZoneName)_Records.json" if (Test-Path $recordsBackupFile) { $zoneRecords = Get-Content $recordsBackupFile | ConvertFrom-Json # Create or recreate the zone Invoke-Command @destSessionParams -ScriptBlock { param($zoneName, $zoneType, $isDynamicUpdate) # Remove existing zone if it exists if (Get-DnsServerZone -Name $zoneName -ErrorAction SilentlyContinue) { Remove-DnsServerZone -Name $zoneName -Force -Confirm:$false } # Create new primary zone Add-DnsServerPrimaryZone -Name $zoneName -DynamicUpdate $isDynamicUpdate } -ArgumentList $zone.ZoneName, $zone.ZoneType, $zone.DynamicUpdate # Import individual records $importedRecords = 0 foreach ($record in $zoneRecords) { try { # Skip SOA and NS records for the zone root as they're automatically created if ($record.HostName -eq "@" -and ($record.RecordType -eq "SOA" -or $record.RecordType -eq "NS")) { continue } Invoke-Command @destSessionParams -ScriptBlock { param($zoneName, $recordData) try { switch ($recordData.RecordType) { "A" { Add-DnsServerResourceRecordA -ZoneName $zoneName -Name $recordData.HostName -IPv4Address $recordData.RecordData.IPv4Address.IPAddressToString -TimeToLive $recordData.TimeToLive.TotalSeconds } "AAAA" { Add-DnsServerResourceRecordAAAA -ZoneName $zoneName -Name $recordData.HostName -IPv6Address $recordData.RecordData.IPv6Address.IPAddressToString -TimeToLive $recordData.TimeToLive.TotalSeconds } "CNAME" { Add-DnsServerResourceRecordCName -ZoneName $zoneName -Name $recordData.HostName -HostNameAlias $recordData.RecordData.HostNameAlias -TimeToLive $recordData.TimeToLive.TotalSeconds } "MX" { Add-DnsServerResourceRecordMX -ZoneName $zoneName -Name $recordData.HostName -MailExchange $recordData.RecordData.MailExchange -Preference $recordData.RecordData.Preference -TimeToLive $recordData.TimeToLive.TotalSeconds } "TXT" { Add-DnsServerResourceRecordTxt -ZoneName $zoneName -Name $recordData.HostName -DescriptiveText $recordData.RecordData.DescriptiveText -TimeToLive $recordData.TimeToLive.TotalSeconds } "PTR" { Add-DnsServerResourceRecordPtr -ZoneName $zoneName -Name $recordData.HostName -PtrDomainName $recordData.RecordData.PtrDomainName -TimeToLive $recordData.TimeToLive.TotalSeconds } } } catch { Write-Warning "Failed to import record $($recordData.HostName) ($($recordData.RecordType)): $($_.Exception.Message)" } } -ArgumentList $zone.ZoneName, $record $importedRecords++ } catch { Write-MigrationLog "Failed to import record: $($_.Exception.Message)" -Level 'WARNING' } } Write-MigrationLog "Successfully imported zone $($zone.ZoneName) with $importedRecords records" -Level 'SUCCESS' } else { Write-MigrationLog "Records backup file not found for $($zone.ZoneName), skipping import" -Level 'WARNING' } } catch { Write-MigrationLog "Failed to import zone $($zone.ZoneName): $($_.Exception.Message)" -Level 'ERROR' } } # Step 6: Apply server settings Write-MigrationLog "=== Step 6: Applying DNS server settings ===" -Level 'INFO' if (-not $WhatIf) { try { $settingsBackupFile = Join-Path $sourceBackupPath "DNS_Server_Settings.json" if (Test-Path $settingsBackupFile) { $serverSettings = Get-Content $settingsBackupFile | ConvertFrom-Json # Apply forwarders if ($serverSettings.Forwarders) { Invoke-Command @destSessionParams -ScriptBlock { param($forwarders) if ($forwarders.IPAddress -and $forwarders.IPAddress.Count -gt 0) { Set-DnsServerForwarder -IPAddress $forwarders.IPAddress -EnableReordering:$forwarders.EnableReordering } } -ArgumentList $serverSettings.Forwarders Write-MigrationLog "Applied DNS forwarders" -Level 'SUCCESS' } # Apply scavenging settings if ($serverSettings.Scavenging -and $serverSettings.Scavenging.ScavengingState) { Invoke-Command @destSessionParams -ScriptBlock { param($scavenging) Set-DnsServerScavenging -ApplyOnAllZones -ScavengingState:$scavenging.ScavengingState -ScavengingInterval $scavenging.ScavengingInterval } -ArgumentList $serverSettings.Scavenging Write-MigrationLog "Applied scavenging settings" -Level 'SUCCESS' } } } catch { Write-MigrationLog "Failed to apply some server settings: $($_.Exception.Message)" -Level 'WARNING' } } else { Write-MigrationLog "WHATIF: Would apply DNS server settings to destination" -Level 'INFO' } # Step 7: Validation if (-not $SkipValidation -and -not $WhatIf) { Write-MigrationLog "=== Step 7: Validating migration ===" -Level 'INFO' try { $destZonesAfter = Invoke-Command @destSessionParams -ScriptBlock { Get-DnsServerZone | Where-Object { $_.ZoneType -ne 'Cache' } } $validationResults = @{ TotalSourceZones = $sourceZones.Count TotalDestinationZones = $destZonesAfter.Count MigratedZones = @() MissingZones = @() ValidationPassed = $true } foreach ($sourceZone in $sourceZones) { $destZone = $destZonesAfter | Where-Object { $_.ZoneName -eq $sourceZone.ZoneName } if ($destZone) { $validationResults.MigratedZones += $sourceZone.ZoneName } else { $validationResults.MissingZones += $sourceZone.ZoneName $validationResults.ValidationPassed = $false } } # Save validation results $validationFile = Join-Path $migrationBackupPath "Migration_Validation.json" $validationResults | ConvertTo-Json -Depth 10 | Out-File -FilePath $validationFile -Encoding UTF8 if ($validationResults.ValidationPassed) { Write-MigrationLog "Migration validation PASSED - All zones migrated successfully" -Level 'SUCCESS' } else { Write-MigrationLog "Migration validation FAILED - Missing zones: $($validationResults.MissingZones -join ', ')" -Level 'ERROR' } } catch { Write-MigrationLog "Validation failed: $($_.Exception.Message)" -Level 'WARNING' } } else { Write-MigrationLog "Skipping validation (SkipValidation=$SkipValidation, WhatIf=$WhatIf)" -Level 'INFO' } Write-MigrationLog "DNS migration completed" -Level 'SUCCESS' } catch { Write-MigrationLog "DNS migration failed: $($_.Exception.Message)" -Level 'ERROR' throw } } end { Write-MigrationLog "=== Migration Summary ===" -Level 'INFO' Write-MigrationLog "Source Server: $SourceServer" -Level 'INFO' Write-MigrationLog "Destination Server: $DestinationServer" -Level 'INFO' Write-MigrationLog "Backup Location: $migrationBackupPath" -Level 'INFO' Write-MigrationLog "Log File: $logFile" -Level 'INFO' if ($WhatIf) { Write-Host "`nWhatIf mode was enabled - no actual changes were made." -ForegroundColor Cyan } Write-Host "`nMigration backup and logs saved to: $migrationBackupPath" -ForegroundColor Cyan # Return migration results return @{ BackupPath = $migrationBackupPath LogFile = $logFile SourceServer = $SourceServer DestinationServer = $DestinationServer WhatIfMode = $WhatIf.IsPresent } } } # Export the function Export-ModuleMember -Function Move-DnsZones |