Mass Migration of Exchange 2007 to Exchange 2010 Users

My company went through a migration a few years ago of all of our users from Exchange 2003 to Exchange 2007.  The mailbox moves were insanely time-consuming.  Each mailbox has to be taken offline during the migration and that meant many, many nights of sitting up all night staring at progress bars.

Exchange 2010 handles mailbox migrations so much smoother since it doesn’t require that someone sit and stare at the screen.  Couple that with the fact that the mailboxes do not need to be offline to handle this task and the pain to the user community and the number of man-hours (or more accurately “person-hours”) drops significantly.

We have an environment where we don’t really care which Mailbox database you end up on.  Some organizations have Mailbox Databases that are configured for the Executives, another for the Professionals, and another for the Staff.  Since we don’t care, I wrote a script to just balance the databases based on size as the requirement.

Here’s the pseudo code for my script:

  1. Get a list of all the mailboxes that need to be moved
  2. Get a list of all the possible target databases
  3. Create a new variable (object) to keep track of the database information
  4. Sort the databases by size (file size less whitespace) with smallest first
  5. Sort the mailboxes by size with the largest first
  6. Create a new Mailbox Move Request to move the largest mailbox to the smallest database
  7. Increment a counter for the number of mailboxes in the database
  8. Increment the database size with the size of the mailbox being moved
  9. Remove that mailbox from the list to move
  10. Go back to step 4 and repeat until there are no more moves to create.

This is how my script looks:

################################################
# Script Name:	Create-AllUsersMoveRequests.ps1
# Author:		Kevin Sparenberg
# Purpose:		Create move requests for all active users and balance the database sizes
################################################

# Clean up any variables that are still sitting out there suppressing errors
Get-Variable | Remove-Variable -ErrorAction SilentlyContinue

# Add the Exchange 2010 PowerShell Snapin (if not already loaded), suppressing errors
Add-PSSnapin Microsoft.Exchange.Management.PowerShell.E2010 -ErrorAction SilentlyContinue

$CreateMoveRequests = $true

# Get all 2010 Databases 
$TargetDatabases = Get-MailboxDatabase -Status | Sort-Object Name

# Build a New Collection to Track the Database Size as mailboxes are added
$TargetDBs = @()

$TargetDatabaseCount = $TargetDatabases.Count

$i = 1 # Counter
ForEach ( $TargetDatabase in $TargetDatabases )
{
	Write-Progress -Activity "Building Custom Mailbox Database Object Collection" -Status ( "Target Database [" + $i + "/" + $TargetDatabaseCount + "]" ) -CurrentOperation ( "Building Custom Object for Database: " + $TargetDatabase.Name ) -PercentComplete ( ( $i / $TargetDatabaseCount ) * 100 )

	$TargetDBItem = New-Object PSObject

	$TargetDBItem | Add-Member -MemberType NoteProperty -Name Name -Value $TargetDatabase.Name
	$TargetDBItem | Add-Member -MemberType NoteProperty -Name OriginalSize -Value $TargetDatabase.DatabaseSize
	$TargetDBItem | Add-Member -MemberType NoteProperty -Name OriginalWhiteSpace -Value $TargetDatabase.AvailableNewMailboxSpace
	$TargetDBItem | Add-Member -MemberType NoteProperty -Name ProjectedSize -Value ( $TargetDatabase.DatabaseSize - $TargetDatabase.AvailableNewMailboxSpace )
	$TargetDBItem | Add-Member -MemberType NoteProperty -Name MailboxCount -Value ( ( Get-Mailbox -ResultSize Unlimited -Database $TargetDatabase | Measure-Object ).Count )

	$TargetDBs += $TargetDBItem

	$i++
}
Write-Progress -Activity "Building Custom Mailbox Database Object Collection" -Status "Completed" -Completed

# Get all Legacy Mailboxes and Filter off any we don't want
$LegacyMailboxes = Get-ExchangeServer | Where-Object { -not $_.IsE14orLater -and $_.IsMailboxServer -and $_.Name -like "*EXMBX*" } | Get-Mailbox -ResultSize Unlimited -Filter { WindowsEmailAddress -like "*@dlapiper.com" -and CustomAttribute1 -ne $null -and CustomAttribute4 -notlike "TERM*" }

# Create an empty collection for the move requests
$MoveRequests = @()

$LegacyMailboxCount = $LegacyMailboxes.Count

$i = 1 # Counter

ForEach ( $Mailbox in $LegacyMailboxes )
{

	Write-Progress -Activity "Building Custom Mailbox Object Collection" -Status ( "Determining Target Database for Mailbox [" + $i + "/" + $LegacyMailboxCount + "]" ) -CurrentOperation ( "Building Custom Object for Mailbox: " + $Mailbox.DisplayName ) -PercentComplete ( ( $i / $LegacyMailboxCount ) * 100 )

	if ( $Mailbox.Identity -notlike "*Disabled*" )
	{
		$MoveRequest = New-Object PSObject

		$MoveRequest | Add-Member -MemberType NoteProperty -Name DisplayName -Value $Mailbox.DisplayName
		$MoveRequest | Add-Member -MemberType NoteProperty -Name Department -Value $Mailbox.CustomAttribute14
		$MoveRequest | Add-Member -MemberType NoteProperty -Name Office -Value $Mailbox.Office
		$MoveRequest | Add-Member -MemberType NoteProperty -Name MBXIdentity -Value $Mailbox.Identity
		$MoveRequest | Add-Member -MemberType NoteProperty -Name MBXSize -Value ( ( Get-MailboxStatistics $Mailbox ).TotalItemSize )
		if ( $Mailbox.ServerName.SubString(0,4) -eq "SAND" )
		{
			$MoveRequest | Add-Member -MemberType NoteProperty -Name DataCenter -Value "West"
		}
		elseif ( $Mailbox.ServerName.SubString(0,4) -eq "BALT" )
		{
			$MoveRequest | Add-Member -MemberType NoteProperty -Name DataCenter -Value "East"
		}
		else
		{
			$MoveRequest | Add-Member -MemberType NoteProperty -Name DataCenter -Value "????"
		}

		# Add the empty Target Database Field to the Move Object
		$MoveRequest | Add-Member -MemberType NoteProperty -Name TargetDB -Value $null

		$MoveRequests += $MoveRequest
		$i++
	}

}
Write-Progress -Activity "Building Custom Mailbox Object Collection" -Status "Completed" -Completed

# Sort the Legacy Mailboxes by Mailbox Size (Largest First)
$MoveRequests = $MoveRequests | Sort-Object MBXSize -Descending

$MoveRequestCount = $MoveRequests.Count

$i = 1 # Counter
ForEach ( $MoveRequest in $MoveRequests )
{
	Write-Progress -Activity "Determining Mailbox Placement" -Status ( "Determining Target Database for Mailbox [" + $i + "/" + $MoveRequestCount + "]" ) -CurrentOperation ( "Assigning Target Database for Mailbox: " + $MoveRequest.DisplayName ) -PercentComplete ( ( $i / $MoveRequestCount ) * 100 )
	$MoveRequest.TargetDB = ( $TargetDBs | Where-Object { $_.Name -like ( $MoveRequest.DataCenter + "*" ) } | Sort-Object ProjectedSize  )[0].Name
	( $TargetDBs | Where-Object { $_.Name -like ( $MoveRequest.DataCenter + "*" ) } | Sort-Object ProjectedSize  )[0].ProjectedSize += $MoveRequest.MBXSize.Value
	( $TargetDBs | Where-Object { $_.Name -like ( $MoveRequest.DataCenter + "*" ) } | Sort-Object ProjectedSize  )[0].MailboxCount++
	$i++
}
Write-Progress -Activity "Determining Mailbox Placement" -Status "Completed" -Completed

if ( $CreateMoveRequests )
{
	$i = 1 # Counter
	ForEach ( $Move in $MoveRequests )
	{
		Write-Progress -Activity "Creating Move Requests" -Status ( "Creating Move Request [" + $i + "/" + $MoveRequestCount + "]" ) -CurrentOperation ( "Creating Move Request for " + $Move.DisplayName ) -PercentComplete ( ( $i / $MoveRequestCount ) * 100 )
		( $Move.MBXIdentity ) | New-MoveRequest -BatchName ( $Move.TargetDB ) -TargetDatabase ( $Move.TargetDB ) -SuspendWhenReadyToComplete
		$i++
	}
	Write-Progress -Activity "Creating Move Requests" -Status "Completed" -Completed
}
else
{
	# Output the Mailbox Move Information if we're not going to create the requests
	$MoveRequests
}

# Output the Target Database Information
$TargetDBs

Download the complete script here (Create-AllUsersMoveRequests.ps1).

There are also a few “follow-up” scripts that I’ve written.

One script checks for any moves that have failed due to failed items in the mailbox, increases the “BadItemLimit” to 10 and resubmits the move request.  If any of those fail, then the same is done, but the “BadItemLimit” is raised to 100 items.  Then I just increase by powers of 10.  I can’t imagine any mailbox that has more than 10,000 items that are all corrupt, but I’ve seen odder things.

There is also a script that runs nightly at a specified time that resumes the Suspended Moves that completed during the previous day.  There is a last one that runs just before the business day begins that clears out any completed mailbox moves.

There will be more rambling shortly about the move process.

Ramble On!

–Kevin

1 thought on “Mass Migration of Exchange 2007 to Exchange 2010 Users”

  1. Hi Kevin, this is a great script, thanks for posting. Do you have the follow-up scripts posted somewhere?

    How do you take care of balancing database sizes/user numbers after you are in production? Do you have script for that as well?

    Greg

    Reply

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.