Migration von Exchange 2016 zu Exchange SE (Subscription Edition)

Exchange SE - Migration von Exchange 2016
Exchange SE – Migration von Exchange 2016

Heute ein Beitrag zur „legacy“ Migration: Der Weg zu Exchange SE auf Windows Server 2025 ausgehend von Exchange Server 2016 CU 23 auf Windows Server 2016. Im Fokus steht der möglichst automatisierteste Weg per PowerShell, um die Zeit bis Oktober effektiv zu nutzen.

Im Artikel geht es um recht simple Exchange Server Bereitstellungen, die einen einzelnen Exchange Server 2016 beinhalten sowie zu einem einzelnen Exchange Server SE migriert werden. Hier sind keine DAGs, Load Balancer oder Web Application Firewalls im Spiel. Ein recht typisches Szenario halt, welches mir häufig in „kleineren“ Umgebungen unterkommt.

Der „alte Exchange“ ist läuft bereits mit Exchange Server 2019? Hier entlang zum Exchange 2019 In-Place Upgrade: Upgrade auf Exchange Server SE – Jans Cloud

Die Ausgangssituation:
  • Exchange Server 2016 CU23 mit derzeit aktuellstem Mai 25 Hotfix Update
  • Windows Server 2016 aktuellster Patch Stand
  • IIS Crypto „Best Practices“ Einstellungen
Das Ziel:
  • Exchange Server SE RTM
    • Split-DNS (mit vorerst selbst signiertem Zertifikat sofern keine Split-DNS Konfiguration vorliegt)
  • Windows Server 2025 aktuellster Patch Stand
  • „Best Practices“ bzgl. TLS dank IIS Crypto
Der ausführende User ist Mitglied der:
  • Domain Admins (Domänen-Administratoren)
  • Enterprise Admins (Organisations-Admins)
  • Schema Admins (Schema-Admins)
  • Organization Management (Organisationsverwaltung)
  • Lokale Gruppe „Administrators“ (Administratoren) der beiden Exchange Server

Alle Scripte werden auf dem neuen Windows Server 2025 (vorerst ohne installierten Exchange SE) ausgeführt. Gestartet wird mit dem Script „Install-ExchangeSEPreRequirements.ps1„, welches die pre Requirements herunterläd und installiert. Ebenso verbindet sich das Script mit dem Exchange Server 2016 und trifft – sofern benötigt – Vorbereitungen für Split DNS samt neuem, selbst signierten Zertifikat. Ist Split DNS bereits eingerichtet, wird das derzeit genutzte Zertifikat exportiert. Wird ein neues Zertifikat erstellt, wird ebenfalls ein neues Gruppenrichtlinienobjekt („C_Distribute Root certificates“) zur Zertifikatsverteilung erstellt.

Installation der pre Requirements von Exchange SE sowie Vorbereitung Exchange Server 2016
PowerShell
# Install-ExchangeSEPreRequirements.ps1
#
#

$IISCryptoUri = "https://www.nartac.com/Downloads/IISCrypto/IISCryptoCli.exe"
$VC2012Uri = "https://download.microsoft.com/download/1/6/b/16b06f60-3b20-4ff2-b699-5e9b7962f9ae/VSU_4/vcredist_x64.exe"
$VC2013Uri = "https://download.visualstudio.microsoft.com/download/pr/10912041/cee5d6bca2ddbcd039da727bf4acb48a/vcredist_x64.exe"
$UCMAUri = "https://download.microsoft.com/download/2/c/4/2c47a5c1-a1f3-4843-b9fe-84c0032c61ec/UcmaRuntimeSetup.exe"
$URLRWUri = "https://download.microsoft.com/download/1/2/8/128E2E22-C1B9-44A4-BE2A-5859ED1D4592/rewrite_amd64_en-US.msi"

if(-not(Test-Path -Path "C:\_install" -PathType Container)){
        New-Item -Path "C:\" `
            -Name "_install" `
            -ItemType Directory |
                Out-Null
}

if(-not(Test-Path -Path "C:\_install\Logging" -PathType Container)){
        New-Item -Path "C:\_install" `
            -Name "Logging" `
            -ItemType Directory |
                Out-Null
}

Write-Host "Downloading IIS Crypto to C:\_install\IISCryptoCli.exe"
Start-BitsTransfer -Source $IISCryptoUri `
    -Destination "C:\_install\IISCryptoCli.exe"
Unblock-File -Path "C:\_install\IISCryptoCli.exe"

Write-Host "Downloading VC++ Redistributable 2012 to C:\_install\VC2012x64.exe"
Start-BitsTransfer -Source $VC2012Uri `
    -Destination "C:\_install\VC2012x64.exe"
Unblock-File -Path "C:\_install\VC2012x64.exe"

Write-Host "Downloading VC++ Redistributable 2013 to C:\_install\VC2013x64.exe"
Start-BitsTransfer -Source $VC2013Uri `
    -Destination "C:\_install\VC2013x64.exe"
Unblock-File -Path "C:\_install\VC2013x64.exe"

Write-Host "Downloading Unified Communications Managed API 4.0 Runtime to C:\_install\UcmaRuntimeSetup.exe"
Start-BitsTransfer -Source $UCMAUri `
    -Destination "C:\_install\UcmaRuntimeSetup.exe"
Unblock-File -Path "C:\_install\UcmaRuntimeSetup.exe"

Write-Host "Downloading IIS URL Rewrite to C:\_install\rewrite_amd64_en-US.msi"
Start-BitsTransfer -Source $URLRWUri `
    -Destination "C:\_install\rewrite_amd64_en-US.msi"
Unblock-File -Path "C:\_install\rewrite_amd64_en-US.msi"

Write-Host "Installing Windows roles for Exchange Server SE"
Install-WindowsFeature Server-Media-Foundation, NET-Framework-45-Core, NET-Framework-45-ASPNET, NET-WCF-HTTP-Activation45, `
    NET-WCF-Pipe-Activation45, NET-WCF-TCP-Activation45, NET-WCF-TCP-PortSharing45, RPC-over-HTTP-proxy, RSAT-Clustering, `
    RSAT-Clustering-CmdInterface, RSAT-Clustering-Mgmt, RSAT-Clustering-PowerShell, WAS-Process-Model, Web-Asp-Net45, `
    Web-Basic-Auth, Web-Client-Auth, Web-Digest-Auth, Web-Dir-Browsing, Web-Dyn-Compression, Web-Http-Errors, `
    Web-Http-Logging, Web-Http-Redirect, Web-Http-Tracing, Web-ISAPI-Ext, Web-ISAPI-Filter, Web-Metabase, Web-Mgmt-Console, `
    Web-Mgmt-Service, Web-Net-Ext45, Web-Request-Monitor, Web-Server, Web-Stat-Compression, Web-Static-Content, `
    Web-Windows-Auth, Web-WMI, Windows-Identity-Foundation, RSAT-ADDS, GPMC, RSAT-DNS-Server |
        Out-Null

Write-Host "Running IIS Crypto"
$objProc = Start-Process -FilePath "C:\_install\IISCryptoCli.exe" `
    -ArgumentList "/template best /backup C:\_install\IISCryptoBackup.reg"`
    -NoNewWindow `
    -PassThru `
    -Wait

if($objProc.ExitCode -ne 0){
    Write-Host "Please check IIS Crypto manualy" `
        -ForegroundColor Yellow
    pause
}

Write-Host "Installing Visual C++ Redistributable 2012"
$objProc = Start-Process -FilePath "C:\_install\VC2012x64.exe" `
     -ArgumentList "/install /quiet /log C:\_install\Logging\vcredist2012x64.txt /norestart" `
     -NoNewWindow `
     -PassThru `
     -Wait

if($objProc.ExitCode -notmatch "^(0|1638|3010)$"){
    Write-Host "Please check VC redist logs `"C:\_install\Logging\vcredist2012x64.txt`"" `
        -ForegroundColor Red
    pause
} else{
    Remove-Item -Path "C:\_install\VC2012x64.exe"
}

Write-Host "Installing Visual C++ Redistributable 2013:"
$objProc = Start-Process -FilePath "C:\_install\VC2013x64.exe" `
    -ArgumentList "/install /quiet /log C:\_install\Logging\vcredist2013x64.txt /norestart" `
    -NoNewWindow `
    -PassThru `
    -Wait

if($objProc.ExitCode -notmatch "^(0|1638|3010)$"){
    Write-Host "Please check VC redist logs `"C:\_install\Logging\vcredist2013x64.txt`"" `
        -ForegroundColor Red
    pause
} else{
    Remove-Item -Path "C:\_install\VC2013x64.exe"
}

Write-Host "Installing Unified Communications Managed API 4.0 Runtime"
$objProc = Start-Process -FilePath "C:\_install\UcmaRuntimeSetup.exe" `
    -ArgumentList "/q /log C:\_install\Logging\UcmaRuntime.txt /norestart" `
    -NoNewWindow `
    -PassThru `
    -Wait

if($objProc.ExitCode -notmatch "^0$"){
    Write-Host "Please check Ucma runtime logs `"C:\_install\Logging\UcmaRuntime.txt`"" `
        -ForegroundColor Red
    pause
} else{
    Remove-Item -Path "C:\_install\UcmaRuntimeSetup.exe"
}

Write-Host "Installing IIS URL Rewrite module"
$objProc = Start-Process -FilePath "C:\Windows\System32\msiexec.exe" `
 -ArgumentList "/i `"C:\_install\rewrite_amd64_en-US.msi`"/quiet /l*v C:\_install\Logging\iisrewrite.txt /norestart" `
 -NoNewWindow `
 -PassThru `
 -Wait

if($objProc.ExitCode -notmatch "^(0|1638|3010)$"){
    Write-Host "Please check IIS rewrite logs `"C:\_install\Logging\iisrewrite.txt`"" `
        -ForegroundColor Red
    pause
} else{
    Remove-Item -Path "C:\_install\rewrite_amd64_en-US.msi"
}

Write-Host "Configuring TLS settings .Net v2.0.50727"
New-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\v2.0.50727" `
    -Name "SchUseStrongCrypto" `
    -PropertyType Dword `
    -Value 1 |
        Out-Null
New-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v2.0.50727" `
    -Name "SchUseStrongCrypto" `
    -PropertyType Dword `
    -Value 1 |
        Out-Null
New-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\v2.0.50727" `
    -Name "SystemDefaultTlsVersions" `
    -PropertyType Dword `
    -Value 1 |
        Out-Null
New-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v2.0.50727" `
    -Name "SystemDefaultTlsVersions" `
    -PropertyType Dword `
    -Value 1 |
        Out-Null

Write-Host "Configuring TLS settings .Net v4.0.30319"
New-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\v4.0.30319" `
    -Name "SchUseStrongCrypto" `
    -PropertyType Dword `
    -Value 1 |
        Out-Null
New-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v4.0.30319" `
    -Name "SchUseStrongCrypto" `
    -PropertyType Dword `
    -Value 1 |
        Out-Null
New-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\v4.0.30319" `
    -Name "SystemDefaultTlsVersions" `
    -PropertyType Dword `
    -Value 1 |
        Out-Null
New-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\v4.0.30319" `
    -Name "SystemDefaultTlsVersions" `
    -PropertyType Dword `
    -Value 1 |
        Out-Null

$objADDomain = Get-ADDomain -Current LocalComputer
$NBDomain = $objADDomain.NetBIOSName
$DNSDomain = $objADDomain.DNSRoot
$DNSServer = $objADDomain.PDCEmulator

$CertGPO = "C_Distribute Root certificates"
$newSANCert = @()

$ExchSRVSE = Get-ADComputer -Identity $env:COMPUTERNAME `
    -Properties DNSHostName
$ExchSRVSEIP = Resolve-DnsName -Name $ExchSRVSE.DNSHostName `
    -Type A |
        Select-Object IPAddress -ExpandProperty IPAddress

$oldExchSRV = Get-ADGroupMember -Identity "Exchange Servers" |
    Where-Object { $_.objectClass -eq "computer" -and $_.Name -ne $env:COMPUTERNAME }
$oldExchSRV = Get-ADComputer -Identity $oldExchSRV.distinguishedName `
    -Properties DNSHostName
$oldExchSRVIP = Resolve-DnsName -Name $oldExchSRV.DNSHostName `
    -Type A |
        Select-Object IPAddress -ExpandProperty IPAddress

$oldExchConUri = -join("http://", `
        $oldExchSRV.DNSHostName , `
        "/PowerShell" `
    )

Write-Host "Connecting to Exchange PowerShell Session `"$oldExchConUri`""
$ExchSession = New-PSSession -ConfigurationName Microsoft.Exchange `
    -ConnectionUri $oldExchConUri
Import-PSSession -Session $ExchSession -DisableNameChecking `
    -AllowClobber |
        Out-Null
    
Write-Host "Getting accepted domains"
$SMTPDoms = Get-AcceptedDomain
$defaultSMTPDom = $SMTPDoms |
    Where-Object { $_.Default -eq $true }

do{
    $SDconf = Read-Host -Prompt "Is Split DNS already configured? (y/n)"
}until($SDconf -match "^(y|n)$")

if($SDconf -eq "n"){
    Write-Host "Starting initial Split DNS configuration"
    do{
        $SDHost = Read-Host "What host name should be used (e.g. outlook, mail or owa)"
    }until(-not [string]::IsNullOrEmpty($SDHost) -and $SDHost -match "^[A-Za-z0-9]{1,15}$")
    do{
        $i = 0
        foreach($s in $SMTPDoms){
            Write-Host "[$($i+1)]: $($s.DomainName)"
            $i++
        }
        do{
            $j = Read-Host "What domain should be used (1 - $i)"
        }until($j -match "^\d{1,2}$")
    }until($j -in 1..$i)
    $SDDom = $SMTPDoms[$j-1].DomainName

    $ExchDNS = -join(
        $SDHost,
        ".",
        $SDDom
    )
    Write-Host "Creating helper file `"C:\_install\ExchDNS.txt`" with content `"$ExchDNS`""
    Set-Content -Path "C:\_install\ExchDNS.txt" `
        -Value $ExchDNS

    $newSANCert += $ExchDNS

    foreach($s in $SMTPDoms){
        $newSANCert += -join(
            "autodiscover.",
            $s.DomainName
        )
    }

    Write-Host "Create new Exchange certificate with subject `"$ExchDNS`""
    $newSANCert += $ExchSRVSE.DNSHostName, $oldExchSRV.DNSHostName, $ExchSRVSE.Name, $oldExchSRV.Name
    $newExchCert = New-ExchangeCertificate -KeySize 2048 `
        -PrivateKeyExportable:$true `
        -FriendlyName "Exchange ($ExchDNS)" `
        -SubjectName "CN=$ExchDNS, O=$DNSDomain, C=DE" `
        -DomainName $newSANCert `
        -Confirm:$false `
        -Force
    
    Write-Host "Exporting new Exchange certificate to `"C:\_install\ExchCert_$ExchDNS.cer`""
    Export-Certificate -Cert $newExchCert `
        -Type CERT `
        -FilePath "C:\_install\ExchCert_$ExchDNS.cer" `
        -Confirm:$false `
        -Force
    
    if(-not(Test-Path -Path "\\$($oldExchSRV.DNSHostName)\c$\_install" -PathType Container)){
        New-Item -Path "\\$($oldExchSRV.DNSHostName)\c$" `
            -Name "_install" `
            -ItemType Directory |
                Out-Null
    }
    Copy-Item -Path "C:\_install\ExchCert_$ExchDNS.cer" `
        -Destination "\\$($oldExchSRV.DNSHostName)\c$\_install\ExchCert.cer" `
        -Confirm:$false `
        -Force
            
    Write-Host "Exporting new Exchange certificate PFX to `"C:\_install\ExchCert_$ExchDNS.pfx`""
    $PFXPass = Read-Host -Prompt "Enter PFX export password" `
        -AsSecureString
    $PFXExchCert = Export-ExchangeCertificate -Thumbprint $newExchCert.Thumbprint `
        -BinaryEncoded `
        -Password $PFXPass `
        -Server $oldExchSRV.Name

    [System.IO.File]::WriteAllBytes("C:\_install\ExchCert_$ExchDNS.pfx", $PFXExchCert.FileData)
    
    Write-Host "Importing certificate in $($ExchSRVSE.Name) trusted roots"
    Import-Certificate -FilePath "C:\_install\ExchCert_$ExchDNS.cer" `
        -CertStoreLocation "Cert:\LocalMachine\Root" `
        -Confirm:$false |
            Out-Null
    
    Write-Host "Importing certificate in $($oldExchSRV.Name) trusted roots"
    Invoke-Command -ComputerName $oldExchSRV.DNSHostName `
        -ScriptBlock {
            Import-Certificate -FilePath "C:\_install\ExchCert.cer" `
                -CertStoreLocation "Cert:\LocalMachine\Root" `
                -Confirm:$false |
                    Out-Null
            Remove-Item -Path "C:\_install\ExchCert.cer" `
                -Confirm:$false `
                -Force
        }

    $objCertGPO = Get-GPO -All |
        Where-Object { $_.DisplayName -eq $CertGPO }
    if($null -eq $objCertGPO){
        Write-Host "Creating new GPO `"$CertGPO`" for self signed certificate distribtion"
        $objCertGPO = New-GPO -Name $CertGPO
        $objCertGPO.GpoStatus = "UserSettingsDisabled"
        Write-Host "Linking GPO `"$CertGPO`" to `"$($objADDomain.DistinguishedName)`""
        New-GPLink -Guid $objCertGPO.Id `
            -Target $objADDomain.DistinguishedName `
            -LinkEnabled Yes |
                Out-Null
    }

    $certRegPath = "HKLM:\SOFTWARE\Microsoft\SystemCertificates\Root\Certificates\{0}" `
        -f $newExchCert.Thumbprint
    $certBlob = Get-ItemProperty -Path $certRegPath `
        -Name "Blob" |
            Select-Object Blob `
                -Expand Blob

    Write-Host "Adding new certificate to GPO `"$CertGPO`""
    $certPolRegPath = "HKLM\SOFTWARE\Policies\Microsoft\SystemCertificates\Root\Certificates\{0}" `
        -f $newExchCert.Thumbprint
    Set-GPRegistryValue -Name $CertGPO `
        -Key $certPolRegPath `
        -ValueName "Blob" `
        -Type Binary `
        -Value $certBlob |
            Out-Null
} else{
    Write-Host "Split DNS is present. Export current certificate"
    Get-ExchangeCertificate -Server $oldExchSRV.DNSHostName
    Write-Host "`nCopy & paste thumbprint of current client access certificate"
    $curCert = Read-Host -Prompt "Thumbprint"

    $curCert = Get-ExchangeCertificate -Thumbprint $curCert `
        -Server $oldExchSRV.Name
    Export-Certificate -Cert $curCert `
        -Type CERT `
        -FilePath "C:\_install\ExchCert_$ExchDNS.cer" `
        -Confirm:$false `
        -Force
        
    Write-Host "Exporting current Exchange certificate PFX to `"C:\_install\ExchCert_$ExchDNS.pfx`""
    $PFXPass = Read-Host -Prompt "Enter PFX export password" `
        -AsSecureString
    $PFXExchCert = Export-ExchangeCertificate -Thumbprint $curCert.Thumbprint `
        -BinaryEncoded `
        -Password $PFXPass `
        -Server $oldExchSRV.Name

    [System.IO.File]::WriteAllBytes("C:\_install\ExchCert_$ExchDNS.pfx", $PFXExchCert.FileData)

    Set-Content -Path "C:\_install\SplitDNSCheck.txt" `
        -Value $curCert.Thumbprint
}

Write-Host "Closing session to `"$oldExchConUri`""
Remove-PSSession -Session $ExchSession

Write-Host "Server will reboot after pressing <Enter>"
pause
Restart-Computer

Jetzt ist es Zeit für mindestens eine Kaffee ☕ oder Bier 🍻 Pause. Idealerweise warten wir bis zum nächsten Tag, damit alle Clients dem neu erstellten und selbst signierten Zertifikat für den neuen Exchange Server Subscription Edition vertrauen.

Neuinstallation Exchange SE auf dem neuen Windows Server 2025

Im nächsten Schritt installieren wir den Exchange Server SE per „Install-ExchangeSE.ps1“ auf einen frisch installierten sowie durchgepatchten Windows Server 2025 und nehmen die Grundkonfiguration vor. Ebenfalls werden die notwendigen DNS Zonen / Records für Split DNS eingerichtet. Sofern Split DNS bereits konfiguriert ist, werden nur die vorhandenen DNS Records auf den neuen Exchange geschwenkt.

Installation und Grundkonfiguration Exchange SE
PowerShell
# Install-ExchangeSE.ps1
#
#

$ExchSEUri = "https://download.microsoft.com/download/ad9b1e53-f28e-4a82-987d-6c88803795b2/ExchangeServerSE-x64.iso"
 
if(-not(Test-Path -Path "C:\_install\ExchSEISO" -PathType Container)){
    New-Item -Path "C:\_install" `
        -Name "ExchSEISO" `
        -ItemType Directory `
        -Confirm:$false `
        -Force |
            Out-Null
}
 
Start-BitsTransfer -Source $ExchSEUri `
    -Destination "C:\_install\ExchSEISO\ExchangeServerSE-x64.iso"
Unblock-File -Path "C:\_install\ExchSEISO\ExchangeServerSE-x64.iso"

Write-Host "Mounting DVD Image `"C:\_install\ExchSEISO\ExchangeServerSE-x64.iso`""
$mdi = Mount-DiskImage -ImagePath "C:\_install\ExchSEISO\ExchangeServerSE-x64.iso" `
    -StorageType ISO `
    -Access ReadOnly `
    -PassThru
 
$DVD = $mdi |
    Get-Volume

$ExchSetup = -join(
    $DVD.DriveLetter,
    ":\Setup.exe"
)

$ExchPrepAD = "/IAcceptExchangeServerLicenseTerms_DiagnosticDataOFF /PrepareAD"
$ExchInstall = "/IAcceptExchangeServerLicenseTerms_DiagnosticDataOFF /Mode:Install /Roles:Mailbox "
$ExchInstall += "/LogFolderPath:`"C:\EX-Logs\EXSE-DB01`" /DbFilePath:`"C:\EX-DBs\EXSE-DB01\EXSE-DB01.edb`" "
$ExchInstall += "/MdbName:`"EXSE-DB01`""

Write-Host "Preparing Active Directory `"$ExchSetup $ExchPrepAD`""
$procPrepAD = Start-Process -FilePath $ExchSetup `
    -ArgumentList $ExchPrepAD `
    -NoNewWindow `
    -PassThru `
    -Wait

if($procPrepAD.ExitCode -ne 0){
    Write-Host "Check setup logs in `"C:\ExchangeSetupLogs`" before continuing by pressing <Enter>" `
        -ForegroundColor Yellow
    pause
}

Rename-Item -Path "C:\ExchangeSetupLogs" `
    -NewName "ExchangeSetupLogs-PrepareAD"
Move-Item -Path "C:\ExchangeSetupLogs-PrepareAD" `
    -Destination "C:\_install\Logging\ExchangeSetupLogs-PrepareAD"

Write-Host "Installaing Exchange Server SE RTM"
$procExchSetup = Start-Process -FilePath $ExchSetup `
    -ArgumentList $ExchInstall `
    -NoNewWindow `
    -PassThru `
    -Wait

if($procExchSetup.ExitCode -ne 0){
    Write-Host "Check setup logs in `"C:\ExchangeSetupLogs`" before continuing by pressing <Enter>" `
        -ForegroundColor Yellow
    pause
}

Rename-Item -Path "C:\ExchangeSetupLogs" `
    -NewName "ExchangeSetupLogs-Installed"
Move-Item -Path "C:\ExchangeSetupLogs-Installed" `
    -Destination "C:\_install\Logging\ExchangeSetupLogs-Installed"

$objADDomain = Get-ADDomain -Current LocalComputer
$NBDomain = $objADDomain.NetBIOSName
$DNSDomain = $objADDomain.DNSRoot
$DNSServer = $objADDomain.PDCEmulator

$ExchSRVSE = Get-ADComputer -Identity $env:COMPUTERNAME `
    -Properties DNSHostName
$ExchSRVSEIP = Resolve-DnsName -Name $ExchSRVSE.DNSHostName `
    -Type A |
        Select-Object IPAddress -ExpandProperty IPAddress

$oldExchSRV = Get-ADGroupMember -Identity "Exchange Servers" |
    Where-Object { $_.objectClass -eq "computer" -and $_.Name -ne $env:COMPUTERNAME }
$oldExchSRV = Get-ADComputer -Identity $oldExchSRV.distinguishedName `
    -Properties DNSHostName
$oldExchSRVIP = Resolve-DnsName -Name $oldExchSRV.DNSHostName `
    -Type A |
        Select-Object IPAddress -ExpandProperty IPAddress

$ExchSEConUri = -join("http://", `
        $ExchSRVSE.DNSHostName , `
        "/PowerShell" `
    )

Write-Host "Connecting to Exchange PowerShell Session `"$ExchSEConUri`""
$ExchSession = New-PSSession -ConfigurationName Microsoft.Exchange `
    -ConnectionUri $ExchSEConUri
Import-PSSession -Session $ExchSession -DisableNameChecking `
    -AllowClobber |
        Out-Null

$PFXFile = Get-ChildItem -Path "C:\_install" `
    -Filter *.pfx |
        Sort-Object LastWriteTime -Descending |
            Select-Object -First 1

Write-Host "Import Exchange certificate"
$PFXPass = Read-Host -Prompt "Enter PFX import password" `
        -AsSecureString
$ExchCert = Import-ExchangeCertificate -Server $ExchSRVSE.DNSHostName `
    -FileData ([System.IO.File]::ReadAllBytes("C:\_install\$($PFXFile.Name)")) `
    -Password $PFXPass `
    -PrivateKeyExportable:$true

if(Test-Path -Path "C:\_install\SplitDNSCheck.txt" -PathType Leaf){
    $tmpUrl = Get-OwaVirtualDirectory -Server $oldExchSRV.DNSHostName |
        Select-Object InternalUrl
    Write-Host "Set Owa vDir Url to `"$($tmpUrl.InternalUrl)`""
    Set-OwaVirtualDirectory -Identity "$($ExchSRVSE.Name)\owa (Default Web Site)" `
        -InternalUrl $tmpUrl.InternalUrl `
        -ExternalUrl $tmpUrl.InternalUrl `
        -WarningAction SilentlyContinue

    $tmpUrl = Get-EcpVirtualDirectory -Server $oldExchSRV.DNSHostName |
        Select-Object InternalUrl
    Write-Host "Set Ecp vDir Url to `"$($tmpUrl.InternalUrl)`""
    Set-EcpVirtualDirectory -Identity "$($ExchSRVSE.Name)\ecp (Default Web Site)" `
        -InternalUrl $tmpUrl.InternalUrl `
        -ExternalUrl $tmpUrl.InternalUrl `
        -WarningAction SilentlyContinue

    $tmpUrl = Get-OabVirtualDirectory -Server $oldExchSRV.DNSHostName |
        Select-Object InternalUrl
    Write-Host "Set Oab vDir Url to `"$($tmpUrl.InternalUrl)`""
    Set-OabVirtualDirectory -Identity "$($ExchSRVSE.Name)\OAB (Default Web Site)" `
        -InternalUrl $tmpUrl.InternalUrl `
        -ExternalUrl $tmpUrl.InternalUrl

    $tmpUrl = Get-ActiveSyncVirtualDirectory -Server $oldExchSRV.DNSHostName |
        Select-Object InternalUrl
    Write-Host "Set Eas vDir Url to `"$($tmpUrl.InternalUrl)`""
    Set-ActiveSyncVirtualDirectory -Identity "$($ExchSRVSE.Name)\Microsoft-Server-ActiveSync (Default Web Site)" `
        -InternalUrl $tmpUrl.InternalUrl `
        -ExternalUrl $tmpUrl.InternalUrl

    $tmpUrl = Get-WebServicesVirtualDirectory -Server $oldExchSRV.DNSHostName |
        Select-Object InternalUrl
    Write-Host "Set Ews vDir Url to `"$($tmpUrl.InternalUrl)`""
    Set-WebServicesVirtualDirectory -Identity "$($ExchSRVSE.Name)\EWS (Default Web Site)" `
        -InternalUrl $tmpUrl.InternalUrl `
        -ExternalUrl $tmpUrl.InternalUrl

    $tmpUrl = Get-MapiVirtualDirectory -Server $oldExchSRV.DNSHostName |
        Select-Object InternalUrl
    Write-Host "Set Mapi vDir Url to `"$($tmpUrl.InternalUrl)`""
    Set-MapiVirtualDirectory -Identity "$($ExchSRVSE.Name)\mapi (Default Web Site)" `
        -InternalUrl $tmpUrl.InternalUrl `
        -ExternalUrl $tmpUrl.InternalUrl

    $tmpUrl = Get-ClientAccessService -Identity $oldExchSRV.DNSHostName |
        Select-Object AutodiscoverServiceInternalUri
    Write-Host "Set Autodiscover Service Internal Uri to `"$($tmpUrl.AutodiscoverServiceInternalUri)`""
    Set-ClientAccessService -Identity $ExchSRVSE.Name `
        -AutoDiscoverServiceInternalUri $tmpUrl.AutoDiscoverServiceInternalUri

    $tmpUrl = Get-OutlookAnywhere -Server $oldExchSRV.Name |
        Select-Object InternalHostname
    Write-Host "Set Outlook Anywhere host names to `"$($tmpUrl.InternalHostname)`""
    Set-OutlookAnywhere -Identity "$($ExchSRVSE.Name)\Rpc (Default Web Site)" `
        -InternalHostname $tmpUrl.InternalHostname `
        -InternalClientsRequireSsl:$true `
        -ExternalHostname $tmpUrl.InternalHostname `
        -ExternalClientsRequireSsl:$true `
        -ExternalClientAuthenticationMethod "Negotiate" `
        -WarningAction SilentlyContinue

} else{
    $ExchDNS = Get-Content -Path "C:\_install\ExchDNS.txt"

    Write-Host "Enabling Exchange certificate on $($oldExchSRV.Name)"
    Enable-ExchangeCertificate -Server $oldExchSRV.DNSHostName `
        -Thumbprint $ExchCert.Thumbprint `
        -Services IIS, IMAP, POP, SMTP `
        -WarningAction SilentlyContinue `
        -Confirm:$false `
        -Force
    
    $tmpUrl = "https://{0}/owa" -f $ExchDNS
    Write-Host "Set Owa vDir Url to `"$($tmpUrl)`""
    Set-OwaVirtualDirectory -Identity "$($ExchSRVSE.Name)\owa (Default Web Site)" `
        -InternalUrl $tmpUrl `
        -ExternalUrl $tmpUrl `
        -WarningAction SilentlyContinue

    $tmpUrl = "https://{0}/ecp" -f $ExchDNS
    Write-Host "Set Ecp vDir Url to `"$($tmpUrl)`""
    Set-EcpVirtualDirectory -Identity "$($ExchSRVSE.Name)\ecp (Default Web Site)" `
        -InternalUrl $tmpUrl `
        -ExternalUrl $tmpUrl `
        -WarningAction SilentlyContinue

    $tmpUrl = "https://{0}/OAB" -f $ExchDNS
    Write-Host "Set Oab vDir Url to `"$($tmpUrl)`""
    Set-OabVirtualDirectory -Identity "$($ExchSRVSE.Name)\OAB (Default Web Site)" `
        -InternalUrl $tmpUrl `
        -ExternalUrl $tmpUrl

    $tmpUrl = "https://{0}/Microsoft-Server-ActiveSync" -f $ExchDNS
    Write-Host "Set Eas vDir Url to `"$($tmpUrl)`""
    Set-ActiveSyncVirtualDirectory -Identity "$($ExchSRVSE.Name)\Microsoft-Server-ActiveSync (Default Web Site)" `
        -InternalUrl $tmpUrl `
        -ExternalUrl $tmpUrl

    $tmpUrl = "https://{0}/EWS/Exchange.asmx" -f $ExchDNS
    Write-Host "Set Ews vDir Url to `"$($tmpUrl)`""
    Set-WebServicesVirtualDirectory -Identity "$($ExchSRVSE.Name)\EWS (Default Web Site)" `
        -InternalUrl $tmpUrl `
        -ExternalUrl $tmpUrl

    $tmpUrl = "https://{0}/mapi" -f $ExchDNS
    Write-Host "Set Mapi vDir Url to `"$($tmpUrl)`""
    Set-MapiVirtualDirectory -Identity "$($ExchSRVSE.Name)\mapi (Default Web Site)" `
        -InternalUrl $tmpUrl `
        -ExternalUrl $tmpUrl

    $tmpUrl = "https://{0}/autodiscover/autodiscover.xml" -f $ExchDNS
    Write-Host "Set Autodiscover Service Internal Uri to `"$($tmpUrl)`""
    Set-ClientAccessService -Identity $ExchSRVSE.Name `
        -AutoDiscoverServiceInternalUri $tmpUrl

    $tmpUrl = $ExchDNS
    Write-Host "Set Outlook Anywhere host names to `"$($tmpUrl)`""
    Set-OutlookAnywhere -Identity "$($ExchSRVSE.Name)\Rpc (Default Web Site)" `
        -InternalHostname $tmpUrl `
        -InternalClientsRequireSsl:$true `
        -ExternalHostname $tmpUrl `
        -ExternalClientsRequireSsl:$true `
        -ExternalClientAuthenticationMethod "Negotiate" `
        -WarningAction SilentlyContinue
}

Write-Host "Enabling Exchange certificate on $($ExchSRVSE.Name)"
Enable-ExchangeCertificate -Server $ExchSRVSE.DNSHostName `
    -Thumbprint $ExchCert.Thumbprint `
    -Services IIS, IMAP, POP, SMTP `
    -WarningAction SilentlyContinue `
    -Confirm:$false `
    -Force

$certDomains = $ExchCert.DnsNameList.Unicode -match "^.*\..*\..*$"
$certDomains = $certDomains |
    ForEach-Object{
        $tmpDom = $_ -replace "^$($oldExchSRV.DNSHostName)|$($ExchSRVSE.DNSHostName)$"
        if(-not [string]::IsNullOrEmpty($tmpDom)){
            $tmpDom
        }
    }

foreach($d in $certDomains){
    $tmpDom = ($d -replace $d.Split(".")[0]).Substring(1)
    $tmpDomZone = Get-DnsServerZone -ComputerName $DNSServer |
        Where-Object { $_.ZoneName -eq $tmpDom }
    if($null -eq $tmpDomZone){
        $tmpZone = Get-DnsServerZone -ComputerName $DNSServer |
            Where-Object { $_.ZoneName -eq $d }
        if($null -eq $tmpZone){
            Write-Host "Adding DNS zone `"$d`" on server `"$DNSServer`""
            Add-DnsServerPrimaryZone -ComputerName $DNSServer `
                -Name $d `
                -DynamicUpdate None `
                -ReplicationScope Forest
            Write-Host "Adding A-Record `".`" with IP `"$ExchSRVSEIP`" in zone `"$d`" on server `"$DNSServer`""
            Add-DnsServerResourceRecordA -ComputerName $DNSServer `
                -ZoneName $d `
                -Name "." `
                -IPv4Address $ExchSRVSEIP      
        } else{
            $tmpRecord = Get-DnsServerResourceRecord -ComputerName $DNSServer `
                -ZoneName $tmpZone.ZoneName `
                -RRType A |
                    Where-Object { $_.Hostname -eq "@" }
            if($null -eq $tmpRecord){
                Write-Host "Adding A-Record `".`" with IP `"$ExchSRVSEIP`" in zone `"$d`" on server `"$DNSServer`""
                Add-DnsServerResourceRecordA -ComputerName $DNSServer `
                    -ZoneName $tmpZone.ZoneName `
                    -Name "." `
                    -IPv4Address $ExchSRVSEIP
            } else{
                Write-Host "Changing A-Record `".`" to IP `"$ExchSRVSEIP`" in zone `"$($tmpZone.ZoneName)`" on server `"$DNSServer`""
                $newRecord = $tmpRecord.Clone()
                $newRecord.RecordData.IPv4Address = $ExchSRVSEIP
                Set-DnsServerResourceRecord -ComputerName $DNSServer `
                    -ZoneName $tmpZone.ZoneName `
                    -OldInputObject $tmpRecord `
                    -NewInputObject $newRecord
            }
        }
    } else{
        $tmpRecord = Get-DnsServerResourceRecord -ComputerName $DNSServer `
            -ZoneName $tmpDomZone.ZoneName `
            -RRType A |
                Where-Object { $_.Hostname -eq $d.Split(".")[0] }
        if($null -eq $tmpRecord){
            Write-Host "Adding A-Record `"$($d.Split(".")[0])`" with IP `"$ExchSRVSEIP`" in zone `"$($tmpDomZone.ZoneName)`" on server `"$DNSServer`""
            Add-DnsServerResourceRecordA -ComputerName $DNSServer `
                -ZoneName $tmpDomZone.ZoneName `
                -Name $d.Split(".")[0] `
                -IPv4Address $ExchSRVSEIP
        } else{
            $newRecord = $tmpRecord.Clone()
            $newRecord.RecordData.IPv4Address = $ExchSRVSEIP
            
            Write-Host "Changing A-Record `"$($newRecord.HostName)`" to IP `"$ExchSRVSEIP`" in zone `"$($tmpDomZone.ZoneName)`" on server `"$DNSServer`""
            Set-DnsServerResourceRecord -ComputerName $DNSServer `
                -ZoneName $tmpDomZone.ZoneName `
                -OldInputObject $tmpRecord `
                -NewInputObject $newRecord
        }  
    }
}

$curOAB = Get-OfflineAddressBook
Write-Host "Setting Offline Address Book `"$($curOAB.Name)`""
Get-MailboxDatabase -Server $ExchSRVSE.DNSHostName |
        Set-MailboxDatabase -OfflineAddressBook $curOAB.Name

Write-Host "Adding `"$($ExchSRVSE.Name)`" to Send Connectors"
Get-SendConnector |
    Set-SendConnector -SourceTransportServers @{Add="$($ExchSRVSE.Name)"}

Write-Host "Disabling TNEF"
Get-RemoteDomain |
        Set-RemoteDomain -TNEFEnabled $False

Get-Service -ComputerName $oldExchSRV.DNSHostName |
    Where-Object { $_.Name -match "^(MSExchangeImap4|MSExchangePop3|wsbexchange)" -and $_.StartType -eq "Automatic" } |
        ForEach-Object {
            Write-Host "Configuring service `"$($_.DisplayName)`" to automatic start"
            Set-Service -Name $_.Name `
                -StartupType Automatic
        }

Remove-PSSession -Session $ExchSession
Write-Host "Server will reboot after pressing <Enter>"
pause
Restart-Computer

Nach dem erfolgten Reboot kann nahtlos mit dem Verschieben der Postfächer vom Exchange Server 2016 auf den Exchange Server Subscription Edition begonnen werden.

Verschieben der Postfächer

Kommen wir mit dem Script „Move-MailboxesToExchangeSE.ps1“ zum letzten Schritt und verschieben die Postfächer vom Exchange Server 2016 auf den Exchange Server SE. Dabei ist zu beachten, dass die Postfächer erst am nächsten Tag um 02:30 „unserer Zeit“ finalisiert werden. Wer das nicht möchte, muss eingreifen. 🙂

PowerShell
# Move-MailboxesToExchangeSE.ps1
#
#

$objADDomain = Get-ADDomain -Current LocalComputer
$NBDomain = $objADDomain.NetBIOSName
$DNSDomain = $objADDomain.DNSRoot
$DNSServer = $objADDomain.PDCEmulator

$ExchSRVSE = Get-ADComputer -Identity $env:COMPUTERNAME `
    -Properties DNSHostName
$ExchSRVSEIP = Resolve-DnsName -Name $ExchSRVSE.DNSHostName `
    -Type A |
        Select-Object IPAddress -ExpandProperty IPAddress

$oldExchSRV = Get-ADGroupMember -Identity "Exchange Servers" |
    Where-Object { $_.objectClass -eq "computer" -and $_.Name -ne $env:COMPUTERNAME }
$oldExchSRV = Get-ADComputer -Identity $oldExchSRV.distinguishedName `
    -Properties DNSHostName
$oldExchSRVIP = Resolve-DnsName -Name $oldExchSRV.DNSHostName `
    -Type A |
        Select-Object IPAddress -ExpandProperty IPAddress

$ExchSEConUri = -join("http://", `
        $ExchSRVSE.DNSHostName , `
        "/PowerShell" `
    )

do{
    Write-Host "Waiting for Exchange services to start"
    $ExServices = Get-Service |
        Where-Object { $_.DisplayName -match "^Microsoft Exchange" -and $_.StartType -eq "Automatic" -and $_.Status -ne "Running" }
    Start-Sleep -Seconds 10
}until($null -eq $ExServices)

Write-Host "Connecting to Exchange PowerShell Session `"$ExchSEConUri`""
$ExchSession = New-PSSession -ConfigurationName Microsoft.Exchange `
    -ConnectionUri $ExchSEConUri
Import-PSSession -Session $ExchSession -DisableNameChecking `
    -AllowClobber |
        Out-Null

$ExchSEDB = Get-MailboxDatabase -Server $ExchSRVSE.Name

Write-Host "Enabling circular logging for mailbox moves"
Set-MailboxDatabase $ExchSEDB.Name `
        -CircularLoggingEnabled:$true `
        -WarningAction SilentlyContinue

Write-Host "Dismounting database `"$($ExchSEDB.Name)`""
Dismount-Database -Identity $ExchSEDB.Identity `
    -Confirm:$false
Start-Sleep -Seconds 5

Write-Host "Mounting database `"$($ExchSEDB.Name)`""
Mount-Database -Identity $ExchSEDB.Identity
Start-Sleep -Seconds 25

Write-Host "Moving Arbitration, Audit log and Discoverysearch Mailbox to `"$($ExchSRVSE.Name)`""
Get-Mailbox -Arbitration `
    -Server $oldExchSRV.Name |
        New-MoveRequest -TargetDatabase $ExchSEDB.Name |
            Out-Null
Get-Mailbox -AuditLog `
    -Server $oldExchSRV.Name |
        New-MoveRequest -TargetDatabase $ExchSEDB.Name |
            Out-Null
Get-Mailbox -RecipientTypeDetails "DiscoveryMailbox" `
    -Server $oldExchSRV.Name |
        New-MoveRequest -TargetDatabase $ExchSEDB.Name |
            Out-Null

do{
    $curMoveReqs = Get-MoveRequest |
        Where-Object { $_.Status -ne "Completed" }
    $curMoveStats = $curMoveReqs |
        Get-MoveRequestStatistics
    
    Write-Host "Waiting to complete running move requests (Arbitration, Audit log and Discoverysearch)..." `
        -ForegroundColor Yellow       
    foreach($m in $curMoveStats){
        Write-Host "$($m.DisplayName) ($($m.TotalMailboxSize)): $($m.PercentComplete)%"
    }
    if($null -ne $curMoveReqs){
        Write-Host ""
    }
    Start-Sleep -Seconds 60
}until($null -eq $curMoveReqs)

$tmpDate = (Get-Date).AddDays(1)
$tmpDate = -join($tmpDate.Day,
        ".",
        $tmpDate.Month,
        ".",
        $tmpDate.Year,
        " 02:30:00"
    )
$CompletionDate = (Get-Date $tmpDate).ToUniversalTime()

Write-Host "Moving all remaining mailboxes with completion date $CompletionDate (UTC)"
Get-Mailbox -PublicFolder `
    -Server $oldExchSRV.Name |
        New-MoveRequest -TargetDatabase $ExchSEDB.Name `
            -CompleteAfter $CompletionDate |
                Out-Null

Get-Mailbox -Server $oldExchSRV.Name |
    New-MoveRequest -TargetDatabase $ExchSEDB.Name.Name `
        -CompleteAfter $CompletionDate |
            Out-Null

do{
    $curMoveReqs = Get-MoveRequest |
        Where-Object { $_.Status -ne "Completed" }
    $curMoveStats = $curMoveReqs |
        Get-MoveRequestStatistics

    Write-Host "Waiting for completion $CompletionDate (UTC)" `
        -ForegroundColor Yellow
    foreach($m in $curMoveStats){
        Write-Host "$($m.DisplayName) ($($m.TotalMailboxSize)): $($m.PercentComplete)%"
    }
    if($null -ne $curMoveReqs){
        Write-Host ""
    }
    Start-Sleep -Seconds 60
} until($null -eq $curMoveReqs)

Write-Host "Disabling circular logging for mailbox moves"
Set-MailboxDatabase $ExchSEDB.Name `
        -CircularLoggingEnabled:$false `
        -WarningAction SilentlyContinue

Write-Host "Dismounting database `"$($ExchSEDB.Name)`""
Dismount-Database -Identity $ExchSEDB.Identity `
    -Confirm:$false
Start-Sleep -Seconds 5

Write-Host "Mounting database `"$($ExchSEDB.Name)`""
Mount-Database -Identity $ExchSEDB.Identity
Start-Sleep -Seconds 25

Write-Host "Done :)"
Write-Host "`nPlease check `"customized things`" not (yet) handled by the script (e.g. journaling, ...)!" `
    -ForegroundColor Yellow
Deinstallation Exchange Server 2016

Sobald mögliche Nacharbeiten (Mailrouting auf neuen Exchange Server konfigurieren, Empfangskonnektoren, Journaling, Scan2Mail, andere Systeme die über Exchange relayen, …) erledigt sind, kann es nach ein paar Tagen / ein, zwei Wochen mit der Deinstallation des alten Exchange weitergehen. Dazu verbindet man sich am einfachsten per RDP auf den auszumusternden Exchange und führt das Script „Uninstall-Exchange.ps1“ aus.

PowerShell
# Install-ExchangeSEPreRequirements.ps1
#
#

$ExBinPath = Join-Path -Path $env:ExchangeInstallPath `
    -ChildPath "bin"
$ExSetup = Get-ChildItem -Path $ExBinPath `
    -Filter "setup.exe"

Write-Host "Uninstalling Exchange Server"
$ExUninstProc = Start-Process -FilePath $ExSetup.FullName `
    -ArgumentList "/Mode:uninstall" `
    -NoNewWindow `
    -PassThru `
    -Wait

$ExchSE = Get-ADGroupMember -Identity "Exchange Servers" |
    Where-Object { $_.objectClass -eq "computer" }

Write-Host "Moving Exchange uninstall logs to `"\\$($ExchSE.Name)\c$\_install\Logging\ExchangeSetupLogs-Uninstall`""
$ExUninstLogs = Get-ChildItem -Path "C:\" `
    -Filter "ExchangeSetupLogs" |
        Sort-Object LastWriteTime -Descending |
            Select-Object -First 1

Rename-Item -Path $ExUninstLogs.FullName `
    -NewName "ExchangeSetupLogs-Uninstall"
Move-Item -Path "C:\ExchangeSetupLogs-Uninstall" `
    -Destination "\\$($ExchSE.Name)\c$\_install\Logging\ExchangeSetupLogs-Uninstall"

Write-Host "Server will reboot after pressing <Enter>"
pause
Restart-Computer

Im Anschluss sollte der alte Server natürlich noch aus der Domäne entfernt und das Computerkonto gelöscht werden.

Bei Fragen, fragt. 🙂 Theoretisch sollte das Script in vielen Umgebungen recht unfallfrei funktionieren. Wenn es Probleme gibt, „lasst nen Kommi da“. 😉


Die gesuchte Lösung noch nicht gefunden oder benötigen Sie Hilfe bei anderen Themen aus meinem Blog? Nehmen Sie gerne Kontakt mit mir bzw. meinem Unternehmen Jan Mischo IT auf. Ich freue mich auf Ihre Anfrage: https://janmischo.it/kontakt/


+49 2801 7004300

info@janmischo.it


Beitrag veröffentlicht

in

, , ,

von

Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre, wie deine Kommentardaten verarbeitet werden.