Thursday, June 26, 2014

Powershell: Scripted Exchange Offline Defrag

I like most other human beings in this world, enjoy my sleep.  So staying up till a late hour to run what could be a time consuming but basic task doesn’t sit in my fun basket.

This script is very handy to schedule and forget, if you have some monitoring software such as Labtech or Kaseya – you can look for the custom event log errors and alert if something didn’t go to plan (such as the store not starting again), well – let’s dissect the script and dig in!


Variables


As per most of my scripts now, I like to run my own little event log catalogue so any alerts are tracked and can be monitored, feel free to use this as a template – simply change the source name to something that works for you and document your own database of logs and codes.

#------- Assign Variables -------#
$Devices = gwmi -class Win32_LogicalDisk | where {$_.Description -like 'Local Fixed Disk' } | sort {$_.FreeSpace }
$TmpDrive = $Devices[$Devices.count - 1]
$ComputerName = hostname
#------- ######## -------#



#------- Event Log Variables -------#
$EventLog = New-Object System.Diagnostics.EventLog('Application')
$EventLog.MachineName = "."
$EventLog.Source = "Otori Logs"

function RaiseEvent {
Param([string]$Msg, [int]$EventLevel, [int]$EventID)

switch ($EventLevel){
0 { $el = "information" }
1 { $el = "warning" }
2 { $el = "error" }

default { $el = 'information' }
}
$EventLog.WriteEntry($Msg, [string]$el, $EventID)
}
#------- ######## -------#




Get Exchange Information



Now we’ll dig into active directory to obtain the Exchange Database information. This is then stored into an object and used later when we process each database.

#------- Search AD for Exchange Info -------#
# This will create a connection to LDAP
$Root = New-Object System.DirectoryServices.DirectoryEntry("LDAP://RootDSE")
$ConfigPartition = [adsi]("LDAP:// CN=Microsoft Exchange,CN=Services,"+$Root.configurationNamingContext)
$Searcher = New-Object System.DirectoryServices.DirectorySearcher($ConfigPartition)

# This will create a search in AD for Exchange Server Details
$Searcher.filter = ' (objectclass=msExchExchangeServer)'
$ExchDetails = $Searcher.FindAll() | where {$_.properties.name -like $Computername}
$ExchBin = [string]$ExchDetails.properties.msexchinstallpath+"\bin"

# This will create an array of private store databases
$Searcher.filter = ' (objectclass=msExchPrivateMDB)'
$PrivMDBs = $Searcher.FindAll() | where {$_.properties.msexchowningserver -like $ExchDetails.properties.distinguishedname}

# This will create an array of public store databases
$Searcher.filter = ' (objectclass=msExchPublicMDB)'
$PubMDBs = $Searcher.FindAll() | where {$_.properties.msexchowningserver -like $ExchDetails.properties.distinguishedname}
#------- ######## -------#




Database Processing Function



This function will do a few things, first we determine if there is a drive with enough free space to do the defrag (plus a little buffer room).  If a drive doesn’t exist with enough free space, it will alert and not process the database.  If however it’s all green lights, it will call eseutil and perform the offline defrag.

#------- Define Function for processing databases -------#
function ProcessDBs([object]$ExchDBs){
# Perform ESEUTIL on the databases if there is enough free space, if not send alert
$buffer = 1024000
foreach ($db in $ExchDBs){
$DBFile = $db.properties.msexchedbfile
$DBSize = (get-item $db.properties.msexchedbfile).length
$FreeSpace = $TmpDrive.FreeSpace + $buffer
$TMPDatabase = [string]$TmpDrive.DeviceID + "\tmp.edb"

if ( $DBSize -gt $FreeSpace ) {
#Notify there is not enough space to continue
$EventMessage = $Computername + " has attempted an Exchange offline defrag however it does not have enough space. " `
+ $TmpDrive.DeviceID + " has only " + $FreeSpace + " bytes free, however it requires " + $TmpDrive.FreeSpace `
+ " as the database size is " + $DBSize + " bytes. `n `n" `
+ "Offline Defrag has not occured for database located at " + $DBFile + "."

RaiseEvent $EventMessage 1 103
}else {
#Perform the offline defrag with eseutil and log progress
RaiseEvent ("Begin defrag of database located at " + $DBFile) 0 103
& $ExchBin\eseutil.exe /d $DBFile /t $TMPDatabase
RaiseEvent ("Complete defrag of database located at " + $DBFile) 0 103
}
}
}
#------- ######## -------#




Run the Process



Here the core section of the script is executed. First the Information Store must be stopped, then the databases are processed and finally the service started again.

#------- Main -------#
# Stop Exchange Services
RaiseEvent "Stopping Exchange Store" 0 103
stop-service msexchangeis
start-sleep -m 3
$MSExchangeIS = get-service msexchangeis
if ($MSExchangeIS.status -ne "Stopped"){
RaiseEvent ("Unable to stop the Exchange store") 2 103
exit
}


# Process each database on the server
ProcessDBs($PrivMDBs)
ProcessDBs($PubMDBs)

# Start Exchange Services
RaiseEvent "Starting Exchange Store" 0 103
start-service msexchangeis
start-sleep -m 3
$MSExchangeIS = get-service msexchangeis
if ($MSExchangeIS.status -ne "Stopped"){
RaiseEvent ("Exchange store has not started") 2 103
exit
}
#------- ######## -------#