I have been working on a mail migration within an environment that has a Hybrid Exchange configuration with a single 365 tenant but which synchronises Active Directory from multiple forests. As part of the migration there is a need to migrate on-prem user accounts from a legacy forest into a new forest, but the accounts need to continue to be synchronised with O365 using AD Sync for password changes. As the mailboxes have already been synchronised with an existing on-prem account, it wasn’t possible to do SMTP matching, so it was necessary to use hard matching with ImmutableID. I wrote the following script to help me as I needed to carry out the migration in small batches rather than big bang, and still allow clients to work on the system in the meantime and avoid any disruption to day to day activities.
The following script has allowed me to do that without disrupting mail flow or restricting the activities of the users other than within the 15-20 period it takes to migrate each batch. The script is broken down here but is also available at the end of the post as a ps1 file.
The first section contains all the variables
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
[cc lang="powershell"]$LogFilePath = $env:LOCALAPPDATA + "\Cloudwyse\Logs\o365_user_migration_" + $(get-date -Format ddMMyy_HHmmss) + ".log" Start-Transcript -Path $LogFilePath -NoClobber $LegacyDC = "server01.contoso.com" $NewEnvDC = "server01.corp.contoso.com" $AADServer = "server02.contoso.com" $scroll = "/-\|/-\|" $idx = 0 $LegacyCred = Get-Credential -UserName "CONTOSO\" -Message "LEGACY credentials for $LegacyDC" $pass = cat C:\cloudwyse\secure.txt | convertto-securestring $LegacyCred = new-object -typename System.Management.Automation.PSCredential -argumentlist "contoso\administrator",$pass $NewCred = Get-Credential -UserName ($env:UserDomain + "\" + $env:UserName) -Message "NEW credentials for $NewEnvDC" $NewPass = cat C:\cloudwyse\securenew.txt | convertto-securestring $NewCred = new-object -typename System.Management.Automation.PSCredential -argumentlist "CORP\administrator",$NewPass $365cred = Get-Credential -UserName admin@contoso.onmicrosoft.com -Message "O365 Credentials" $365Pass = cat C:\cloudwyse\secure365.txt | convertto-securestring $365Cred = new-object -typename System.Management.Automation.PSCredential -argumentlist "admin@contoso.com",$NewPass $NewTargetOU = "OU=Swap,OU=Users,OU=London,OU=Tailspin Toys,DC=corp,DC=contoso,DC=com" $LegacyTargetOU = "OU=_Migrated Users,DC=contoso,DC=com" $DateTime = (Get-Date -Format "MMddyyyy-HHmmss")[/cc] |
This section contains all the functions we will use later on
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
[cc lang="powershell" escape="true"]function PresstoContinue { Read-Host "Press Return to continue..." | Out-Null } function Check365 { Read-Host "`nIMPORTANT MESSAGE - PLEASE READ!!! `rBefore continuing to the next section it is important to ensure that the selected mailboxes `rhave been moved to deleted users in O365. A list of these mailboxes will now be displayed. `rIf you do not see the relevant mailboxes in the list, wait until they are present. `rPress Return to continue and see the list..." | Out-Null } function HavePatience { write-host -ForegroundColor Magenta -NoNewLine "Allowing a few seconds for propogation of changes…" start-sleep 1 write-host -ForegroundColor Magenta -NoNewLine "5…" start-sleep 1 write-host -ForegroundColor Magenta -NoNewLine "4…" start-sleep 1 write-host -ForegroundColor Magenta -NoNewLine "3…" start-sleep 1 write-host -ForegroundColor Magenta -NoNewLine "2…" start-sleep 1 write-host -ForegroundColor Magenta "1…" start-sleep 1 } function DoubleCheck { write-host -ForegroundColor Blue -BackgroundColor White "----------BEGIN PREVIEW---------------" get-content -Path "C:\Cloudwyse\MigrateList.csv" -totalcount 10 write-host -ForegroundColor Blue -BackgroundColor White "----------END PREVIEW-----------------" Read-Host "Press Return to process the current list of users in C:\Cloudwyse\MigrateList.csv as previewed above" | Out-Null } function DistImport { write-host -ForegroundColor Blue -BackgroundColor White "----------BEGIN PREVIEW---------------" get-content -Path "C:\Cloudwyse\distGroups_bak.csv" -totalcount 10 write-host -ForegroundColor Blue -BackgroundColor White "----------END PREVIEW-----------------" Read-Host "Press Return to process the distribution lists previewed above." | Out-Null } function Scrolly-Scrolly { write-host -ForegroundColor Magenta "Please be patient…" $origpos = $host.UI.RawUI.CursorPosition $origpos.Y += -1 $origpos.X += 20 while (($CurrentJob.State -eq "Running") -and ($CurrentJob.State -ne "NotStarted")) { $host.UI.RawUI.CursorPosition = $origpos Write-Host -ForegroundColor Yellow $scroll[$idx] -NoNewline $idx++ if ($idx -ge $scroll.Length) {$idx = 0 } Start-Sleep -Milliseconds 100 } $host.UI.RawUI.CursorPosition = $origpos Write-Host -ForegroundColor Yellow "`nThe command completed"[/cc] |
This section imports the pre-prepared CSV file which we have named migratelist.csv. This list is what is used to match the newly created accounts in the destination domain with the accounts in the old domain. Before importing the file it will show a preview of the file contents so that there is an opportunity for the admin to trap any mistakes such as referencing the wrong file. When creating this file you need to ensure that it contains the following row headers:
SrcName,DstName,License,Password,GUID,ImmutableID,UPN
- SrcName – contains the SAMAccountName of the existing user account eg. j.doe
- DstName – contains the SAMAccountName of the destination account however it will appear in the new active directory domain ie john.doe
- License – contains the “AccountSKUId” (this can be obtained by running Get-MsolAccountSku)
- Password – Include the password you wish to be set on the mailbox once the mogration is complete. Read this article for a guide on how to generate random passwords suitable for O365
- GUID – Leave blank
- ImmutableID – Leave blank
- UPN – Leave blank
Delete any blank rows in the csv file (ie rows that would be imported as ,,,,,,,,,,,,,,,,,,,,).
1 2 |
[cc lang="powershell"]DoubleCheck $MigrateList = Import-CSV -Path "C:\Cloudwyse\MigrateList.csv" #-Header SrcName,DstName,License,Password,GUID,ImmutableID,UPN[/cc] |
This section forces an AD synchronisation in the legacy environment. It’s run as a job so that we can see that the script is still running and isn’t just hanging. I don’t like it when my scripts go away and do things without reporting progress to me!
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[cc lang="powershell" escape="true"]$CurrentJob = Start-Job -ScriptBlock {param($LegacyDC,$LegacyCred) $LegacySession = New-PSSession -ComputerName $LegacyDC -Credential $LegacyCred Invoke-Command $LegacySession -Scriptblock {Import-Module ActiveDirectory} Invoke-Command $LegacySession -Scriptblock {repadmin /syncall /APed} Invoke-Command $LegacySession -Scriptblock {start-sleep 3} Remove-PSSession $LegacySession } -ArgumentList $LegacyDC,$LegacyCred write-host -ForegroundColor Magenta -BackgroundColor White "Running the legacy Active Directory sync job" Scrolly-Scrolly $JobSecondsTaken = [math]::Round(($CurrentJob.PSEndTime.TimeOfDay.TotalSeconds - $CurrentJob.PSBeginTime.TimeOfDay.TotalSeconds),1) Write-Host -ForegroundColor Yellow "The job took $JobSecondsTaken seconds" $CurrentJob = $null HavePatience[/cc] |
I found that very large mailboxes can take longer to appear in the list of soft deleted mailboxes in O365. For that reason I’ve added a quick check that allows the administrator to see the size of all the mailboxes in the migration. If there are any that are very large the administrator is then aware that more patience may be require for these to appear in the softdeleted mailbox list.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
[cc lang="powershell"]$365Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $365cred -Authentication Basic -AllowRedirection Import-PSSession $365Session $sizelist = @() foreach ($user in $Migratelist) { $stats = Get-MailboxStatistics $user.SrcName $stats | foreach-object { $build = New-Object PSObject $build | Add-Member -type NoteProperty -Name 'Name' -Value $_.DisplayName $build | Add-Member -type NoteProperty -Name 'IsArchiveMailbox' -Value $_.IsArchiveMailbox $build | Add-Member -type NoteProperty -Name 'Items' -Value $_.ItemCount $build | Add-Member -type NoteProperty -Name 'Size' -Value $_.TotalItemSize $sizelist += $build } } $sizelist | sort-object -Property Name | format-table start-sleep 1 Remove-PSSession $365Session $GoAhead = Read-Host -Prompt "Please check the list above which includes all the mailboxes you are planning to migrate. If any are larger than 500MB please consider this will take longer than normal. `nDo you wish to continue? [y/n] `(Default is `"N`"`)" if ( $GoAhead -NotMatch "[yY]" ) { write-host "Exiting…" Stop-Transcript exit } else { #continue running script }[/cc] |
In order to fool O365 into deprovisioning the existing user account and softdeleting the mailbox, we need to make O365 think that the associated user account has been deleted. There are two ways to do this… one is to use this undocumented filter and populate the “adminDescription” attribute for the user account with the value “User_NoO365Sync”. The trouble with this is that it isn’t then clear exactly which accounts will sync and which won’t. I prefer the second option which is to create an OU called “_Migrated Users” and to move the users to that. The reason is that it helps to organize the environment and make it clear which users are migrated and which aren’t. It’s also preferable to deleting the accounts as it allows the migrated users to continue to log on in the legacy domain if that’s required.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[cc lang="powershell" escape="true"]$LegacySession = New-PSSession -ComputerName $LegacyDC -Credential $LegacyCred Invoke-Command $LegacySession -Scriptblock {Import-Module ActiveDirectory} Import-PSSession $LegacySession -Module ActiveDirectory $MigrateList | ForEach-Object { $UserDN = (Get-ADUser -Identity $_.SrcName).distinguishedName Write-Host -ForegroundColor Magenta "Moving account for $UserDN" Move-ADObject -Identity $UserDN -TargetPath $LegacyTargetOU $total = $total +1 } Write-Host -ForegroundColor Yellow "Batch complete" Write-Host -ForegroundColor Yellow "$total accounts moved" $total = $null Remove-PSSession $LegacySession[/cc] |
Sync legacy AD again to propagate changes
1 2 3 4 5 6 7 8 9 10 11 12 |
[cc lang="powershell"]$CurrentJob = Start-Job -ScriptBlock { param ($LegacyDC,$LegacyCred) $LegacySession = New-PSSession -ComputerName $LegacyDC -Credential $LegacyCred Invoke-Command $LegacySession -Scriptblock {Import-Module ActiveDirectory} Invoke-Command $LegacySession -Scriptblock {repadmin /syncall /APed} Remove-PSSession $LegacySession } -ArgumentList $LegacyDC,$LegacyCred write-host -ForegroundColor Magenta -BackgroundColor White "Running the legacy Active Directory sync job" Scrolly-Scrolly $JobSecondsTaken = [math]::Round(($CurrentJob.PSEndTime.TimeOfDay.TotalSeconds - $CurrentJob.PSBeginTime.TimeOfDay.TotalSeconds),1) Write-Host -ForegroundColor Yellow "The job took $JobSecondsTaken seconds" $CurrentJob = $null HavePatience[/cc] |
Initiates a delta synchronisation cycle through AADsync. This is the point at which O365 will think the users have been deleted in the local AD. O365 will also remove the ImmutableID value at this stage so that we can re-populate it later.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[cc lang="powershell" escape="true"]$CurrentJob = Start-Job -ScriptBlock { param ($AADServer,$LegacyCred) $AADSession = New-PSSession -ComputerName $AADServer -Credential $LegacyCred Invoke-Command $AADSession -Scriptblock {Import-Module ADSync} Invoke-Command $AADSession -Scriptblock {Start-ADSyncSyncCycle -PolicyType Delta} Invoke-Command $AADSession -Scriptblock {start-sleep 3} Remove-PSSession $AADSession } -ArgumentList $AADServer,$LegacyCred write-host -ForegroundColor Magenta -BackgroundColor White "Running the Azure AD sync job" Scrolly-Scrolly $JobSecondsTaken = [math]::Round(($CurrentJob.PSEndTime.TimeOfDay.TotalSeconds - $CurrentJob.PSBeginTime.TimeOfDay.TotalSeconds),1) Write-Host -ForegroundColor Yellow "The job took $JobSecondsTaken seconds" $CurrentJob = $null HavePatience[/cc] |
This uses the migratelist object we imported earlier, and populates the it with the GUIDs from the new AD. This will match the accounts from the spreadsheet with the new accounts and pull in the GUID data. The GUID is then converted to a base 64 string that will match the required format for the ImmutableID in O365. The revised list is then exported as “revisedMigrateList.csv” so that we have a backup.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
[cc lang="powershell" escape="true"]$TargetSession = New-PSSession -ComputerName $NewEnvDC -Credential $NewCred Invoke-Command $TargetSession -Scriptblock {Import-Module ActiveDirectory} Import-PSSession $TargetSession -Module ActiveDirectory $MigrateList | ForEach-Object { $guid = (Get-ADUser -Identity $_.DstName).Objectguid $immutableid = [System.Convert]::ToBase64String($guid.tobytearray()) Add-Member -InputObject $_ -MemberType NoteProperty -Name GUID -Value $guid -Force Add-Member -InputObject $_ -MemberType NoteProperty -Name ImmutableID -Value $immutableid -Force $currentuser = $_ | select -ExpandProperty "DstName" Write-Host -ForegroundColor Magenta "Added GUID and ImmutableID details for $currentuser" $total = $total +1 } $MigrateList | Export-CSV C:\Cloudwyse\revisedMigrateList.csv Write-Host -ForegroundColor Yellow "Batch complete" Write-Host -ForegroundColor Yellow "GUID details added for $total users" $total = $null Remove-PSSession $Targetsession[/cc] |
Pulls the UPN value over from the Legacy AD and populates the file with the information. The revised list is then exported once again, this time as “revisedMasterListwithUPN.csv”.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[cc lang="powershell" escape="true"]$LegacySession = New-PSSession -ComputerName $LegacyDC -Credential $LegacyCred Invoke-Command $LegacySession -Scriptblock {Import-Module ActiveDirectory} Import-PSSession $LegacySession -Module ActiveDirectory $MigrateList | ForEach-Object { $UPN = (Get-ADUser -Identity $_.SrcName).UserPrincipalName Add-Member -InputObject $_ -MemberType NoteProperty -Name UPN -Value $UPN -Force $currentuser = $_ | select -ExpandProperty "SrcName" Write-Host -ForegroundColor Magenta "Added UPN details for $currentuser" $total = $total +1 } $MigrateList | Export-CSV C:\Cloudwyse\revisedMasterListwithUPN.csv Write-Host -ForegroundColor Yellow "Batch complete" Write-Host -ForegroundColor Yellow "UPN details added for $total users" $total = $null Remove-PSSession $LegacySession[/cc] |
This section is not always necessary, but it makes sure we have a backup of the exisiting distribution groups for peace of mind. A lookup is done to populate a variable with all distribution groups for each user. This is then exported to a csv file as a backup (in case there are issues with the migration – better safe than sorry). It will create a backup of the existing lists, change the current list to a backup copy and archive any exisiting lists so that we never over-write any of this data. Later on there is logic that will import these backups if the distribution list variable is empty – something that can happen if the script is stopped halfway through. We have to rely on the backup because once the user has been deprovisioned, we can’t go back and import the data again.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
[cc lang="powershell" escape="true"]$365Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $365cred -Authentication Basic -AllowRedirection Import-PSSession $365Session $distGroups = @() foreach ($User in $MigrateList) { $Mailbox = get-Mailbox $user.UPN $DN = $Mailbox.DistinguishedName $Filter = "Members -like ""$DN""" $userDLs = Get-DistributionGroup -Filter $filter $userDLs | ForEach-Object { $gr = New-Object PSObject $gr | Add-Member -type NoteProperty -Name 'UPN' -Value $User.UPN $gr | Add-Member -type NoteProperty -Name 'Group' -Value $_.Id $distGroups += $gr $groupname = $_.Id Write-Host -ForegroundColor Magenta "The Group $groupname was added to the list for" $user.srcname $total = $total +1 } } Write-Host -ForegroundColor Yellow "$total distribution groups processed" $total = $null if (Test-Path "C:\Cloudwyse\distGroups_bak.csv") { Rename-Item -Path "C:\Cloudwyse\distGroups_bak.csv" -NewName "distGroups_$Datetime.csv" } else { #continue script } if (Test-Path "C:\Cloudwyse\distGroups.csv") { Rename-Item -Path "C:\Cloudwyse\distGroups.csv" -NewName "distGroups_bak.csv" } else { #continue script } $distGroups | Export-CSV C:\Cloudwyse\distGroups.csv remove-pssession $365Session[/cc] |
The next step asks the user to check that the mailboxes are visible amongst the softdeleted mailboxes in O365 (as this can take a little time), then they are undeleted, which will set them to a cloud mailbox rather than “synced with AD”. You could change sort-object -Property Displayname to sort-object -Property WhenSoftDeleted to change the view to latest first if you prefer. I did think about automating this part of the script by checking for the existence of the softdeleted mailboxes within the script rather than relying on the administrator but I think it’s better to have eyes on what’s going on at this point. It’s a powerful script to just leave running without any human interaction. Continuing at this point will undelete the mailbox and assign the new password.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
[cc lang="powershell" escape="true"]check365 $365Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $365cred -Authentication Basic -AllowRedirection Import-PSSession $365Session Get-Mailbox -SoftDeletedMailbox -filter {ExternalDirectoryObjectID -ne $null} | select DisplayName,WhenSoftDeleted,PrimarySmtpAddress,ExchangeGuid,ExternalDirectoryObjectID | sort-object -Property Displayname | format-table do { $HappyToContinue = Read-Host -Prompt "Are the users there? Are you happy to continue? Yes continues, No waits a bit longer [y/n]" if ($HappyToContinue -eq "n") { Get-Mailbox -SoftDeletedMailbox -filter {ExternalDirectoryObjectID -ne $null} | select DisplayName,WhenSoftDeleted,PrimarySmtpAddress,ExchangeGuid,ExternalDirectoryObjectID | sort-object -Property Displayname | format-table } } until ($HappyToContinue -eq "y") $MigrateList | ForEach-Object { Undo-SoftDeletedMailbox $_.UPN -WindowsLiveID $_.UPN -Password (ConvertTo-SecureString -String $_.Password -AsPlainText -Force) $currentuserUPN = $_ | select -ExpandProperty "UPN" Write-Host -ForegroundColor Magenta "Processed user $currentuserUPN" $total = $total +1 } Write-Host -ForegroundColor Yellow "Batch complete" Write-Host -ForegroundColor Yellow "Mailboxes undeleted for $total users" $total = $null Remove-PSSession $365Session[/cc] |
At this point I added the pause because we are at a point of no return (that’s not actually true as I do have a reverse migration script as well, but it’s a lot of hassle if we can avoid it. The immutableID is then updated with the value which was converted from the GUID and a license is assigned. The viability check just makes sure that the destination users are in the _Inbound Users OU in the target domain. This is also filtered so that it is not synchronised by AD Sync. It’s our ‘staging area’ for the incoming users.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
[cc lang="powershell" escape="true"]PresstoContinue $CurrentJob = Start-Job -ScriptBlock { param ($NewEnvDC,$NewCred) $TargetSession = New-PSSession -ComputerName $NewEnvDC -Credential $NewCred Invoke-Command $TargetSession -Scriptblock {Import-Module ActiveDirectory} Invoke-Command $TargetSession -Scriptblock {repadmin /syncall /APed} Remove-PSSession $TargetSession } -ArgumentList $NewEnvDC,$NewCred write-host -ForegroundColor Magenta -BackgroundColor White "Running the new Active Directory sync job" Scrolly-Scrolly $JobSecondsTaken = [math]::Round(($CurrentJob.PSEndTime.TimeOfDay.TotalSeconds - $CurrentJob.PSBeginTime.TimeOfDay.TotalSeconds),1) Write-Host -ForegroundColor Yellow "The job took $JobSecondsTaken seconds" HavePatience $CurrentJob = $null $TargetSession = New-PSSession -ComputerName $NewEnvDC -Credential $NewCred Invoke-Command $TargetSession -Scriptblock {Import-Module ActiveDirectory} Import-PSSession $TargetSession -Module ActiveDirectory Write-Host -ForegroundColor Yellow "Checking viability of migration…" $warning = 0 $MigrateList | ForEach-Object { $Location = (Get-ADUser -Identity $_.DstName).distinguishedName if ($Location -NotLike "*_OU=Inbound Users*") {$warning = 1 $problems = $problems +1 } else { $successes = $successes +1 } $currentuserUPN = $_ | select -ExpandProperty "UPN" Write-Host -ForegroundColor Magenta "Processed user $Location" $total = $total +1 } Write-Host -ForegroundColor Yellow "$total checks complete" if ($warning -eq $true) { Write-Host -ForegroundColor Red -BackgroundColor Yellow "!!!WARNING!!!" Write-Host -ForegroundColor Red -BackgroundColor Yellow "$problems issues were detected" Write-Host -ForegroundColor Red -BackgroundColor Yellow "To rectify the problems, ensure that all target user accounts are in the `"_Inbound Users`" OU in corp.contoso.com" Write-Host -ForegroundColor Red -BackgroundColor Yellow "The script will now terminate" Remove-PSSession $TargetSession Stop-Transcript exit } elseif ($successes -gt 1) { Write-Host -ForegroundColor Yellow "Validated that $successes mailboxes are safe to migrate" } else { Write-Host -ForegroundColor Yellow "One mailbox checked and validated safe for migration" } $total = $null Write-Host -ForegroundColor Magenta "Beginning mailbox migration" Connect-Msolservice -Credential $365cred $MigrateList | ForEach-Object { Set-MsolUser -UserPrincipalName $_.UPN -UsageLocation GB Set-MsolUserLicense -UserPrincipalName $_.UPN -AddLicenses $_.License Set-MsolUser -UserPrincipalName $_.UPN -ImmutableId $_.ImmutableID $currentuserUPN = $_ | select -ExpandProperty "UPN" Write-Host -ForegroundColor Magenta "Processed user $currentuserUPN" $total = $total +1 } Write-Host -ForegroundColor Yellow "Batch complete" Write-Host -ForegroundColor Yellow "Immutable ID set for $total users" $total = $null[/cc] |
One more chance to back out of the changes that have jsut been made here before the accounts are moved to an OU that will be synchronised. I added this extra step because if the ImmutableID was not set earlier for any reason (such as the script errored), I didn’t want this to run on as it would create duplicate accounts in Azure.
1 2 3 4 5 6 7 8 9 10 11 |
[cc lang="powershell" escape="true"]PresstoContinue $MigrateList| ForEach-Object { $UserDN = (Get-ADUser -Identity $_.DstName).distinguishedName Write-Host -ForegroundColor Magenta "Moving account for $UserDN" Move-ADObject -Identity $UserDN -TargetPath $NewTargetOU $total = $total +1 } Write-Host -ForegroundColor Yellow "Batch complete" Write-Host -ForegroundColor Yellow "$total accounts moved" $total = $null Remove-PSSession $TargetSession[/cc] |
The new AD is then synchronised to ensure that all domain controllers are up to date with the changes
1 2 3 4 5 6 7 8 9 10 11 12 |
[cc lang="powershell" escape="true"]$CurrentJob = Start-Job -ScriptBlock { param ($NewEnvDC,$NewCred) $TargetSession = New-PSSession -ComputerName $NewEnvDC -Credential $NewCred Invoke-Command $TargetSession -Scriptblock {Import-Module ActiveDirectory} Invoke-Command $TargetSession -Scriptblock {repadmin /syncall /APed} Remove-PSSession $TargetSession } -ArgumentList $NewEnvDC,$NewCred write-host -ForegroundColor Magenta -BackgroundColor White "Running the new Active Directory sync job" Scrolly-Scrolly $JobSecondsTaken = [math]::Round(($CurrentJob.PSEndTime.TimeOfDay.TotalSeconds - $CurrentJob.PSBeginTime.TimeOfDay.TotalSeconds),1) Write-Host -ForegroundColor Yellow "The job took $JobSecondsTaken seconds" $CurrentJob = $null HavePatience[/cc] |
AADSync is the forced to run another synchronisation. At this point it will spot the matching ImmutableIDs on the cloud mailbox and the local AD account and associate the cloud mailbox with the on-prem account. The status of the mailbox in O365 will then change back to “synced with AD”.
1 2 3 4 5 6 7 8 9 10 11 |
[cc lang="powershell" escape="true"]$CurrentJob = Start-Job -ScriptBlock { param ($AADServer,$LegacyCred) $AADSession = New-PSSession -ComputerName $AADServer -Credential $LegacyCred Invoke-Command $AADSession -Scriptblock {Import-Module ADSync} Invoke-Command $AADSession -Scriptblock {Start-ADSyncSyncCycle -PolicyType Delta} Remove-PSSession $AADSession } -ArgumentList $AADServer,$LegacyCred write-host -ForegroundColor Magenta -BackgroundColor White "Running the Azure AD sync job" Scrolly-Scrolly $JobSecondsTaken = [math]::Round(($CurrentJob.PSEndTime.TimeOfDay.TotalSeconds - $CurrentJob.PSBeginTime.TimeOfDay.TotalSeconds),1) Write-Host -ForegroundColor Yellow "The job took $JobSecondsTaken seconds" $CurrentJob = $null[/cc] |
The distribution groups should have remained with the mailbox, but I have know some get missed. This next step just tries to add the users to the distribution groups we gathered earlier. If the variable is empty it will warn and then try to import the backup. This may still be empty (if there weren’t any groups to add) but the script can just be allowed to run on. If the user is already a member of the group then the error checking will display the message that the use is already a member of the group.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
[cc lang="powershell" escape="true"]$365Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $365cred -Authentication Basic -AllowRedirection Import-PSSession $365Session if (!$distGroups) { read-host "The required list of distribution groups is empty. This may be because the script was terminated prematurely. An attempt will be made to re-import the backup data. Press Return to continue and see a preview of the imported list." | out-null DistImport $distGroups = Import-CSV -Path C:\Cloudwyse\distGroups_bak.csv } else { #continue script } write-host -ForegroundColor Magenta -BackgroundColor White "Adding users to distribution lists" $Totalprocessed = 0 $Totalupdated = 0 foreach ($identity in $distGroups) { Try { Add-DistributionGroupMember -Identity $identity.Group -Member $identity.UPN -ErrorAction Stop | out-null } Catch [System.Management.Automation.RemoteException] { Write-Host -ForegroundColor Cyan "The user" $identity.UPN "was already a member of" $identity.Group $NextAction = "skip" } Finally { if ($NextAction -ne "skip") { write-host -ForegroundColor Magenta "Added" $identity.UPN "to the" $identity.Group "group" $totalupdated = $totalupdated +1 $NextAction = $null } else { $NextAction = $null } $totalprocessed = $totalprocessed +1 } Write-Host -ForegroundColor Yellow "Processed $totalprocessed record(s)" Write-Host -ForegroundColor Yellow "Changed $totalupdated group memberships(s)" Remove-PSSession $365Session Write-Host -ForegroundColor Yellow "`nThe script has completed. `nAll migrated accounts in the old domain can be found in $LegacyTargetOU `nAll target accounts in the new domain can be found in $NewTargetOU `nAll logs for this job can be found at $LogFilePath" Stop-Transcript[/cc] |