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