<# Script Name: Migrate-TerminatedUsers.ps1 Created By: Kevin Sparenberg Last Modified: 2012-02-13 Overview: CustomAttribute4 contains the word "TERM" and the termination date (if possible) of a user Mailboxes are flagged as disabled when the user is terminated Terminated User Mailbox Servers exist in each data center and contain the word "TERM" in the server name Only move mailboxes which have passed the termination date (if we have it), where the user account is disabled, the database drive has sufficient storage, the log drive has sufficient storage, and the AD site matches that, for the office Requirements: This must be run on an Exchange 2007 Server. It does NOT need to be executed on a mailbox server. #> #region Change Display Characteristics # Change the Display to something a little more "informative" $OriginalTitle = ( Get-Host ).UI.RawUI.WindowTitle ( Get-Host ).UI.RawUI.WindowTitle = "Migrate Terminated User Mailboxes" ( Get-Host ).UI.RawUI.ForegroundColor = "White" ( Get-Host ).UI.RawUI.BackgroundColor = "Black" Clear-Host #endregion #region Load Exchange Snapins # Load all Exchange Snapins. If they are already loaded, then an error is # thrown, so we suppress that. Add-PSSnapin Microsoft.Exchange.* -ErrorAction SilentlyContinue #endregion #region Define Variables for Run # Define Any Variables Here that are used globally $WhatIf = $true # General "Debug" Variable $BadItemLimit = 10000 # Number of Bad Items to Allow $PercentFreeBufferEDB = 0.75 # Minimum Percent of Free Space that must exist on the Database Drive $MinFreeBufferEDB = 25GB # Minimum amount of disk space that must exist on the Database Drive $PercentFreeBufferLog = 0.75 # Minimum Percent of Free Space that must exist on the Log Drive $MinFreeBufferLog = 5GB # Minimum amount of disk space that must exist on the Log Drive $LogFolder = "D:\Scripts\Logs" # Location for Output Logs $LocalADSite = ( Get-ExchangeServer $env:COMPUTERNAME ).Site.Name # Local Active Directory Site #endregion #region Office-to-Data Center Hash Table Write-Host "Building Office to Data Center Hash" -ForegroundColor Yellow # Build a Hash Table that Correlates an "Office" to a specific Data Center's AD Site $DataCenterBinding = @{ ` "Atlanta" = "EAST-DataCenter"; "Austin" = "WEST-DataCenter"; "Baltimore" = "EAST-DataCenter"; "Boston" = "EAST-DataCenter"; "Chicago" = "EAST-DataCenter"; "Dallas" = "WEST-DataCenter"; "Los Angeles" = "WEST-DataCenter"; "New York" = "EAST-DataCenter"; "Philadelphia" = "EAST-DataCenter"; "Phoenix" = "WEST-DataCenter"; "Raleigh" = "EAST-DataCenter"; "Sacramento" = "WEST-DataCenter"; "San Diego" = "WEST-DataCenter"; "San Francisco" = "WEST-DataCenter"; "Seattle" = "WEST-DataCenter"; "Tampa" = "EAST-DataCenter"; "Washington DC" = "EAST-DataCenter" } Write-Host ( "`t" + "Hash Built with " + $DataCenterBinding.Count + " entries." ) -ForegroundColor Yellow #endregion #region Get Available Databases & Add ScriptProperties to Database Objects to include EDB File Size and Disk Free Space Write-Host "Collecting Available Terminated User Database Information" -ForegroundColor Yellow # Additional Script Properties that get information on the Mailbox Database Objects - to be added to the Mailbox Database Object $EDBFileSizeScriptProp = { ( Get-ItemProperty -Path ( "\\" + $this.ServerName + "\" + $this.EDBFilePath.PathName.Replace(":", "$") ) -Name Length ).Length } $EDBDiskFreeScriptProp = { ( Get-WmiObject -ComputerName ( $this.ServerName ) -Query ( "SELECT FreeSpace From Win32_LogicalDisk Where DeviceID = '" + $this.EdbFilePath.DriveName + "'" ) ).FreeSpace } $LogPointCheckScriptProp = { ( ( Get-ItemProperty -Path ( "\\" + $this.ServerName + "\" + ( ( Get-StorageGroup $this.StorageGroup ).LogFolderPath.PathName ).Replace(":", "$") ) -Name Attributes ).Attributes -like "*ReparsePoint*" ) } $LogDiskFreeScriptProp = { ( Get-WMIObject -ComputerName ( $this.ServerName ) -Query ( "SELECT FreeSpace FROM Win32_Volume WHERE Caption = '" + ( ( ( Get-StorageGroup ( $this.StorageGroup ) ).LogFolderPath.PathName ).Replace("\", "\\") ) + "\\'" ) ).FreeSpace } # Get Servers in the Local AD Site, which are Mailbox Servers, and the name is like *TERM*... (indicating that they are for terminated user mailboxes) # then get databases on that server which are mounted and a backup is *not* running. $TermDatabases = Get-ExchangeServer | Where-Object { ( $_.Site.Name -eq $LocalADSite ) -and ( $_.IsMailboxServer ) -and ( $_.Name -like "*TERM*" ) } | Get-MailboxDatabase -Status | Where-Object { ( $_.Mounted ) -and ( -not ( $_.BackupInProgress ) ) } Write-Host ( "`t" + $TermDatabases.Count + " applicable mailbox databases found." ) -ForegroundColor Yellow # Add Script Properties to get the disk space and disk free spaces $TermDatabases | Add-Member -MemberType ScriptProperty -Name EDBFileSize -Value $EDBFileSizeScriptProp $TermDatabases | Add-Member -MemberType ScriptProperty -Name EDBDiskFree -Value $EDBDiskFreeScriptProp $TermDatabases | Add-Member -MemberType ScriptProperty -Name LogsOnMountPoint -Value $LogPointCheckScriptProp $TermDatabases | Add-Member -MemberType ScriptProperty -Name LogDiskFree -Value $LogDiskFreeScriptProp #endregion #region Get mailboxes and add mailbox size to the object Write-Host "Collecting Terminated User Mailbox Information" -ForegroundColor Yellow $DateRegex = [regex]"\d{4}-\d{2}-\d{2}" # Used to Parse CustomAttribute4 for the Termination Date $MailboxSizeScriptProp = { ( Get-MailboxStatistics $this ).TotalItemSize.Value.ToBytes() } # Used to return the mailbox size $MailboxItemScriptProp = { ( Get-MailboxStatistics $this ).ItemCount } # Used to return the mailbox item count $MailboxDataCenterScriptProp = { $DataCenterBinding[ ( ( Get-Mailbox $this ).Office ) ] } # Used to return the user's proper datacenter $TermDateScriptProp = { ( Get-Date ( $DateRegex.Match( $this.CustomAttribute4 ).Value ) -Format d ) } # Used to return the term date as a date $DisabledUserScriptProp = { ( $this.UserAccountControl.ToString() -like "*AccountDisabled*" ) } # Used to return is an account is disabled # Get me all Mailboxes that... # ... have CustomAttribute4 which starts with "TERM" # ... are Exchange 2007 Mailbox Users (2003 users show up as "LegacyMailbox", not "UserMailbox" ) # ... are *not* already on the Terminated Servers (Server name has "TERM" in it) $TermMailboxes = Get-Mailbox -ResultSize Unlimited -Filter { CustomAttribute4 -like "TERM*" } | Where-Object { ( $_.RecipientTypeDetails -eq "UserMailbox" ) -and ( $_.ServerName -notlike "*TERM*" ) } Write-Host ( "`t" + $TermMailboxes.Count + " applicable mailboxes found." ) -ForegroundColor Yellow # Add script properties to the object to get: # the mailbox size, # the mailbox item count, # the data center site, # and the termination date as a date Write-Host ( "`tAdding Script Properties for Size, Item Count, Data Center, Termination Date, and Account Status" ) -ForegroundColor Yellow $TermMailboxes | Add-Member -MemberType ScriptProperty -Name MailboxSize -Value $MailboxSizeScriptProp $TermMailboxes | Add-Member -MemberType ScriptProperty -Name MailboxItem -Value $MailboxItemScriptProp $TermMailboxes | Add-Member -MemberType ScriptProperty -Name DataCenter -Value $MailboxDataCenterScriptProp $TermMailboxes | Add-Member -MemberType ScriptProperty -Name TermDate -Value $TermDateScriptProp $TermMailboxes | Add-Member -MemberType ScriptProperty -Name IsDisabled -Value $DisabledUserScriptProp Write-Host ( "`tSorting mailbox by size, descending (largest mailboxes first)" ) -ForegroundColor Yellow $TermMailboxes = $TermMailboxes | Sort-Object MailboxSize -Descending #endregion #region Main Script # Create Empty Collection for the Report of the Migration Process $ProcessReport = @() $i = $TermMailboxes.Count ForEach ( $Mailbox in $TermMailboxes ) { # Get a list of possible target servers # Requirements: In same AD Site as the computer running the script # Is a mailbox server (has the mailbox role installed # Has the string "TERM" in the name $TargetServers = Get-ExchangeServer | Where-Object { ( $_.Site.Name -eq $LocalADSite ) -and ( $_.IsMailboxServer ) -and ( $_.Name -like "*TERM*" ) } # Get the databases from those servers $TargetDatabases = $TermDatabases | Where-Object { ( $TargetServers.Name -contains $_.ServerName ) -and ( $_.Mounted ) } if ( $WhatIf ) { ( Get-Host ).UI.RawUI.WindowTitle = ( "SIMULATION: Migrate Terminated User Mailboxes (" + $i + " left) [" + $Mailbox.DisplayName + "]" ) $FGColor = "Yellow" $BGColor = "Black" Write-Host ( "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host ( "!!!!! !!!!!" ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host ( "!!!!! Simulation Run !!!!!" ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host ( "!!!!! !!!!!" ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host ( "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" ) -ForegroundColor $FGColor -BackgroundColor $BGColor } Else { ( Get-Host ).UI.RawUI.WindowTitle = ( "Migrate Terminated User Mailboxes (" + $i + " left) [" + $Mailbox.DisplayName + "]" ) $FGColor = "Green" $BGColor = "Black" } if ( $TargetDatabases -ne $null ) # There are databases available on Term Servers { # Determine the Database with the smallest EDB File # This will be the target database $SmallestEDB = $TargetDatabases | Sort-Object EDBDiskFree -Descending | Select-Object -First 1 # Determine the mailbox size and store it $MailboxSizeBytes = $Mailbox.MailboxSize # Get the bytes free on the EDB drive $FreeBytesEDB = $SmallestEDB.EDBDiskFree # Get the bytes free on the log drive $FreeBytesLog = $SmallestEDB.LogDiskFree # Calculate our "safe" minimums based on the percentages defined in the global variables $MinFreeBytesEDB = $FreeBytesEDB * $PercentFreeBufferEDB # If there is less than 5 GB free on the log drive in question, then set the amount free to "0" # This should prevent any issues with running out of log space. $MinFreeBytesLog = $FreeBytesLog * $PercentFreeBufferLog # Get the termination date (as a string... for now) $TerminationDate = $Mailbox.TermDate # If there is no entry for Termination Date then set to an arbitrarily early date if ( -not ( $TerminationDate ) ) { $TerminationDate = "1980-01-01" } # Default the process that all mailboxes will process $ProcessMailbox = $true $ValidationErrors = "" # Default that the mailbox passes all checks - this will be changed for any which fail the checks $DebugMigrationMessageAcct = "Yes" $DebugMigrationMessageEDB = "Yes" $DebugMigrationMessageLog = "Yes" $DebugMigrationMessageDate = "Yes" $DebugMigrationMessageSite = "Yes" # The following checks are "failure" checks. # If the mailbox fails any one check here, it will not be processed. # Check to see if the mailbox account is disabled if ( -not ( $Mailbox.IsDisabled ) ) { # Build the failure message to display in Simulation mode $DebugMigrationMessageAcct = "No`n" $DebugMigrationMessageAcct += "`t The user mailbox account is not disabled" # Buld the failure message to display in standard mode $ValidationErrors += "Account Mailbox is not Disabled`n" # Mark this mailbox to NOT Process $ProcessMailbox = $false } # Check to see if the Mailbox Size is greater than the space available on the Database Disk if ( $MailboxSizeBytes -ge $MinFreeBytesEDB ) { # Build the Failure Message to Display is in Simulation Mode $DebugMigrationMessageEDB = "No`n" $DebugMigrationMessageEDB += "`t" + "{0:N2}" -f ( $MailboxSizeBytes / 1MB ) $DebugMigrationMessageEDB += " MB in Mailbox and " $DebugMigrationMessageEDB += "{0:N2}" -f ( $MinFreeBytesEDB / 1MB ) $DebugMigrationMessageEDB += " MB free on Database Drive" # Buld the failure message to display in standard mode $ValidationErrors += "Insufficient Database Space`n" # Mark this mailbox to NOT Process $ProcessMailbox = $false } # Check to see if the Database Disk is above our minimum free threshold if ( $MinFreeBytesEDB -le $MinFreeBufferEDB ) { # Build the Failure Message to Display is in Simulation Mode $DebugMigrationMessageEDBMin = "No`n" $DebugMigrationMessageEDBMin += "`t" + "{0:N2}" -f ( $MinFreeBufferEDB / 1MB ) $DebugMigrationMessageEDBMin += " MB minimim required Database Disk and " $DebugMigrationMessageEDBMin += "{0:N2}" -f ( $MinFreeBytesEDB / 1MB ) $DebugMigrationMessageEDBMin += " MB free on Database Drive" # Buld the failure message to display in standard mode $ValidationErrors += "Database Disk Below Safe Threshold`n" # Mark this mailbox to NOT Process $ProcessMailbox = $false } # Check to see if the Mailbox Size is greater than the space available on the Log Disk if ( $MailboxSizeBytes -ge $MinFreeBytesLog ) { # Build the Failure Message to Display is in Simulation Mode $DebugMigrationMessageLog = "No`n" $DebugMigrationMessageLog += "`t" + "{0:N2}" -f ( $MailboxSizeBytes / 1MB ) $DebugMigrationMessageLog += " MB in Mailbox and " $DebugMigrationMessageLog += "{0:N2}" -f ( $MinFreeBytesLog / 1MB ) $DebugMigrationMessageLog += " MB free on Log Drive" # Buld the failure message to display in standard mode $ValidationErrors += "Insufficient Log Space`n" # Mark this mailbox to NOT Process $ProcessMailbox = $false } # Check to see if the Log Disk is above our minimum free threshold if ( $MinFreeBytesLog -le $MinFreeBufferLog ) { # Build the Failure Message to Display is in Simulation Mode $DebugMigrationMessageLogMin = "No`n" $DebugMigrationMessageLogMin += "`t" + "{0:N2}" -f ( $MinFreeBufferLog / 1MB ) $DebugMigrationMessageLogMin += " MB minimim required Log Disk and " $DebugMigrationMessageLogMin += "{0:N2}" -f ( $MinFreeBytesLog / 1MB ) $DebugMigrationMessageLogMin += " MB free on Database Drive" # Buld the failure message to display in standard mode $ValidationErrors += "Log Disk Below Safe Threshold`n" # Mark this mailbox to NOT Process $ProcessMailbox = $false } # Check to see if the termination date has passed if ( ( Get-Date ( $TerminationDate ) ) -ge ( Get-Date ) ) { # Build the Failure Message to Display is in Simulation Mode $DebugMigrationMessageDate = "No`n" $DebugMigrationMessageDate += "`tThe termination date of " + $TerminationDate $DebugMigrationMessageDate += " has not yet arrived" # Buld the failure message to display in standard mode $ValidationErrors += "Account has not yet reached termination date`n" # Mark this mailbox to NOT Process $ProcessMailbox = $false } # Check to see if the mailbox is in this AD Site (only processes locally) if ( $Mailbox.DataCenter -ne $LocalADSite ) { # Build the Failure Message to Display is in Simulation Mode $DebugMigrationMessageSite = "No`n" $DebugMigrationMessageSite += "`tThe script is running in the wrong AD Site (" + $LocalADSite + ")" # Buld the failure message to display in standard mode $ValidationErrors += "Mailbox is in the wrong Active Directory Site`n" # Mark this mailbox to NOT Process $ProcessMailbox = $false } # Update the summary message if ( $ProcessMailbox ) { $ValidationErrors += "Summary: Move Mailbox" } Else { $ValidationErrors += "Summary: Do Not Move Mailbox" } # New Object for the Export Process Report $ProcessObject = New-Object -Type PSObject $ProcessObject | Add-Member -MemberType NoteProperty -Name ProcessServer -Value $env:COMPUTERNAME $ProcessObject | Add-Member -MemberType NoteProperty -Name Mailbox -Value $Mailbox.DisplayName $ProcessObject | Add-Member -MemberType NoteProperty -Name Office -Value $Mailbox.Office $ProcessObject | Add-Member -MemberType NoteProperty -Name DataCenter -Value $DataCenterBinding[$Mailbox.Office] $ProcessObject | Add-Member -MemberType NoteProperty -Name AccountStatus -Value $Mailbox.IsDisabled $ProcessObject | Add-Member -MemberType NoteProperty -Name TerminationDate -Value $TerminationDate $ProcessObject | Add-Member -MemberType NoteProperty -Name TargetDatabase -Value $SmallestEDB.Name $ProcessObject | Add-Member -MemberType NoteProperty -Name DatabaseBuffer -Value $PercentFreeBufferEDB $ProcessObject | Add-Member -MemberType NoteProperty -Name LogBuffer -Value $PercentFreeBufferLog $ProcessObject | Add-Member -MemberType NoteProperty -Name DatabaseFreeSpace -Value $SmallestEDB.EDBDiskFree $ProcessObject | Add-Member -MemberType NoteProperty -Name LogFreeSpace -Value $SmallestEDB.LogDiskFree $ProcessObject | Add-Member -MemberType NoteProperty -Name ValidationErrors -Value ( $ValidationErrors.Replace("`n", ", ") ) $ProcessObject | Add-Member -MemberType NoteProperty -Name IsSimulation -Value $WhatIf # Write Summary Block to the Screen Write-Host ( "============================Migrate Mailbox============================" ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host ( "Mailbox: " + ( "{0,53}" -f $Mailbox.DisplayName ) ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host ( "=================================Source================================" ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host ( "Server: " + ( "{0,53}" -f ( $Mailbox.ServerName.ToUpper() ) ) ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host ( "Database: " + ( "{0,53}" -f ( $Mailbox.Database.Name ) ) ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host ( "Termination Date: " + ( "{0,53}" -f ( Get-Date $TerminationDate -Format d ) ) ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host ( "Size: (MB) " + ( "{0,53:N2}" -f ( ( $Mailbox.MailboxSize ) / 1MB ) ) ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host ( "Items: " + ( "{0,53:N0}" -f ( $Mailbox.MailboxItem ) ) ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host ( "==============================Destination==============================" ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host ( "Server: " + ( "{0,53}" -f $SmallestEDB.ServerName ) ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host ( "Database: " + ( "{0,53}" -f $SmallestEDB.Name ) ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host ( "Free Disk Space: (MB)" + ( "{0,50:N2}" -f ( $SmallestEDB.EDBDiskFree / 1MB ) ) ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host ( "Free Log Space: (MB)" + ( "{0,50:N2}" -f ( $SmallestEDB.LogDiskFree / 1MB ) ) ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host ( "================================Process================================" ) -ForegroundColor $FGColor -BackgroundColor $BGColor $Items = $Mailbox.MailboxItem $Size = $Mailbox.MailboxSize # Check to see that the mailbox passed check if ( ( $ProcessMailbox ) ) # Move the Mailbox { $StartTime = Get-Date # Start Date of Processing # Write Additional Line to the Console Write-Host ( "Start Time: " + ( "{0,53}" -f ( Get-Date $StartTime -Format s ) ) ) -ForegroundColor $FGColor -BackgroundColor $BGColor # If we're debugging, then just simulate a move by sleeping for 15 seconds. if ( $WhatIf ) { Write-Host ( "`t!!! Simulate Move with 15 seconds of ""sleep"" !!!" ) -ForegroundColor $FGColor -BackgroundColor $BGColor Start-Sleep -Seconds 15 } Else { $MoveProcess = Move-Mailbox $Mailbox -TargetDatabase $SmallestEDB -Confirm:$false -BadItemLimit:$BadItemLimit -WhatIf:$WhatIf | Out-Null } $EndTime = Get-Date # End Date of Processing $TimeSpan = $EndTime - $StartTime $ProcessObject | Add-Member -MemberType NoteProperty -Name StartTime -Value ( $MoveProcess.StartTime ) $ProcessObject | Add-Member -MemberType NoteProperty -Name EndTime -Value ( $MoveProcess.EndTime ) $ProcessObject | Add-Member -MemberType NoteProperty -Name MoveStatus -Value ( $MoveProcess.StatusMessage ) Write-Host ( "End Time: " + ( "{0,53}" -f ( ( Get-Date $EndTime -Format s ) ) ) ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host ( "Processing Time: " + ( "{0,53}" -f ( $TimeSpan.ToString() ) ) ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host ( "Processing Speed: (MBps)" + ( "{0,47:N2}" -f ( ( $Size / $TimeSpan.Seconds ) / 1MB ) ) ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host ( " (Items/Sec)" + ( "{0,42:N2}" -f ( ( $Items / $TimeSpan.Seconds ) ) ) ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host ( "=======================================================================" ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host "`n" # If we're *not* debugging, then dump the mvoe results to a CSV for later review. if ( -not ( $WhatIf ) ) { $MoveProcess | Export-Csv -Path ( $LogFolder + "\Migrate-TerminatedUsers_" + ( $Mailbox.Alias.ToString() ) + '.csv' ) -NoTypeInformation -Force } } Else # Give the reason why we're not moving the mailbox { Write-Host ( $ValidationErrors ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host ( "=======================================================================" ) -ForegroundColor $FGColor -BackgroundColor $BGColor Write-Host "`n" } # Add the ProcessObject to the ProcessReport collection $ProcessReport += $ProcessObject } Else # There are no avaiable databases for the move { Write-Host "No available databases for the Migration." -ForegroundColor Red } $i-- # decrement mailbox counter } #endregion #region Closing # Write Report of Jobs to Disk # Writes the file name like this: # Migrate-TerminatedUsers_YYYY_MM_DDTHH-mm-ss.csv --> Migrate-TerminatedUsers_2012-02-13T12-25-15.csv $ProcessReport | Export-Csv -Path ( $LogFolder + "\Migrate-TerminatedUsers_" + ( ( Get-Date -Format s ).Replace(":","-") ) + ".csv" ) -NoTypeInformation # Return the Window Title Back to the Original ( Get-Host ).UI.RawUI.WindowTitle = $OriginalTitle #endregion