Wednesday, October 26, 2005

Reporting on SMTP Protocol Log Settings and Log Directories on all Exchange Server in a Domain

Apart from message tracking logs one of the most useful things you can have when trying to track down message transfer problems are SMTP protocol logs. This is one thing I usually always enable on any Exchange server I’m managing. One of the things that is missing is a way in which you can manage these files over a long period of time if you leave logging enabled (something like the badmail detection and archiving would be nice actually most of the work is already done its just a matter of adaptation).

So what I’ve decided to do was put together a script that would first enumerate all the SMTP virtual servers on all Exchange servers within a domain and then report on if logging was enabled, what type of logging is being used, how many logfiles are in the logging directory, how much space the log files are taking up and what the oldest log file in the directory is. Because the log settings for a SMTP VS are stored in the metabase the next part of the script uses the ADSI IIS provider to connect to the metabase on the Exchange server and then retrieve information about logging for that Virtual server. To determine first if logging is enabled the Logtype property is checked if the value is 1 then logging has been enabled on the VS if its 0 it hasn’t. The next value that is checked is the logpluginclsid property this property will tell you what type of logging is being used via which clsid is stored in the property. With Exchange this will be one of four types W3C , NCSA, IIS or ODBC. If its ODBC logging then there will be no log files so the script will drop out here. If a log file format is being used the next property that is checked is the logfiledirectory property. By default if you turn on logging and don’t configure the logging directory this property will be null under the VS node and the Logfile directory from the parent node will be used. The next part of the script formats the log file directory into a format that can be used in the rest of the script. I’ve created two versions of this script the first is a fso version which uses the WSH File Scripting Object to connect to the administrative shares on the Exchange Server and then access the logfile directory. The second version is a WMI version which uses WMI to query the CIM_DATAFILE class this will mean the script will still work even if the administrative shares have been disabled for security reasons. The rest of the script then connects to the logging directory via FSO or WMI and then counts the number of log files in the directory and works out how much space they are using and what the oldest file in the directory is. The result is then output to the commandline and also they are written to a CSV file on the c:\ called STMPVSSetting.csv

I’ve put a downloadable copy of the code here the WMI version of the script looks like

lfcount = 0
lfsize = 0
lfoldatenum = ""
Set fso = CreateObject("Scripting.FileSystemObject")
set wfile = fso.opentextfile("c:\SMTPVSSettings.csv",2,true)
wfile.writeline("Servername,Virtual Server Name,Logging Type,Log File Dir,Number
of LogFiles,Space Used(MB),Oldest Log File")
set conn = createobject("ADODB.Connection")
set com = createobject("ADODB.Command")
Set iAdRootDSE = GetObject("LDAP://RootDSE")
strNameingContext = iAdRootDSE.Get("configurationNamingContext")
strDefNamingContext = iAdRootDSE.Get("defaultNamingContext")
Conn.Provider = "ADsDSOObject"
Conn.Open "ADs Provider"
Com.ActiveConnection = Conn
Com.ActiveConnection = Conn
Wscript.echo
Wscript.echo "SMTP Virtual Servers Logfile Setting and Resources"
vsQuery = "<LDAP://" & strNameingContext & ">;(objectCategory=protocolCfgSMTPServer);name,distinguishedName;subtree"
Com.ActiveConnection = Conn
Com.CommandText = vsQuery
Set Rs = Com.Execute
While Not Rs.EOF
strstmsrv = "LDAP://" & rs.fields("distinguishedName")
set svsSmtpserver = getobject(strstmsrv)
wscript.echo
wscript.echo "ServerName:" &
mid(svsSmtpserver.distinguishedName,instr(svsSmtpserver.distinguishedName,"CN=Protocols,")+16,instr(svsSmtpserver.distinguishedName,",CN=Servers")-(instr(svsSmtpserver.distinguishedName,"CN=Protocols,")+16))
call
getSTMPstatus(mid(svsSmtpserver.distinguishedName,instr(svsSmtpserver.distinguishedName,"CN=Protocols,")+16,instr(svsSmtpserver.distinguishedName,",CN=Servers")-(instr(svsSmtpserver.distinguishedName,"CN=Protocols,")+16)),svsSmtpserver.adminDisplayName)
rs.movenext
wend
wfile.close

sub getSTMPstatus(servername,vsname)
Set SMTPVSS = GetObject("IIS://" & Servername & "/SMTPSVC")
for each SMTPVS in SMTPVSS
if SMTPVS.KeyType = "IIsSmtpServer" then
if SMTPVS.ServerComment = vsname then
wscript.echo "SMTP Server : " & SMTPVS.ServerComment
if SMTPVS.logtype = 0 then
wscript.echo "Logging not enabled"
wfile.writeline(servername & "," & SMTPVS.ServerComment & "," & "Logging not
enabled,,,,")
else
select case SMTPVS.logpluginclsid
case "{FF160663-DE82-11CF-BC0A-00AA006111E0}" Wscript.echo "Logging Type : W3C
Extended Log File Format"
lfiles = 1
ltype = "W3C Extended Log File Format"
case "{FF16065B-DE82-11CF-BC0A-00AA006111E0}" Wscript.echo "Logging Type : ODBC
Logging"
wfile.writeline(servername & "," & SMTPVS.ServerComment & "," & "ODBC
Logging,,,,")
case "{FF16065F-DE82-11CF-BC0A-00AA006111E0}" Wscript.echo "Logging Type : NCSA
Log File Format"
lfiles = 1
ltype = "NCSA Log File Format"
case "{FF160657-DE82-11CF-BC0A-00AA006111E0}" Wscript.echo "Logging Type :
Microsoft IIS Log File Format"
lfiles = 1
ltype = "Microsoft IIS Log File Format"
end select
if lfiles = 1 then
if isnull(SMTPVS.logfiledirectory) then
wscript.echo "Log File Directory : " & SMTPVSS.logfiledirectory & "SMTPSVC" &
SMTPVS.name
lfiledir = SMTPVSS.logfiledirectory & "SMTPSVC" & SMTPVS.name
logfileunc = "\" &
mid(SMTPVSS.logfiledirectory,3,len(SMTPVSS.logfiledirectory)-2) & "SMTPSVC" &
SMTPVS.name
else
if mid(SMTPVS.logfiledirectory,len(SMTPVS.logfiledirectory),1) = "\" then
lfiledir = SMTPVS.logfiledirectory & "SMTPSVC" & SMTPVS.name
wscript.echo "Log File Directory : " & SMTPVS.logfiledirectory & "SMTPSVC" &
SMTPVS.name
logfileunc = "\" & mid(SMTPVS.logfiledirectory,3,len(SMTPVS.logfiledirectory)-2)
& "SMTPSVC" & SMTPVS.name
else
lfiledir = SMTPVS.logfiledirectory & "\" & "SMTPSVC" & SMTPVS.name
wscript.echo "Log File Directory : " & SMTPVS.logfiledirectory & "\" & "SMTPSVC"
& SMTPVS.name
logfileunc = "\" & mid(SMTPVS.logfiledirectory,3,len(SMTPVS.logfiledirectory)-2)
& "\" & "SMTPSVC" & SMTPVS.name
end if
end if
drive = left(SMTPVS.logfiledirectory,1) & ":"
lfilepath = replace(logfileunc,"\","\\")
lfilepath = right(lfilepath,(len(lfilepath)-2)) & "\\"
Set objWMIService = GetObject("winmgmts:" _
& "{impersonationLevel=impersonate}!\\" & servername & "\root\cimv2")
Set lfiles = objWMIService.ExecQuery _
("select * from CIM_DataFile where path = """ & lfilepath & """ and extension =
""log"" and drive = """ & drive & """")
for each lfile in lfiles
lfcount = lfcount + 1
lfsize = lfsize + lfile.filesize
if lfcount = 1 then lfolddatenum = cdate(DateSerial(Left(lfile.lastmodified, 4),
Mid(lfile.lastmodified, 5, 2), Mid(lfile.lastmodified, 7, 2)) & " " &
timeserial(Mid(lfile.lastmodified, 9, 2),Mid(lfile.lastmodified, 11,
2),Mid(lfile.lastmodified,13, 2)))
if lfolddatenum > lfile.LastModified then
lfolddatenum = cdate(DateSerial(Left(lfile.lastmodified, 4),
Mid(lfile.lastmodified, 5, 2), Mid(lfile.lastmodified, 7, 2)) & " " &
timeserial(Mid(lfile.lastmodified, 9, 2),Mid(lfile.lastmodified, 11,
2),Mid(lfile.lastmodified,13, 2)))
end if
next
wscript.echo "Number of Log files in Directory : " & lfcount
wscript.echo "Disk Space being used : " & formatnumber(lfsize/1048576,2,0,0,0) &
" MB"
wscript.echo "Oldest Log file in this Directory : " & lfolddatenum
wfile.writeline(servername & "," & SMTPVS.ServerComment & "," & ltype & "," &
lfiledir & "," & lfcount & "," & formatnumber(lfsize/1048576,2,0,0,0) & "," &
lfolddatenum)
lfcount = 0
lfsize = 0
lfolddatenum = ""
ltype = ""
lfiledir = ""

end if
lfiles = 0
end if
end if
end if
next

end sub
 

Thursday, October 20, 2005

Parsing SMTP Protocol log files with Monad

Now that SP2 is out in the wild and I’m starting to look more at what’s happening with SenderID (by the way if you haven't seen this already there is a great post on SenderID on the Exchange Team Blog). I wanted a way I could aggregate the information that is stored in the SMTP protocol logs so I could see for each domain that is sending me email what are the IP address’s of the mail servers and how many emails do have i recieved from each IP (and do this for a time period say the last 1-2 hours). I’ve had the beta of Monad which is the next version of the Windows Shell that will be in Vista (maybe) and E12 (downloadable for here) installed on my machine for a while and this seemed like a good task to take it out for a test drive. The main advantage of Monad from my point of view is being able to get access to all the objects in the .NET framework so this means you can finally get access to hashtables in your scripts (Perl users have had this for years). Hashtables are very versatile objects to use in scripts and are perfect for the sort of aggregation I wanted to do. Adam Barr has posted a very good example of using nested hash-tables that helped a lot with working out how to get this to work.

The script is relatively simple it takes two command-line parameters the first is the directory where the logs file are (can be network drive although be careful if you have really large log files) and the second parameter is the number of hours you want to look back. So the first couple of lines deal with inputting the parameters and next couple looks in the directory for any files that where modified within the time period imputed. The next part of the script does a line by line parse of the log file, the split method is used to break the log file into an array so each element can be processed as needed. Because I’m only interested in Inbound traffic there is an if statement to drop any outbound connections. And because I’m only interested in the “FROM” lines in the log file there’s some further if statements and also finally some code that does a time comparison so only the events within the inputted time period are processed. Because the time used in the log files is in UTC there’s some code that does the UTC conversion (this is a really cool compared to how you do this in VBS). The next part of script basically handles aggregating the domains into one hashtable and then creating a nested hashtable table to handle storing each of the IP address’s that are sending for that domain using a key derived from the IP-address’s and Domain name it also counts the number of email sent from each IP address. The last part to the script handles going back though the hashtable and displaying the data in a hierarchal format.

I’ve put a downloadable copy of the script here the script itself looks like the following

param([String] $LogDirectory = $(throw "Please specify path for a Log for Directory"),
[int32] $timerange = $(throw "Please specify a Time Range in Hours"))
$reqhash1 = @{ }
$Di = New-Object System.IO.DirectoryInfo $LogDirectory
foreach($fs in $Di.GetFileSystemInfos()){
if ($fs.LastWriteTime -gt [DateTime]::get_Now().AddHours(-$timerange) ){
foreach ($line in $(Get-Content $fs.Fullname)){
if ($line.Substring(0,1) -ne "#"){
$larry = $line.split(" ")
if ($larry[3] -ne "OutboundConnectionCommand"){
if ($larry[8] -eq "MAIL"){
$ltime = [System.Convert]::ToDateTime($larry[0] + " " + $larry[1])
if($ltime -gt [DateTime]::get_UtcNow().addhours(-$timerange)){
$femail = $larry[10].Substring($larry[10].IndexOf("<")+1,$larry[10].IndexOf(">")-$larry[10].IndexOf("<")-1)
$fdomain = $femail.Remove(0, $femail.IndexOf("@")+1)
if($reqhash1.ContainsKey($fdomain)){
$hashtabedit = $reqhash1[$fdomain]
if($hashtabedit.ContainsKey($larry[2] + "/" + $fdomain)){
$hashtabedit[$larry[2] + "/" + $fdomain] = $hashtabedit[$larry[2] + "/" + $fdomain] + 1
}
else{
$hashtabedit.Add($larry[2] + "/" + $fdomain,1)
}
}
else{
$reqhash2 = @{ }
$reqhash2.Add($larry[2] + "/" + $fdomain,1)
$reqhash1.Add($fdomain,$reqhash2)
}
}
}
}
}
}
}
}
foreach ($htent in $reqhash1.keys){
$htent
$reqhash2 = $reqhash1[$htent]
foreach ($htent1 in $reqhash2.keys){
" " + $htent1.Substring(0,$htent1.IndexOf("/")) + " " + $reqhash2[$htent1]
}
}

Mail Enabling a Public folder via MAPI and WebDAV

I’ve had a few questions from people recently asking about the different methods you can mail enable a folder when your running Exchange 2003 in Native Mode. The standard method for mail enabling objects programmatically is to use CDOEXM and the IRecipient Interface . To mail enable a public folder you need to combine this with a little CDOEX like this

FolderURL = "http://server/public/folder/"
set objFolder1 = createobject("CDO.Folder")
objFolder1.DataSource.Open FolderURL, , 3,-1
Set objRecip1 =
objFolder1.Getinterface("IMailRecipient")
objRecip1.MailEnable
objFolder1.DataSource.Save

Now because this uses CDOEX which runs over Exoledb it will only work locally on an Exchange server. To do this remotely you can use the Exchange_PublicFolder WMI class I posted a sample of doing the here previously.

I was curious at the way Exchange System Manger went about mail enabling folders when you where using this on a remote machine as this obviously wasn’t using CDOEX. I did some network captures on a remote machine and the only conversation that the remote machine was having with any server when mailenabling a folder was that it was doing a proppatch on two properties on the public folder via webdav. There was no discernable LDAP traffic at all. I decided to give that go in script myself on a non enabled public folder to see if this would mail enable it and it seemed to work just fine and the RUS seemed to be happy enough to populate the email address’s and also deal with the directory object in Active Directory. I tried the same thing with a CDO script that created a new public folder in the root and mail enabled it via setting these two mapi properties and it seemed to work equally as well.

Now wether this method would be a supported way to mail enable public folders I don’t know it does seem to be how ESM is doing it. There’s also a little more discussion in a recent post on the Exchange Team blog and also in the following KB .

The two properties in question are

PR_PUBLISH_IN_ADDRESS_BOOK = &H3FE6000B
PR_PF_PROXY_REQUIRED = &H671F000B

I’ve only tested this on Exchange 2003 in Native mode as I said this is probably unsupported but the WebDAV to do this would look like

server = "server"
folderpath = "folder"
sDestinationURL = "http://" & server & "/public/" & folderpath & "/"
xmlstr = "<?xml version=""1.0"" ?><a:propertyupdate xmlns:a=""DAV:"" xmlns:b=""urn:schemas-microsoft-com:datatypes""
" _
& "xmlns:c=""xml:"" xmlns:d=""http://schemas.microsoft.com/mapi/proptag/"" " _
& "xmlns:e=""http://schemas.microsoft.com/exchange/events/"" xmlns:f=""http://schemas.microsoft.com/exchange/"">
" _
& "<a:set><a:prop>" _
& "<d:0x3FE6000B b:dt=""boolean"">1</d:0x3FE6000B>" _
& "<d:0x671F000B b:dt=""boolean"">1</d:0x671F000B>" _
& "</a:prop></a:set></a:propertyupdate>"
Set XMLreq = CreateObject("Microsoft.xmlhttp")
XMLreq.open "PROPPATCH", sDestinationURL, False
XMLreq.setRequestHeader "Content-Type", "text/xml;"
XMLreq.setRequestHeader "Translate", "f"
XMLreq.setRequestHeader "Content-Length:", Len(xmlstr)
XMLreq.send(xmlstr)
If (XMLreq.Status >= 200 And XMLreq.Status < 300) Then
Wscript.echo "Success! " & "Results = " & XMLreq.Status & ": " &
XMLreq.statusText
ElseIf XMLreq.Status = 401 then
Wscript.echo "You don't have permission to do the job! Please check your
permissions on this item."
Else
Wscript.echo "Request Failed. Results = " & XMLreq.Status & ": " &
XMLreq.statusText
End If


A CDO example would look like.

MailServer = "servername"
Mailbox = "mailbox"
PR_PUBLISH_IN_ADDRESS_BOOK = &H3FE6000B
PR_PF_PROXY_REQUIRED = &H671F000B
set objSession = CreateObject("MAPI.Session")
strProfile = MailServer & vbLf & Mailbox
objSession.Logon "",,, False,, True, strProfile
Set objInfoStores = objSession.InfoStores
set objInfoStore = objSession.GetInfoStore
Set objpubstore = objSession.InfoStores("Public Folders")
Set objTopFolder = objSession.GetFolder(objpubstore.Fields(&H66310102),objpubstore.ID)
Set objFolder = objTopFolder.Folders.Add("YellowCab1")
Set objFields = objFolder.Fields
objfields.add PR_PUBLISH_IN_ADDRESS_BOOK, true
objfields.add PR_PF_PROXY_REQUIRED, true
objFolder.Update

Thursday, October 13, 2005

Reporting on forwarding rules in Mailboxes and Public Folders via a script

Reporting on rules in mailboxes and public folders can get a little challenging for the humble sysadmin. Because most the time the setting of these rules is out of your control knowing where some rules are forwarding can be a little scary (and in some cases can be against company policy or breach privacy laws). There are a few ways to manipulate rules one of the most often used is the rule.dll which can be used to create and enumerate rules while this is useful it still only offers you the hex value of the binary action property which contains the list of the recipient addresses for a forwarding rule. An active rule in a mailbox or public folders is a special message in a mailbox of type IPM.Rule.Message. In CDO 1.2 you can access these messages by using the hidden messages collection on a folder. If a rule is a forwarding rule then the email address’s the rule is forwarding to gets stored in the PR_RuleMsgActions (0x65EF0102) Mapi Property on that IPM.Rule.Message. Unfortunately this property is a binary property whose format is undocumented. Reverse engineering a complete decode for this property is a little hard and time consuming so what I did was just concentrate on matching the patterns within the binary property (its kind of like doing a jigsaw in the dark) but armed with my trusty ASCII chart I came up with some formulas that can separate out the text address’s from the rest of the information that is stored in the property and produce some meaning results (well it works for me anyway).

I’ve created two versions of this script the first loops though all the mailboxes on a server and looks at rules in the inbox of each mailbox. The second script loops though all the public folders on a server and reports on any forwarding rules in those public folders. The results are output to the screen and also a CSV file is created on the c: drive with the results.

The flow of the mailbox script is it takes in the name of the server as a command line parameter and then does a query of active directory for all the visible mailboxes on this server. Its then opens all the mailboxes and checks the hidden collection for any IPM.Rule.Message rule messages and then checks the PR_RuleMsgActions property on any rule messages it finds. If any address objects are found these are output to the command line and also stored in an array so they can be written to a CSV file at the end of the iteration. Once all rules have been checked for a mailbox the result is written in to a CSV file and it moves on to the next mailbox. The public folders script works similarly except instead of querying for mailboxes via Active Directory it uses CDO to loop though the folders collection and any subfolder therein.

To run the mailbox script you need to add the name of the server you want it to run against as a command-line parameter eg cscript mbxruleloop.vbs servername .With the public folders script you need to configure the servername and the name of mailbox on the server you want to report against (this is so the script can logon to the server the actually mailbox content isn’t accessed).

I’ve posted a downloadable copy of the script here the Mailbox script looks like

servername = wscript.arguments(0)
PR_HAS_RULES = &H663A000B
PR_URL_NAME = &H6707001E
PR_CREATOR = &H3FF8001E
Set fso = CreateObject("Scripting.FileSystemObject")
set wfile = fso.opentextfile("c:\mbxforwardingRules.csv",2,true)
wfile.writeline("Mailbox,FolderPath,Creator,AdressObject,SMTPForwdingAddress")
set conn = createobject("ADODB.Connection")
set com = createobject("ADODB.Command")
Set iAdRootDSE = GetObject("LDAP://RootDSE")
strNameingContext = iAdRootDSE.Get("configurationNamingContext")
strDefaultNamingContext = iAdRootDSE.Get("defaultNamingContext")
Conn.Provider = "ADsDSOObject"
Conn.Open "ADs Provider"
svcQuery = "<LDAP://" & strNameingContext & ">;(&(objectCategory=msExchExchangeServer)(cn="
& Servername & "));cn,name,legacyExchangeDN;subtree"
Com.ActiveConnection = Conn
Com.CommandText = svcQuery
Set Rs = Com.Execute
while not rs.eof
GALQueryFilter = "(&(&(&(& (mailnickname=*)(!msExchHideFromAddressLists=TRUE)(|
(&(objectCategory=person) (objectClass=user)(msExchHomeServerName=" &
rs.fields("legacyExchangeDN") & ")) )))))"
strQuery = "<LDAP://" & strDefaultNamingContext & ">;" & GALQueryFilter & ";distinguishedName,mailnickname;subtree"
com.Properties("Page Size") = 100
Com.CommandText = strQuery
Set Rs1 = Com.Execute
while not Rs1.eof
call procmailboxes(servername,rs1.fields("mailnickname"))
wscript.echo rs1.fields("mailnickname")
rs1.movenext
wend
rs.movenext
wend
rs.close
wfile.close
set fso = nothing
set conn = nothing
set com = nothing
wscript.echo "Done"


Sub procmailboxes(servername,mailboxname)

set objSession = CreateObject("MAPI.Session")
objSession.Logon "","",false,true,true,true,servername & vbLF & mailboxname
Set objInfoStores = objSession.InfoStores
set objInfoStore = objSession.GetInfoStore
set Inbox = objSession.Inbox
if inbox.fields.item(PR_HAS_RULES) = true then
Set objMessages = inbox.HiddenMessages
for Each objMessage in objMessages
if objMessage.type = "IPM.Rule.Message" then
call procrule(objMessage,mailboxname,inbox.fields.item(PR_URL_NAME).value)
end if
next
end if

end sub

sub procrule(objmessage,MailboxName,folderpath)
frule = false
splitarry = split(hextotext(objmessage.fields.item(&H65EF0102)),chr(132),-1,1)
if ubound(splitarry) <> 0 then
wscript.echo
wscript.echo "Mailbox Name :" & MailboxName
wscript.echo "Folder Path :" & folderpath
wscript.echo "Rule Created By : " & objmessage.fields.item(PR_CREATOR).value
mbname = MailboxName
fpath = folderpath
creator = objmessage.fields.item(PR_CREATOR).value
frule = true
end if
tfirst = 0
addcount = 1
for i = 0 to ubound(splitarry)
addrrsplit = split(splitarry(i),chr(176),-1,1)
for j = 0 to ubound(addrrsplit)
addrcontsep = chr(3) & "0"
if instr(addrrsplit(j),addrcontsep) then
if tfirst = 1 then addcount = addcount + 1
wscript.echo
wscript.echo "Address Object :" & addcount
redim Preserve resarray(1,1,1,1,1,addcount)
resarray(1,0,0,0,0,addcount) = mbname
resarray(1,1,0,0,0,addcount) = fpath
resarray(1,1,1,0,0,addcount) = creator
if instr(addrrsplit(j),"0/o=") then
resarray(1,1,1,1,0,addcount) = mid(addrrsplit(j),(instr(addrrsplit(j),"0/o=")+1),len(addrrsplit(j)))
WScript.echo "ExchangeDN :" & mid(addrrsplit(j), (instr(addrrsplit(j),"0/o=")+1),len(addrrsplit(j)))
else
WScript.echo "Address :" & mid(addrrsplit(j),3, len(addrrsplit(j)))
resarray(1,1,1,1,0,addcount) = mid(addrrsplit(j),3,len(addrrsplit(j)))
end if
tfirst = 1
end if
smtpsep = Chr(254) & "9"
if instr(addrrsplit(j),smtpsep) then
slen = instr(addrrsplit(j),smtpsep) + 2
elen = instr(addrrsplit(j),chr(3))
Wscript.echo "SMTP Forwarding Address : " & mid(addrrsplit(j),slen,(elen-slen))
resarray(1,1,1,1,1,addcount) = mid(addrrsplit(j),slen,(elen-slen))
end if
next
next
if frule = true then
for r = 1 to ubound(resarray,6)
wfile.writeline(resarray(1,0,0,0,0,r) & "," & resarray(1,1,0,0,0,r) & "," &
resarray(1,1,1,0,0,r) & "," & resarray(1,1,1,1,0,r) & "," &
resarray(1,1,1,1,1,r))
next
end if

end sub


Function hextotext(binprop)
arrnum = len(binprop)/2
redim aout(arrnum)
slen = 1
for i = 1 to arrnum
if CLng("&H" & mid(binprop,slen,2)) <> 0 then
aOut(i) = chr(CLng("&H" & mid(binprop,slen,2)))
mid(binprop,slen,2)))
end if
slen = slen+2
next
hextotext = join(aOUt,"")
end function
 

Wednesday, October 05, 2005

Displaying Details about Transaction Log files for Each Storage Group

Someone asked me about a script that would display details about the log files for each storage group on all the Exchange servers in their domain. While initially puzzled as to why this might be useful after a little thought the idea does bare a little fruit. Note the idea behind this is to look at the file details eg filename, date and size not the content of the file or to in anyway open or lock the files (which would be a bad thing). The general gist of the script is to enumerate the log file directory for each storage group and then count how many log files there are currently for that SG. While your counting them you can also add there sizes together to give you a figure of how much space you log files are taking up and you can also look at what the age of the oldest log file is this can let you know if your backups are working correctly and log files are being purged at the end of the backup cycle. If you start to think a little more laterally on this if where to snapshot this information at intervals during the day it can also become another Performance and usage indicator. Eg if you where to snap the size and number of your log files every hour you get could start to get a picture of the volume of transactions your mail database was processing what times it was busiest etc. Because transactions are more then just receiving and sending new mail this can act as a point of difference from other indicators you might look at (Remember performance monitoring is more then just measuring 1 dimensional metrics). You could even go as far as putting thresholds etc that might jink you to possible problems.

This script is generally based around the database file size script I posted here. I’ve done two versions of the script they both use an ADSI query to retrieve all the storage group objects within a domain and then connect to the servers based on the information that is parsed from some Ad properties on the storage group. The first script uses VBS’s FileSystemObject to connect to the administrative shares on a server and then uses the folder and files collection to read the file attributes. To make sure it only counts files that belong to this particular storage group the msExchESEParamBaseName property is used. This property contains the name prefix used for each storage groups log files. The rest of the script just counts the number of files, adds the size of the files together (and your always going to get something that dividable by 5120 KB if not start panicking). The second version uses WMI to do much the same thing this is useful if you have removed the admin shares on your server. One note on the WMI version is that if you have Daylight saving offsets then the file times might be out by this offset.

If put a downloadable copy of the code here the FSO version looks like

lfcount = 0
lfsize = 0
lfoldatenum = ""
set conn = createobject("ADODB.Connection")
set com = createobject("ADODB.Command")
Set iAdRootDSE = GetObject("LDAP://RootDSE")
strNameingContext = iAdRootDSE.Get("configurationNamingContext")
Conn.Provider = "ADsDSOObject"
Conn.Open "ADs Provider"
sgQuery = "<LDAP://" & strNameingContext & ">;(objectCategory=msExchStorageGroup);
name,distinguishedName,msExchESEParamBaseName,
adminDisplayName,msExchESEParamLogFilePath,msExchEDBFile;subtree"
Com.ActiveConnection = Conn
Com.CommandText = sgQuery
Set Rs = Com.Execute
Wscript.echo "Storeage Groups"
Wscript.echo
While Not Rs.EOF
slen = instr(rs.fields("distinguishedName"),"CN=InformationStore,") + 23
elen = instr(rs.fields("distinguishedName"),"CN=Servers,")-1
Set fso = CreateObject("Scripting.FileSystemObject")
logfileunc = "\\" & mid(rs.fields("distinguishedName"),slen,elen-slen) & "\" &
left(rs.fields("msExchESEParamLogFilePath").value,1) & "$" &
mid(rs.fields("msExchESEParamLogFilePath").value,3,len(rs.fields("msExchESEParamLogFilePath").value)-2)
set lfolder = fso.getfolder(logfileunc)
set lfiles = lfolder.files
for each lfile in lfiles
if left(lfile.name,3) = rs.fields("msExchESEParamBaseName").value and
right(lfile.name,3) = "log" then
lfcount = lfcount + 1
lfsize = lfsize + lfile.size
if lfcount = 1 then lfolddatenum = lfile.DateLastModified
if lfolddatenum > lfile.DateLastModified then
lfolddatenum = lfile.DateLastModified
end if
end if
next
wscript.echo "ServerName : " & mid(rs.fields("distinguishedName"),slen,elen-slen)

wscript.echo "Storage Group Name : " & rs.fields("adminDisplayName")
wscript.echo "Log file Path : " & rs.fields("msExchESEParamLogFilePath").value
wscript.echo "Log file Prefix : " & rs.fields("msExchESEParamBaseName").value
wscript.echo "Number of Log files in Directory : " & lfcount
wscript.echo "Disk Space being used : " & formatnumber(lfsize/1048576,2,0,0,0) &
" MB"
wscript.echo "Oldest Log file in this Directory : " & lfolddatenum
wscript.echo
lfcount = 0
lfsize = 0
lfolddatenum = ""
Rs.MoveNext
Wend
Rs.Close
Conn.Close
set mdbobj = Nothing
set pdbobj = Nothing
Set Rs = Nothing
Set Com = Nothing
Set Conn = Nothing