Friday, 8 February 2019

Powershell LDAP query to find Azure / O365 users synchronised with AD Sync

 

 

Recently I needed to create a quick report that would allow me to see at a glance which accounts in that domain had been synchronised with AD Sync into Azure AD.  It wasn’t possible using Get-ADuser and I knew an LDAP query would do the trick.  First I had to download a powershell module called System.DirectoryServices.Protocols.  Once the module is downloaded run:

 

 

Add-Type -AssemblyName System.DirectoryServices.Protocols

Import-Module C:\Cloudwyse\Tools\S.DS.P.psm1

 

Then to query the information I required I ran:

 

 

$MigratedUsers=Find-LdapObject -SearchFilter:"(msDS-ExternalDirectoryObjectId=*)" -SearchBase:"DC=contoso,DC=com" -LdapConnection:"server01.contoso.com" -PageSize 500

 

 

Conversely, if you wanted to find all users that HADN’T been synchronised you could run the following:

 

 

$MigratedUsers=Find-LdapObject -SearchFilter:"(!msDS-ExternalDirectoryObjectId=*)" -SearchBase:"DC=contoso,DC=com" -LdapConnection:"server01.contoso.com" -PageSize 500

 

 

I still had a few service accounts showing so I just filtered these in Excel based on the DN.  To export the fil just run…

 

 

Export-CSV C:\Cloudwyse\User_report.csv

 

 

 

Tuesday, 5 February 2019

Migrate O365 mailboxes using Hard Matching with ImmutableID

 

 

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.