John Howard - Senior Program Manager in the Hyper-V team at Microsoft

Senior Program Manager, Hyper-V team, Windows Core Operating System Division.

Improved Generic Exchange Backup Script

Improved Generic Exchange Backup Script

  • Comments 7
  • Likes

So after numerous emails about this, and some comments on my previous blog post back in June, I spent a couple of hours this afternoon improving the backup script (which although was written as a generic Exchange backup script, is suitable for file system, system state and more - it just depends entirely what you set the selection criteria as). I've got it running now at home great, so it's worth sharing to the bigger audience now.

The biggest request I had was for email notification if an error occurs. Sending an email is simple (honestly). However, I didn't just want to send any old boring email, I wanted to utilise some of the more advanced features capable in Outlook, and show you how you can use the extended version of CDO for Exchange (CDOEx) rather than CDO. That having been said, to keep it generic and so that this script would run on any Windows Server, I ended up using CDO and SMTP rather than the more advanced capabilities of CDOEx. I've left the CDOEx code in there if you want to go that route, and I have tested it and it does work, so is a good example if you need it. The advanced features are so that the message flag is set to highlight some action is required. It is complicated due to timezone issues, but I think they're pretty well sust, so it should work all the way from Seattle to Sydney.

Here's an example of what I mean by "advanced" - notice that the email is red, and flagged with a follow-up action

Here's the revised script. I have a question though for someone really techy which I just can't find the answer to. I'll post a followup entry shortly. Cut/Paste and save as backup.vbs. Generate your Windows Backup selection file as before, and change the series of constants at the top of the file according to your needs - recipients for error emails, SMTP servers etc. Note that I haven't tested anything except anonymous SMTP drop.

' ************************************************************************************
' * Weekly Backup Script for Exchange
' * John Howard, Microsoft UK. Created 25th June 2005
' * History:
' *  04 Nov 2005 - Added Email Sending on Failure (see inline comments)
' *
' * Feel free to use/modify for your own needs.
' * No guarantees though although it works for me :-)
' * However, if you can do better, contact me through http://blogs.technet.com/jhoward
' ************************************************************************************

Option Explicit
On error resume next
Const NO_ERROR = 0
Const BACKUP_PROGRAM   = "c:\windows\system32\ntbackup.exe "
Const cdoImportance    = "urn:schemas:httpmail:importance"
Const cdoHigh          =  2 ' Importance
Const cdoAnonymous     = 0
Const cdoBasic         = 1
Const cdoNTLM          = 2
Dim   SMTP_AUTH

' TAILOR THESE NEXT CONSTANTS TO YOUR REQUIREMENTS
Const DEFAULT_SENDER   = "Backup Job <servername@contoso.com>"
Const DEFAULT_RECIP    = "user@contoso.com"
Const DEFAULT_SUBJECT  = "Backup Failure on Exchange!"
Const SMTP_SERVER      = "exchange.contoso.com"
Const SMTP_PORT        =  25
      SMTP_AUTH        = cdoAnonymous  ' Choose one of the above
Const SMTP_TIMEOUT     = 60 ' Seconds to wait for SMTP Server
Const MESSAGE_FLAG     = "URGENT: Backup Job has failed"
Const BACKUP_SHARE     = "\\RemoteServer\ExchangeBackups"
Const BACKUP_SELECTION = "Exchange Backup Selection.bks"

 


Dim szYYWW                    ' Date in YYYY-WW format (Week of year)
Dim szYYMMDD                  ' Date in YYYY-MM-DD format
Dim szFlagsSelection          ' The backup selection script, prepopulated
Dim szSetDescription          ' The description of the backup set
Dim szDestinationFile         ' The destination file in the destination directory
Dim szFlagsJobName            ' Flags for the name of the job  [/j "jobname")
Dim szFlagsVerify             ' Flags for verify the backup    [yes|no]
Dim szFlagsRemoteStorage      ' Flags for remote storage       [/rs:no|yes]
Dim szFlagsHardwareCompress   ' Flags for hardware compression [/hc:off|on]
Dim szFlagsLogging            ' Flags for logging in ntbackup  [/l:f|s|n] Full SUmmary None
Dim szFlagsAppend             ' Flags for appending data       [/a] or nothing
Dim szFlagsRestrict           ' Flags for restricting access   [/r:yes|no]
Dim szFlagsType               ' Flags for backup type          [/m normal|Incremental|Differential...]
Dim szFlagsTapeName           ' Flags for name of tape
Dim oFSO                      ' File System Object to see if file already exists
Dim owShell                   ' To execute a shell command
Dim rc                        ' Return code
Dim szError                   ' If we have an error, record it in here
Dim szCommandLine             ' What we are going to run as a backup
Dim szUTC                     ' UTC Date/Time for SMTP Reply-By field

Set oFSO        = Nothing
set owShell     = Nothing
rc              = NO_ERROR  ' OK So far
szCommandLine   = ""        ' Not sure what we're running yet
szError         = ""        ' Not had an error yet

' Setup our variables
if (NO_ERROR = rc) Then
    szYYWW                   = year(now()) & " w" & formatNumber(DatePart("WW",now()))
    szYYMMDD                 = year(now()) & "-" & formatNumber(month(now())) & "-" & formatNumber(day(now()))
    szFlagsSelection         = chr(34) & "@" & BACKUP_SHARE & "\" & BACKUP_SELECTION & chr(34)
    szFlagsJobName           = "/j " & chr(34) & "Exchange Backup" & chr(34)
    szFlagsVerify            = "/v:yes"        ' Verify YES|NO
    szFlagsRemoteStorage     = "/rs:no"
    szFlagsHardwareCompress  = "/hc:off"       ' Hardware compression off - this is to disk
    szFlagsLogging           = "/l:f"          ' f=full s=summary n=none
    szFlagsAppend            = "/a"            ' /a for Append or leave blank to overwrite
    szFlagsRestrict          = "/r:no"         ' no|yes Restrict access to administrators
    szFlagsTapeName          = "/t:Exchange " & szYYWW
    szDestinationFile        = "Exchange " & szYYWW & ".bkf"
    szSetDescription         = "/d " & chr(34) & "Created " & szYYMMDD & chr(34)
    szUTC                    = GetUTCSMTPDateString()
end if

 

' Instantiate File System Object
if (NO_ERROR = rc) Then
    err.clear
    Set oFSO = CreateObject("Scripting.FileSystemObject")
    if (err.number) or (oFSO is nothing) Then
        rc = -1
        szError = "Failed Creating FSO: " & err.description & " -0x" & hex(err.number)
    end if
end if

' Look to see if the file exists to determine the backup type
if (NO_ERROR = rc) Then
    if not oFSO.FileExists(BACKUP_SHARE & "\" & szDestinationFile) then
        ' Normal | Copy | Differential | Incremental  Backup Type
        szFlagsType       = "/m normal "    
        szFlagsAppend     = ""  ' Don't Append if does not exist
    else
        ' File exists, so incremental backup. We are already in Append mode
        szFlagsType       = "/m incremental "  
        szSetDescription  = "/d " & chr(34) & "Inc " & szYYMMDD & chr(34)
    end if

    ' Release File SYstem Object
    set oFSO = Nothing
end if


' Create a Shell Object to be able to run the backup executable
if (NO_ERROR = rc) Then
    err.clear
    Set owShell = wscript.createobject("wscript.shell")
    if (err.number) or (owShell is nothing) Then
        rc = -2
        szError = "Failed Creating wscript.shell: " & err.description & " -0x" & hex(err.number)
    end if
end if

' Build the backup command and run it
if (NO_ERROR = rc) Then
    szCommandLine  = BACKUP_PROGRAM         & _
                     "backup"         & " " & _
                     szFlagsSelection      & " " & _
                     szSetDescription & " " & _
                     "/f " & chr(34) & BACKUP_SHARE & "\" & szDestinationFile & chr(34) & " " & _
                     szFlagsAppend           & " " & _
                     szFlagsLogging          & " " & _
                     szFlagsVerify           & " " & _
                     szFlagsRestrict         & " " & _
                     szFlagsRemoteStorage    & " " & _
                     szFlagsHardwareCompress & " " & _
                     szFlagsType             & " " & _
                     szFlagsJobName          & " "

    rc=owshell.run(szCommandLine,,True)
end if

 

set owShell = Nothing
if (rc) Then SendErrorEmail
wscript.quit(rc)


Function FormatNumber(szIn)
   FormatNumber = szIn
   if len(szIn) = 1 then FormatNumber = "0" & szIn
End Function


'-----------------------------------------------------------------------------------
' Function Added: JJH 04 Nov 2005
'-----------------------------------------------------------------------------------
' For Sending an Email. We have the option to use send using exchange, but
' this can only be done on the Exchange server itself and requires CDOEx which
' is only installed on an Exchange Server. However CDO (CDOSys in old speak) is
' also installed on any Windows 2003 Server, and is a subset of CDOEx, we can
' use SMTP to send from any server pointing towards our Exchange Server.
' HOWEVER: It is nice to be able to get a reminder in Outlook to say that
' we need to do something rather than just an email. This requires an additional
' message header Reply-By which is in the Format (eg) Fri, 4 Nov 2005 15:23:08 -0000.
' Now, rather than mess around with timezones etc, I just use UTC. Seems to work,
' but it probably also helps that I'm in the UK so don't have to worry too much
' about timezones generally :-) However, appreciate many people may use this
' who are outside of UK, so best to make an effort!
'
' Note: We also fail safe to assume no time bias if we can't read registry
'-----------------------------------------------------------------------------------
Function GetUTCSMTPDateString()
    Dim dtUTC        ' UTC Date
    Dim szUTC          ' UTC Date string in SMTP RFC Format
    Dim oShell         ' To read registry
    Dim szATBRegKey    ' Registry key for Active Time Bias
    Dim lMinutesOffset ' From UTC
    Dim rc             ' Function return
 
    On error resume next
    rc = 0
    szATBRegKey = "HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\TimeZoneInformation\ActiveTimeBias"
    lMinutesOffset = 0
    err.clear

    if (NO_ERROR = rc) Then 
        err.clear
        set oShell = CreateObject("WScript.Shell")
        if (err.number) or (oShell = Nothing) Then rc = -1
    end if
   
    if (NO_ERROR = rc) Then
        lMinutesOffset = oShell.RegRead(szATBRegKey)
        if (err.number) then
     rc = -1
            lMinutesOffset = 0
        end if
    end if

    ' Regardless of error, we do the formatting (default to zero offset)
    dtUTC = dateadd("n", lMinutesOffset, now())
    szUTC = WeekdayName(Weekday(dtUTC),True) & ", " & _
            Day(dtUTC) & " " & _
            MonthName(Month(dtUTC),True) & " " & _
            Year(dtUTC) & " " & _
            FormatNumber(Hour(dtUTC)) & ":" & _
            FormatNumber(Minute(dtUTC)) & ":" & _
            FormatNumber(Second(dtUTC)) & " " & _
            "-0000"

    set oShell = Nothing
    err.clear ' Don't pass any error back as we will have _something_
    GetUTCSMTPDateString = szUTC

End Function

'-----------------------------------------------------------------------------------
' Function Added: JJH 04 Nov 2005
'-----------------------------------------------------------------------------------
' There are several solutions to sending email. We _could_ use CDOEx, but we need
' to be running on an Exchange Server to do this. While you might say "But this is
' a generic Exchange backup program, so of course it's running on an Exchange Server",
' remember although I built it for Exchange backups, it's still pretty generic and
' can be used for file system backups or system state etc.
'
' CDOEx is not "remoteable" - it can only be instantiated on the Exchange Server
' itself. Hence, not very generic.
'
' BUT - CDOEx has advantages. You can use "SendUsingExchange" rather than SMTP
' which allows you to relatively easily set a reminder on the message itself.
' You would unfortunately also be limited to not just running on an Exchange
' Server, but who you are sending it _from_ must have their mailbox on that
' local server.
'
' Plan B: Use CDO which is present on all Windows Servers. However, to do this
' has its own problems if you also want the message to flag up if you're using
' a rich Outlook client to read the message.
'
' Plan B and a half is to incorporate elements of both solution (see inline comments,
' with some code commented out) but using SMTP.
'
' Note: I could have done this, but it just would have been _too_ easy :-)
'
'Dim oMsg
'set oMsg = CreateObject("CDO.Message")
'oMsg.To = "user@contoso.com"
'oMsg.From = "Exchange@contoso.com"
'oMsg.Subject = "Backup Status"
'oMsg.TextBody = "Whatever you want"
'oMsg.Send
'set oMsg = Nothing

'-----------------------------------------------------------------------------------
Function SendErrorEmail()

    Dim oMsg       ' Message we send if failure occurs
    Dim szFrom        ' String
    Dim szBaseFolder  ' Base folder for the current users mailbox
    Dim rc

    szFrom = ""
    rc = NO_ERROR
    set oMsg = Nothing

' COMMENTED OUT - COULD USE IF WE KNOW THIS IS RUNNING ON AN EXCHANGE SERVER
' REASON BEING: CDO.Person and IMailbox Interface are only implemented in CDOEx, not CDO
' Following block is to generate a full from email address so that we
' use a better display name if we are sending the email externally from
' the Exchange organisation. Not strictly necessary, but nicer.
' Also, no error checking implemented
'    Dim IMailbox      ' Mailbox Interface to CDO.Person object
'    Dim oADSInfo      ' ActiveDS.ADSystemInfo
'    Dim oPerson       ' CDO.Person
'    set oADSInfo = CreateObject("ADSystemInfo")
'    set oPerson = CreateObject("CDO.Person")
'    oPerson.DataSource.Open("LDAP://" & oADSInfo.DomainDNSName & "/" & oADSInfo.UserName)
'    szFrom = oPerson.FileAs  ' May be blank if no description in AD
'    if 0 = len(szFrom) then szFrom = "Exchange Backup Script"
'    szFrom = szFrom & " <" & oPerson.Email & ">"


    ' Need to have a default sender
    if (NO_ERROR = rc) Then
        if 0 = len(szFrom) Then szFrom = DEFAULT_SENDER
    end if


' COMMENTED OUT - AGAIN CDOEX SPECIFIC. Assuming we use code block commented
' above, we need to make sure that the users mailbox we are sending FROM (which
' is the currently logged on user) is homed on the current Exchange Server.
' The base folder will be something like file://./backofficestorage/<domain>/MBX/<User>/
'    set IMailbox = oPerson.GetInterface("IMailbox")
'    szBaseFolder = IMailbox.BaseFolder
'    if (0=len(szBaseFolder)) Then <TRAP ERROR HERE>


    ' Create a message object. Not a lot we can do if we can't - could put further
    ' checking maybe to write to event log. Exercise for the reader though.
    if (NO_ERROR = rc) Then
        err.clear
        set oMsg = CreateObject("CDO.Message")
        if (err.number) or (oMsg is nothing) Then rc = -1
    end if

    ' Configure the outbound email
    if (NO_ERROR = rc) Then

        With oMsg.Configuration
            '.Fields(cdoSaveSentItems) = False

            ' CDOEX Specific next line
            '.Fields("http://schemas.microsoft.com/cdo/configuration/sendusing") = cdoSendUsingExchange
       
            ' SMTP Specific lines instead of above commented out line
            .Fields("http://schemas.microsoft.com/cdo/configuration/sendusing") = 2 ' SMTP By Port
            .Fields("http://schemas.microsoft.com/cdo/configuration/smtpserver") = SMTP_SERVER
            .Fields("http://schemas.microsoft.com/cdo/configuration/smtpserverport") = SMTP_PORT
            .Fields("http://schemas.microsoft.com/cdo/configuration/smtpauthenticate") = SMTP_AUTH
            .Fields("http://schemas.microsoft.com/cdo/configuration/smtpconnectiontimeout") = SMTP_TIMEOUT
            ' If using BASIC, need to set cdo/configuration/sendusername and sendpassword accordingly
       
            ' Update fields from the configuration
            .Fields.Update
        end With

        oMsg.To = DEFAULT_RECIP
        oMsg.From = szFrom
        oMsg.Subject = DEFAULT_SUBJECT

        oMsg.TextBody = "Backup Job has failed" & vbcrlf
        oMsg.TextBody = oMsg.TextBody & "Error: " & szError & vbcrlf & vbcrlf
        oMsg.TextBody = oMsg.TextBody & "Backup Job: " & szCommandLine

        ' CDOEX/SendUsingExchange Specific to set the message flag
        'oMsg.Fields("http://schemas.microsoft.com/mapi/proptag/0x10900003")=2
        'oMsg.Fields("urn:schemas:httpmail:messageflag")= MESSAGE_FLAG

        ' SMTP Way of doing this
        oMsg.Fields("urn:schemas:mailheader:Reply-By") = szUTC
        oMsg.Fields("urn:schemas:mailheader:X-Message-Flag") = MESSAGE_FLAG

       oMsg.Fields(cdoImportance) = cdoHigh
       oMsg.Fields.Update
       oMsg.Send
    end if

    set oMsg = Nothing

    ' If using CDOEx
    'set oPerson = Nothing
    'set oADSInfo = Nothing


End Function

Someone smart may wonder what's oMsg.Fields("http://schemas.microsoft.com/mapi/proptag/0x10900003")=2 about. If you want to know, and how I went about finding out, it's a long story. Post a comment and I'll put up the details when I get a spare day (it's that long). <GRIPE>Previous to working at Microsoft, obtaining info like this was my day job - I had plenty of support cases lined up at MS and got pretty up tight about some of the decisions made by the Exchange Dev team, and specifically those working on the SDK tools. Seems like even since my gripes a few years ago, the same lack of documentation (or deliberate hiding of this info more specifically is still there)</GRIPE>

 (Nov 2008) Updated as I discovered the formatting of the above script was awful in the new blog template. Also adding a link to a direct download of the script (save as a .vbs file rather than .txt). It's here.

Comments
  • I can't get it to run. I get the e-mail with the red and the flag and all that, but it doens't start any backups. I changed
    Const BACKUP_SHARE = "F:\ExchangeBackups"
    Const BACKUP_SELECTION = "F:\ExchangeBackups\Mailbox Stores Normal Backups.bks"

  • Hi Carlos

    Good that you're getting an email at least. You havve checked and double-check that f:\ExchangeBackups exists with permissions for you to write in that folder, and the "Mailbox Stores Normal Backups.bks" also exists in that folder?

    Debugging steps:
    - What's the contents of the email sent to you? Does it give any clues as to the failure?
    - Immediately after the line rc=wscript.run..., add a new line "wscript.echo szCommandLine"
    - Run backup.vbs from the command line (ie start/run/cmd then cscript backup.vbs. Copy the text into a .cmd file (eg test.cmd), run test.cmd and see if the backup utility even opens.

    If you still don't get anywhere, post back the output from the script with the new line in place and I'll take a further look. Should be trivial to fix, I hope ;)

    Cheers,
    John.

  • Better late than never... in regards to Carlos problem (which I think he solved himself already):

    I encountered the same problem and managed to get it working by changing the lines to...

    Const BACKUP_SHARE = "F:\ExchangeBackups"
    Const BACKUP_SELECTION = "Exchange Backups.bks"

    ...where Exchange Backups.bks is located at F:\ExchangeBackups\.

    Btw, thanks for the script, John, looks like it's saving me a lot of time. :)

  • This really is a great script! Thank you very much...

  • Hey John... I can tell it's been some time since someone responded but thanks a million. I've been reading this script looking for the logic since I'm not a programmer but it doesn't matter. My networking job just got much easier using this script. Thanks once again...

  • John,

    Great Script! Can it send an email to say that it completed ok and not just on error? Thanks!

  • wow...Great work i would say it's really saving time..i have been looking for such scrip. thank you very much.

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment