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
}
#------- ######## -------#

Wednesday, August 3, 2011

SBS Monitoring–Fixing high resource usage

One (of many) bugbears of Windows SBS 2008 is the SBS Monitoring feature which probes the network for WMI information.  This process has been known to chew up a lot of resources at time and also cause huge disk IO.

Without even looking for this fix, I stumbled across a post on the Microsoft Social forums and thought it quite interesting.  I knew of the perfect server to put this on and at the very moment the SQL query finished, the CPU usage had dropped and disk IO from the database resumed to a near idle state.

I can not claim glory for this – but hope to definitely share to others who need.
Run this query if any of the below meets your criteria…

  • High Disk IO – Resource  Monitor reports high read/write for SBSMonitoring.mdf
  • Moderate to high CPU usage on the SQLServr.exe responsible for the SBS Monitoring
  • SBS Console becomes unresponsive or takes a long time to open

Perform this query in SQL Management Studio while connected to the SBSMonitoring instance

USE SBSMONITORING
GO
CREATE NONCLUSTERED INDEX [IDX_Alerts_GetAlertsPerID] ON [dbo].[Alerts]
(
[DefinitionID] ASC,
[ComputerID] ASC,
[DateOccured] ASC,
[IsSet] ASC,
[ID] ASC
)
GO
CREATE NONCLUSTERED INDEX [IDX_WMICollectedData_GetSecurityProductPerComputer] ON [dbo].[WMICollectedData]
(
[WMIInstanceID] ASC,
[WMIPropertyID] ASC,
[DateCollected] ASC,
[ID] ASC
)
GO

create index IDX_WMIObjectProperties_ObjectID on WMIObjectProperties(ObjectID,ID)
Go

create index IDX_WMIObjectInstances_ObjectID on WMIObjectInstances(ObjectID,ID)

Monday, August 30, 2010

VSS: Exclude Files and Folders from Shadow Copy

This very simple little hint can help greatly when dealing with older servers while limited drive space and demanding restore retention. This will only work on Vista upwards (Server 2008 for example)

Quite simply, open the registry key HKLM\System\CurrentControlSet\Control\BackupRestore\FilesNotToSnapshot – there will be a few default entries in there.
Add a new Multi-String Value and name it with a description for the set of files to exclude – for example My Custom Application. Now for the data, add a file or folder path to add to the exclusion list.

A folder can also include all sub-folders with the recursive switch /s, with the example below…

String Name Data
My Application %ProgramData%\MyApp\*
c:\MyApp\database.mdf
c:\MyApp\Sundry\* /s
My Application 2 %ProgramData%\MyApp2\scratch.bin

Thursday, August 19, 2010

VBScript: Deploy Outlook Stationery and Signature

Today I was tasked with scripting a custom and uniform stationery and signature for each user in the domain.  After thinking about this for a little while, I decided the best way to tackle this was to leave the stationery and signature HTML files in tact with a few alterations…

For the HTML files, simply pop them open in an editor (notepad for example – certainly not word!)  and find the variables.  In my example, i used the Full Name, Email Address and Mobile Number as the variables in the stationery and replaced them with %FULLNAME%, %EMAIL% and %MOBILE%.

To distribute the file, choose your preferred method mine is a simple Group Policy with a logon script that copies these files, then calls the VBScript.

The Script

Below is the script, it is fairly easy to understand, shoot me a message if not and I’ll go into more detail afterwards.

'Set Variables (LOOOOOOOTS of Variables)
dim objShell, objSysInfo, strUser, objUser, strName, strMobile, _
strEmail, appdata, strStationery, strSignature, objWord, _
strComputer, strKeyPath, strRegValueName, strRegValue, _
strValue, signature, stationery

set objShell = CreateObject("WScript.Shell")
set objSysInfo = CreateObject("ADSystemInfo")
strUser = objSysInfo.UserName
set objUser = GetObject("LDAP://" & strUser)
strName = objUser.FullName
strMobile = objUser.Mobile
strEmail = objUser.emailAddress
appdata = objShell.ExpandEnvironmentStrings("%appdata%")
strStationery = appdata & "\Microsoft\Stationery\Stationery.html"
strSignature = appdata & "\Microsoft\Signatures\Signature-Reply.htm"
set objWord = CreateObject("Word.Application")
strKeyPath = "SOFTWARE\Microsoft\Office\" & objWord.Version _
& "\Common\MailSettings"
strRegValueName = "NewStationery"
strValue = "Stationery"
objWord.EmailOptions.EmailSignature.NewMessageSignature = ""
objWord.EmailOptions.EmailSignature.ReplyMessageSignature = "Signature-Reply"
'Set Registry to selected stationery
objShell.RegWrite strKeyPath & strRegValueName, strRegValue, "REG_SZ"

'Read source stationery and signature
stationery = GetFile(strStationery)
signature = GetFile(strSignature)

'Write the modified files
WriteFile strStationery, ReplaceTemplate(stationery)
WriteFile strSignature, ReplaceTemplate(signature)

'Read file function
function GetFile(FileName)
If FileName<>"" Then
Dim FS, FileStream
Set FS = CreateObject("Scripting.FileSystemObject")
on error resume Next
Set FileStream = FS.OpenTextFile(FileName)
GetFile = FileStream.ReadAll
End If
End Function

'Write modified files
function WriteFile(FileName, Contents)
Dim OutStream, FS

on error resume Next
Set FS = CreateObject("Scripting.FileSystemObject")
Set OutStream = FS.OpenTextFile(FileName, 2, True)
OutStream.Write Contents
End Function

'Replace blocks in template with user data.
function ReplaceTemplate(TempFile)
TempFile = replace(TempFile, "%FULLNAME%", strName)
TempFile = replace(TempFile, "%EMAIL%", strEmail)

If strMobile = "" Then
TempFile = replace(TempFile, "%MOBILE%", "")
Else TempFile = replace(TempFile, "%MOBILE%", "Mob:" & strMobile)
End If

ReplaceTemplate = TempFile
End Function

Monday, August 16, 2010

Powershell: Force AD Users to Reset Password

There are times when going into ADUC and modifying a bunch of users to have their password change on next logon is either a) too much hassle or b) needs to be a specific time.  By using powershell and Quests ActiveRoles modules, we can create a fairly simple script to do so.

When running this script, there are a few things that can go wrong – for example a user might be set to ‘can not change password’.  So there are a few ways to catch this, my personal favourite is by raising an event log error and having our management software pick up on the event id.

Setting the Variables
First thing we need to do is to set some variables, this script assuming we want to force the reset for all users in a specified OU – this does propagate down to child OU’s as well.

#------- Assign Variables -------#
Add-PSSnapin Quest.ActiveRoles.ADManagement
$OUvar = $args[0]
$OU = Get-QADObject | ? { ($_.Type -eq "organizationalUnit") -and ($_.Name -eq $OUvar) }
if (!$OU) {
write-host "No Organizational Unit with the name of "+$OUvar+" could be found."
break
}else {
$Users = Get-QADUser -SearchRoot $OU.DN
if (!$Users) { write-host "No users found in OU "+$OU.DN+"."
else { write.host "Successfully harvested the following users from OU "+$OU.DN+" `n "+$Users
}
#------- ######## -------#



So basically here we are accepting a command line variable as the OU to find the users in. There is a little error checking here as well to make sure we have some users to apply this too.




Forcing the Change
Here is the real meat and potatoes of this script, it also includes some basic error checking and makes sure that all users that were told to flag were indeed flagged.

#------- Force Password Change On Next Logon -------#
ForEach ($User in $Users) {
Set-QADUser $User -PasswordNeverExpires $false -UserMustChangePassword $true
$UsersVerified = Get-QADUser -SearchRoot $OU.DN | ? { $_.UserMustChangePassword }
}
if ($Users.count -ne $UsersVerified.count) {
$UsersDif = compare $Users $UsersVerified
write-host "Not all users were modified, the following users were not affected. `n "
+$UsersDif.inputobject.logonname
}else { write-host "Users were successfuly set to change password on next logon" }
}
#------- ######## -------#

Friday, August 13, 2010

Powershell: Unlocking an AD User Account

Once again cutting the time for basic administrative tasks, Powershell comes to the rescue!  This below script requires Quests ActiveRoles AD Management script pack which can be freely downloaded from Their Website

The Script

#------- Assign Variables -------#
Add-PSSnapin Quest.ActiveRoles.ADManagement
$UserArg = $args[0]
$User = Get-QADUser | ? {$_.LogonName -eq $UserArg}
#------- ######## -------#


#------- Unlock the User -------#
if (!$User){ write-host "Uh Oh! That user can not be found!" }
else {
Set-QADUser $User -ObjectAttributes @{lockouttime='0'}
if (!$User.AccountIsLockedOut){
write-host "Successfully unlocked user account "+$User.LogonName
}else {
write-host "There was an error resetting the account for "+$User.LogonName+ `
". Account still has lockout period set."
}
}
#------- ######## -------#

Special Notes


This script is designed to run with a command line variable as the username (hence the $args[0]) – so remember to call it correctly.


My production script has some event log functions that raise an event at each stage, and if an error happens (normally human error mis-typing the username!) it can raise an error with a custom Source and EventID which our monitoring software picks up and creates a service ticket.