Thursday, December 26, 2013

My Exchange 2010 Mailbox Move Script

I am coming up to the end of my mailbox moves from Exchange 2007 to Exchange 2010. Over the last year I've worked on this script to help me load balance mailboxes across the databases for each client. Depending on the client, I used various methods for moving and load balancing.

What does the script want?

  1. Identity - Names of each mailbox to move. The script runs a 'get-mailbox' based on what ever you feed it, so it needs to match one of the values it requests. Usually I just run a get-mailbox against whatever data I am provided then feed the script that. 
  2. TargetDatabase - Name of the database(s) to target. You can either list them implicitly or using wildcards. i.e. SAC-DB01,SAC-DB02   or SAC-DB*
  3. DBSelection - This switch is how you want to distribute the mailboxes across your target databases. 
    1. RoundRobin - Default option - divide # of mailboxes by # of target databases, put even number on each. 
    2. LeastFirst - Always find the DB with the least # of mailboxes and move mailboxes there.
    3. FILL - Move up to "FILLMax" value set to each DB, then move to next.
  4. DontAutoSuspendWhenComplete (optional)- By default the script will set the AutoSuspendWhenReadyToComplete switch for each move request. 
  5. BypassDepartmentCheck (optional) - Since each department is getting their own DB when over 250 employees, I use this switch when I mix the smaller departments on a single DB.
  6. ExludedMailboxes (optional) <mailbox name> - Mailboxes to NOT move. If for example, my mailbox selection was all entries on a single DB (get-mailbox -database CCR01-SG1-DB1), I may want to exclude service account mailboxes on that DB. I would list them here.
  7. WhatIf (optional) - doesn't actually schedule move requests, but only shows what would happen if ran.
  8. BatchName (optional) <name> - By default the script will use the department field and schedule all move requests under that batch name. For the HR department, they'd be something like "HR0", "HR1" ... "HR#". This switch lets you specify something different.
Limits:
  • Script will not schedule a request if the mailbox already resides on one of the target databases. 
  • Script will not schedule a request if a move request already exists (in any state). 
  • Script will not schedule a request if using FILL and there are not enough databases with free space.


<#
.SYNOPSIS
   Move Mailboxes to 2010 databases using policy
.DESCRIPTION
   Load balances mailboxes to 2010 dbs based on rules defined by admin. 
   Will not double-move mailboxes going to similar destinations.
   You can specify 3 different move types, roundrobin (evenly to all dbs) Fill250 and Fill350 to put # mailboxes on a DB then move on. 
   
.PARAMETER IDENTITY 
   unique mailbox identity.
.PARAMETER TargetDatabase
   Name(s) of target databases for moves to. 
.PARAMETER ExcludedMailboxes
   Mailboxes to avoid moving. i.e. selected OU, but don't want to move BESAdmin mailbox.  
.PARAMETER DBSelectionMethod 
   How DBs are selected for moves:
   RoundRobin - Mailboxes go to DBS 0..N in order.
   FILL - Move FILLMAX mailboxes to DB, then go to next db.
   Fill250 - Move 250 mailboxes to DB, then go to next DB
   Fill350 - Move 350 mailboxes to DB, then go to next DB
   LeastFirst - Move mailboxes to lowest population DB first.
.PARAMETER WhatIf
   Switch - Show sample MOVE without actually scheduling moves.
.PARAMETER BatchName
   Specify a BatchName for the moves. Otherwise, it will generate one based on the mailbox department field and increment by 1 (SALES0, SALES1) each time script ran. 
.PARAMETER DontAutoSuspendWhenComplete
 By default the script will set the SuspendWhenComplete flag to true. Adding this switch will disable that.
.PARAMETER BypassDeptCheck
 If existing mailboxes reside in the targetDB location, the script will compare the department fields and not move mailboxes if they don't match. If you enable this switch, it will bypass that check.
.EXAMPLE All mailboxes on SG1-DB1 will be moved to the 4 DBs selected evenly. Excludes BESAdmin mailbox.
 $moves = Get-Mailbox -database Server1\SG1\DB1 
   .\MV-MBX2010.ps1 -identity $moves -targetDatabase DB1,DB2,DB3,DB4 -DBSelectionMethod RoundRobin -excludedmailboxes Besadmin@example.com 
.EXAMPLE All Mailboxes in distribution group will be moved to DBs filling up to 250. Doesn't take into account existing Move Requests pending.
   $moves = Get-distributiongroupMember "Sales Department"
   .\MV-MBX2010.ps1 -identity $moves -targetDatabase DB1,DB2,DB3,DB4 -DBSelectionMethod Fill250
.EXAMPLE All Mailboxes defined in CSV will be moved to any Database without suspending at 99%.
 $moves = import-CSV "NewMailboxMoves.CSV" | %{get-mailbox $_.primarysmtpaddress}
 .\MV-MBX2010.PS1 -identity $moves -targetdatabase DB* -DBSelectioMethod RoundRobin -DontAutoSuspendWhenComplete
#>

[CmdLetBinding()]
param(
 [parameter(Position=0,Mandatory=$true,ValueFromPipelineByPropertyName=$true,HelpMessage="List of mailboxes to move")]
 [string[]]$Identity,
 [parameter(Position=1,Mandatory=$true,HelpMessage="Names of the DBs to move mailboxes to. Can use wildcards.")]
 [string[]]$TargetDatabase,
 [parameter(Position=2,HelpMessage="Valid entries: RoundRobin, FILL, LEASTFIRST")]
 [string]$DBSelectionMethod,
 [parameter(HelpMessage="Combine with DBSelection:FILL to set fill limits")]
 [int]$FillMax,
 [parameter(HelpMessage="Identifier of mailboxes to NOT move")] 
 [string[]]$ExcludedMailboxes,
 [parameter(HelpMessage="Show how moves would proceed, but don't actually perform moves")]
 [switch]$Whatif,
 [parameter(HelpMessage="Specify a BatchName. Otherwise automatically assigned via mailbox department field.")] 
 [string]$BatchName,
 [parameter(HelpMessage="Perform mailbox moves even if department fields don't match mailboxes already on DB.")] 
 [switch]$byPassDeptCheck,
 [parameter(HelpMessage="Set script to not suspend when ready to complete")] 
 [switch]$DontAutoSuspendWhenComplete,
 [parameter(HelpMessage="Don't retry missed mailboxes.")] 
 [switch]$DontRetry
)

if ($DBSelectionMethod.toupper() -eq "FILL" -and $FillMax -eq 0) {
 Write-Host "ERROR: Please include -FILLMAX when using the DBSelectionMethod of FILL" -ForegroundColor red
 break
}

$testRun = $Whatif.IsPresent
if ($testRun) {
 Write-Host "TEST RUN" -ForegroundColor YELLOW 
}

Write-Host "DBSelectionMethod: $DBSelectionMethod" -ForegroundColor yellow
if ($FillMax -ne 0) {Write-Host "FillMAX: $FILLMAX" -ForegroundColor yellow}
$byPassDept = $ByPassdeptCheck.isPresent
if ($bypassdept) {Write-Host "ByPassing Department Match requirement" -ForegroundColor YELLOW }
$FinalizeAtOnce = $DontAutoSuspendWhenComplete.ispresent
if ($FinalizeAtOnce) {Write-Host "Not using 'autosuspendwhencomplete' switch." -ForegroundColor YELLOW }
Write-Host "Mailboxes in source: $($identity.count)"


write-host "reading DBs in $targetdatabase..." -NoNewline
$dbs = $TargetDatabase | %{Get-MailboxDatabase $_}
Write-Host "$($dbs.count) DBs selected" 


write-verbose "Reviewing mbxes to see who is already at destination db"
#Read entire group
$id = $identity | %{get-mailbox -identity $_ -ErrorAction silentlyCOntinue}
#eliminate those already on target db, or have moverequest outstanding.
$movegroup = $id | ?{(($DBS -like $_.database.name) -eq $false) -or (($DBS -like $_.Database.Name).count -eq 0) -and ((Get-MoveRequest -Identity $_.identity -erroraction SilentlyContinue) -eq $null)}

if ($movegroup -eq $null) {
 Write-Host -ForegroundColor RED "No Mailboxes selected to move."
 break
}

$comparison = compare $movegroup $id
if ($comparison -ne $null) {
 Write-Host "These mailboxes will not be moved."
 if ($comparison.count -le 40) {
  $comparison | select @{Name="display";expression={$_.inputobject.displayname}},@{name="DB";expression={$_.inputobject.database}}
 } else {
  Write-Host "$($comparison.count) mailboxes not to be moved now"
 }
}

#Return # of mailboxes per DB. 
function Get-MDBMailboxCount ([string]$DN) {
 $Searcher = New-Object System.DirectoryServices.DirectorySearcher
 $Searcher.SearchRoot = New-Object System.DirectoryServices.DirectoryEntry ("LDAP://$(([system.directoryservices.activedirectory.domain]::GetCurrentDomain()).Name)")
 $Searcher.Filter = "(&(objectClass=user)(homeMDB=$DN))"
 $Searcher.PageSize = 10000
 $Searcher.SearchScope = "Subtree"
 $results = $Searcher.FindAll()
 $returnValue = $results.Count
 #dispose of the search and results properly to avoid a memory leak
 $Searcher.Dispose()
 $results.Dispose()
 return $returnValue
}

#Write-Host "DB Selection Method: $DBSelectionMethod"
Write-Host "filtering DBS based on criteria"

Switch ($DBSelectionMethod.toupper()) {
 "FILL250" { 
  $DBSize = @{}
  $avail = 0
  $maxDBSize = 250
  #get # of existing mailboxes.
  $dbs | %{ $dbsize[$_.name] = (Get-MDBMailboxCount $_.distinguishedname)}
  #get # of incomplete move requests.
  $dbs | %{ $dbsize[$_.name] += (Get-moverequest -TargetDatabase $_.name -ResultSize Unlimited | ?{$_.status -notlike "complet*"}).count }
  #Calculate # of mailboxes that can go to these destinations and not exceed balancing.
  $dbs = $dbs | ?{$dbsize[$_.name] -lt $maxDBSize}
  $dbs | %{$avail += $maxDBSize - $dbsize[$_.name]}
 } 
 "FILL350" {
  $avail = 0 
  $DBSize = @{}
  $maxDBSize = 350
  #get # of existing mailboxes.
  $dbs | %{ $dbsize[$_.name] = (Get-MDBMailboxCount $_.distinguishedname)}
  #get # of incomplete move requests.
  $dbs | %{ $dbsize[$_.name] += (Get-moverequest -TargetDatabase $_.name -ResultSize Unlimited | ?{$_.status -notlike "complet*"}).count }
  #Calculate # of mailboxes that can go to these destinations and not exceed balancing.
  $dbs = $dbs | ?{$dbsize[$_.name] -lt $maxDBSize}
  $dbs | %{$avail += $maxDBSize - $dbsize[$_.name]}
 } 
 "FILL" {
  $avail = 0 
  $DBSize = @{}
  $maxDBSize = $FillMax
  #get # of existing mailboxes.
  $dbs | %{ $dbsize[$_.name] = (Get-MDBMailboxCount $_.distinguishedname)}
  #get # of incomplete move requests.
  $dbs | %{ $dbsize[$_.name] += (Get-moverequest -TargetDatabase $_.name -ResultSize Unlimited | ?{$_.status -notlike "complet*"}).count }
  #Calculate # of mailboxes that can go to these destinations and not exceed balancing.
  $dbs = $dbs | ?{$dbsize[$_.name] -lt $maxDBSize}
  $dbs | %{$avail += $maxDBSize - $dbsize[$_.name]}
 } 

 "LEASTFIRST" {
  $avail = 0 
  $DBSize = @{}
  #get # of existing mailboxes.
  $dbs | %{ $dbsize[$_.name] = (Get-MDBMailboxCount $_.distinguishedname)}
  #get # of incomplete move requests.
  $dbs | %{ $dbsize[$_.name] += (Get-moverequest -TargetDatabase $_.name -ResultSize Unlimited | ?{$_.status -notlike "complet*"}).count }
 } 
 default {
  $DBSelectionMethod = "RoundRobin" 
  $DBSize = @{}
  Write-Host "resorting DBs to be least to greatest load" 
  $dbs = $dbs | select name,@{name="MbxCount";Expression={Get-MDBMailboxCount $_.distinguishedname}}| sort MBXCount | %{Get-MailboxDatabase $_.name}
  $dbs | %{ $dbsize[$_.name] += (Get-moverequest -TargetDatabase $_.name -ResultSize Unlimited | ?{$_.status -notlike "complet*"}).count }
 }
}

#$dbs | select name,@{name="MbxCount";Expression={Get-MDBMailboxCount $_.distinguishedname}}| sort MBXCount

$dbs | %{write-host $_.name,$dbsize[$_.name]}
if ($DBSelectionMethod.toupper() -like "FILL*" -and $movegroup.count -gt $avail) {
 Write-Host "Not enough free space based on DB Selection Method" -ForegroundColor red
 Write-Host "Mailboxes Selected:" $($moveGroup.count)
 Write-Host "Available Space in DBs:" $avail
 Switch ($DBSelectionMethod.toupper()) {
  "FILL250" { 
   $a = ($moveGroup.count - $avail) / 250
  }
  "FILL350" { 
   $a = ($moveGroup.count - $avail) / 350
  }
  "FILL" { 
   $a = ($moveGroup.count - $avail) / $FILLMAX
  }
 }
 $at = [int]$a
 if ($at -gt $a) {$a++}
 write-host "please create $([int]$a) more databases"
 break
}


Write-Host "Checking to see if DEPT on destination matches destination of source mailboxes"
$dbDeptgrp = $dbs | %{get-mailboxdatabase $_ | Get-Mailbox -resultsize 25 -warningaction silentlycontinue | get-user } | group department | sort count -Descending
if ($dbDeptgrp -is [array]) {$dbDept = $dbDeptgrp[0].name} else {$dbDept = $dbDeptgrp.Name}


if ($ExcludedMailboxes -ne $null) {
 $excludes = $ExcludedMailboxes | %{Get-Mailbox $_ -erroraction silentlycontinue}
 $excludes | %{Write-Host "Excluded:",$_.DisplayName}
} else {$excludes = $null}

#reviewing DEPT - Destination mailboxes and Source Mailboxes. 
$moveDeptGrp = $movegroup | Get-User | group department
if ($moveDeptGrp -is [array]) {$moveDept = $moveDeptGrp[0].name} else {$moveDept = $moveDeptGrp.Name}

if ($moveDept -ne $dbDept -and $dbDept -ne $null ) {
 Write-Host "ERROR:Target DB ($dbDept) dept <> mailbox department ($MoveDept)" -ForegroundColor RED
 if (!$bypassdept) { break} 
}

#if BatchName not defined in cmdline, assign batchname based on off DEPT field. 
if ($BatchName -eq "") {
 $BatchIndex = 0
 $FindBatch = Get-MoveRequest -BatchName ($moveDept + [string]$BatchIndex) -resultsize 1 -WarningAction SilentlyContinue
 While ( $FindBatch -ne $null) {
  $BatchIndex++
  $FindBatch = Get-MoveRequest -BatchName ($moveDept + [string]$BatchIndex) -ResultSize 1 -WarningAction SilentlyContinue
 }
 $BatchName = ($moveDept + [string]$BatchIndex)
}
Write-Host "BatchName: $BatchName"

if ($DBSelectionMethod.toupper() -eq "ROUNDROBIN") {
 #Filter out moves already in action.
 $Movegroup = $movegroup | ?{!(Get-MoveRequest -Identity $_.displayname -erroraction silentlycontinue)}

 if ($dbs -is [array]) {$dbc = $dbs.count} else {$dbc = 1}
 if ($movegroup -is [array]) {$Mbc = $movegroup.Count} else {$mbc = 1}
 Write-host "Found $MBC Mailboxes in source group"
 if ($mbc -lt $dbc) {
  $MbxPerDB = 1
 } else {
  $MbxPerDB = [int] ($mbc / $dbc) 
 }

 #Define groups of mailboxes to move to each DB
 $index = 0
 for ($i = 0; $i -lt $mbc; $i = $i + $mbxPerDB) { 
  $MoveTo = ($i + $mbxPerDB )-1 
  if ($moveTo -gt $mbc) { $moveTo = $mbc} 
  if ($movegroup -is [array]) {
   $moveThese = $movegroup[$i..$moveTo]
  } else {$moveThese = $movegroup}
  if ($dbs -is [array]) {
   $targetDB = $DBS[$index % $dbc]
  } else {
   $targetdb = $dbs
  }
  $index++
  Write-Progress -Activity "Scheduling moves ($batchName)" -Status "$mbxPerDB to $targetDB" -PercentComplete (($i / $mbc)*100)
  if (!$testrun) {
   if ($FinalizeAtOnce) {
    $moveThese | New-MoveRequest -TargetDatabase $TargetDB -baditem 5 -batchname $BatchName -WarningAction SilentlyContinue
   } else {
    $moveThese | New-MoveRequest -TargetDatabase $TargetDB -baditem 5 -batchname $BatchName -WarningAction SilentlyContinue -SuspendWhenReadyToComplete 
   } 
  } else {
   $MoveCount = $moveThese.count
   Write-Host "WhatIF: $MoveCount to $targetDB"
  }
 }
} else {
 if ($dbs -is [array]) {$dbc = $dbs.count} else {$dbc = 1}
 if ($movegroup -is [array]) {$Mbc = $movegroup.Count} else {$mbc = 1}
 Write-host "Found $MBC Mailboxes in source group"
 ForEach ($Mbx in $movegroup ) { 
  $toBeExcluded = $excludes | ?{$_.Name -eq $Mbx.name}
  if ($toBeExcluded -eq $null) {

   #Checks to see if existing MoveRequest already submitted.
   $alreadyMoving = Get-MoveRequest $mbx.displayname -ErrorAction silentlyContinue

   #This finds currently selected mailbox's location in group. (0..N)
   if ($movegroup -is [array]) { 
    $index = [array]::IndexOf($movegroup, $mbx) 
   } else {
    $index = 0
   }

   #Figure out which DB to move mailbox to... 
   Switch ($DBSelectionMethod.toupper()) {
    "FILL250" {
     $possdb = $DBS | ?{$dbsize[$_.name] -lt 250}
     if ($possdb -is [array]) {
      $targetDB = $possdb[0]
     } elseif ($possdb -eq $null) {
      Write-Host "ERROR:no valid destinations"
      break
     } else {$targetDB = $possdb} 
     $dbsize[$targetdb.name]++
    } 
    "FILL350" {
     $possdb = $DBS | ?{$dbsize[$_.name] -lt 350}
     if ($possdb -is [array]) {
      $targetDB = $possdb[0]
     } elseif ($possdb -eq $null) {
      Write-Host "ERROR:no valid destinations"
      break
     } else {$targetDB = $possdb} 
     $dbsize[$targetdb.name]++
    }
    "FILL" {
     $possdb = $DBS | ?{$dbsize[$_.name] -lt $FillMax }
     if ($possdb -is [array]) {
      $targetDB = $possdb[0]
     } elseif ($possdb -eq $null) {
      Write-Host "ERROR:no valid destinations"
      break
     } else {$targetDB = $possdb} 
     $dbsize[$targetdb.name]++
    }
    "LEASTFIRST" {
     if ($dbs -is [array]) {
      $lowest = $dbs[0] | %{Get-MailboxDatabase $_}
      $dbs | %{Get-MailboxDatabase $_} | %{If ($dbsize[$_.name] -lt $dbsize[$lowest.name]) { $lowest = $_}}
      $targetdb = $lowest
     } else {
      $targetdb = $dbs
     }
     $dbsize[$targetdb.name]++
    }
    default { 
     #Target Database is determined by a modulus of the # of DBs and the current mailbox index. 
     $targetDB = $DBS[$index % $dbc]
    }
   }

   Write-Progress -Activity "Scheduling moves ($batchName)" -Status "$($mbx.displayname) to $targetDB" -PercentComplete (($index / $mbc)*100)

   #Report to screen.
   if (-not $alreadyMoving) { 
    Write-Host "New:" ,$($mbx.displayname), "to $targetdb"
    if (!$testRun) {
     if ($FinalizeAtOnce) {
      New-MoveRequest -Identity $mbx -TargetDatabase $TargetDB -baditem 5 -batchname $BatchName -WarningAction SilentlyContinue
     } else {
      New-MoveRequest -Identity $mbx -TargetDatabase $TargetDB -baditem 5 -batchname $BatchName -WarningAction SilentlyContinue -SuspendWhenReadyToComplete 
     }
    }
   } else {
    write-host "$($alreadyMoving.Status.ToString().ToUpper()): ",$($mbx.displayname) -ForegroundColor Yellow
   }
  } else {
   Write-Host "Excluded:" ,$($mbx.displayname) -ForegroundColor Red
  }
 }
}

if (!$testRun -and !($dontRetry.ispresent)) {
 Write-Host "...checking to see if all mailboxes in move group schedule for move..."
 start-sleep -seconds 60
 $scheduledMoves = Get-MoveRequest -ResultSize unlimited -BatchName $BatchName
 $missed = compare $scheduledmoves $movegroup -Property displayname | ?{$_.sideindicator -eq "=>"}| %{$_.displayname}
 if ($missed.count -gt 0 -and $missed -ne $null) {
  Write-Host ($missed.count) "missed" 
  $OverQuota = $missed | ?{(Get-MailboxStatistics $_).storagelimitstatus -eq "Mailboxdisabled"}
  If ($OverQuota -ne $null) {
   Write-Host "Users are over established mailbox quotas $overquota"
   ForEach ($OQMbx in $OverQuota ) {
    $CurrentSize = (Get-MailboxStatistics -Identity $OQMbx).TotalItemSize.value 
    Set-Mailbox -identity $oqMbx -ProhibitSendReceiveQuota ($CurrentSize + 10MB)
   }
  } 
  if ($FinalizeAtOnce) {
   PS:\Mv-Mbx2010.ps1 -batchname $BatchName -identity $missed -targetdatabase $dbs -dbselectionmethod $DBSelectionMethod -DontAutoSuspendWhenComplete -DontRetry
  } else {
   PS:\Mv-Mbx2010.ps1 -batchname $BatchName -identity $missed -targetdatabase $dbs -dbselectionmethod $DBSelectionMethod -dontretry
  }
 }
}
Write-Host "Batch Name: $batchName"

1 comment:

  1. Hello

    Many thanks for the great script you provided. I've made some changes to the script because of the processing time. Even when I included the parameter -byPassDeptCheck it still checks the department of every users. So...

    I changed:

    Write-Host "Checking to see if DEPT on destination matches destination of source mailboxes"
    $dbDeptgrp = $dbs | %{get-mailboxdatabase $_ | Get-Mailbox -resultsize 25 -warningaction silentlycontinue | get-user } | group department | sort count -Descending
    if ($dbDeptgrp -is [array]) {$dbDept = $dbDeptgrp[0].name} else {$dbDept = $dbDeptgrp.Name}

    To:

    if (!$byPassDept) {
    Write-Host "Checking to see if DEPT on destination matches destination of source mailboxes"
    $dbDeptgrp = $dbs | %{get-mailboxdatabase $_ | Get-Mailbox -resultsize 25 -warningaction silentlycontinue | get-user } | group department | sort count -Descending
    if ($dbDeptgrp -is [array]) {$dbDept = $dbDeptgrp[0].name} else {$dbDept = $dbDeptgrp.Name}

    }

    There where also some issues with the hard coded paths like PS:\Mv-Mbx2010.ps1. It didn't work for me. So I defined the following underneath the param function:

    $global:scriptpath = Split-Path $MyInvocation.MyCommand.Path

    and changed:

    PS:\Mv-Mbx2010.ps1 -batchname $BatchName -identity $missed -targetdatabase $dbs -dbselectionmethod $DBSelectionMethod -DontAutoSuspendWhenComplete -DontRetry

    To:

    & "$global:scriptpath\Mv-Mbx2010.ps1" -batchname $BatchName -identity $missed -targetdatabase $dbs -dbselectionmethod $DBSelectionMethod -DontAutoSuspendWhenComplete -DontRetry

    and changed:

    PS:\Mv-Mbx2010.ps1 -batchname $BatchName -identity $missed -targetdatabase $dbs -dbselectionmethod $DBSelectionMethod -dontretry

    To:

    & "$global:scriptpath\Mv-Mbx2010.ps1" -batchname $BatchName -identity $missed -targetdatabase $dbs -dbselectionmethod $DBSelectionMethod -dontretry

    ReplyDelete