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
|
$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")
|
This section contains all the functions we will use later on
|
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"
|
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 ,,,,,,,,,,,,,,,,,,,,).
|
DoubleCheck
$MigrateList = Import-CSV -Path "C:\Cloudwyse\MigrateList.csv" #-Header
SrcName,DstName,License,Password,GUID,ImmutableID,UPN
|
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!
|
$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
|
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.
|
$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
}
|
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.
|
$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
|
Sync legacy AD again to propagate changes
|
$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
|
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.
|
$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
|
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.
|
$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
|
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".
|
$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
|
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.
|
$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
|
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.
|
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
|
At this point I added the pause because we
are at a point of no retrun (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.
|
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
|
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.
|
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
|
The new AD is then synchronised to ensure
that all domain controllers are up to date with the changes
|
$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
|
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".
|
$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
|
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.
|
$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
|
The script can be downloaded complete as a
ps1 file here.
Or you can view the code on Github below
|