Tuesday, February 2, 2016

Tegile Zebi Replication Reports

Those of you who are working with Tegile storage systems are very aware that reporting is one of the things that is still lacking.  There are a number of canned reports you can get via the GUI, but as far as custom reports or getting data out programatically,  good luck.

I'm told by folks in Tegiles' management that they are developing PowerShell commandlets for use with their storage appliances.  That's great news.  When we get to see them I don't know.

In the interim I needed to get data out of the arrays.  I came up with a PowerShell script that will connect to a list of controllers and extract replication data into an Excel spreadsheet.  I only care about completed verses failed so the output is limited to that.  Actually as it sits it only reports good runs, I'm working on failed runs.

This script will create a spreadsheet with the last 2 months of successful replication events.  It can be modified for other logs but this is specifically designed to parse the replication logs.  Each target is a new worksheet, and each tab is labeled as such.   It's still a work in progress so some variables are incomplete (I like to have them all at the "script:" context...).  I'll update this as I get farther along, but it does work as is.

UPDATE: 09-02-16
It's been noted that this script doesn't catch failed jobs.  It's very targeted towards completed jobs.  I have retooled it to (hopefully) catch the failed jobs.  Seems to be working on my end.  The parsing of the ingested logs is tricky since the formats change depending on what's being reported.

I've overwritten the code below with the v2 version.  I shrunk the font down to try and avoid wrapping.  Tegile has released their PowerShell commandlets.  They do a lot but still don't allow for this type of reporting.

Updated to correct a few typos... sorry :-)

UPDATE: 11-07-16
Tegile recently released the 3.5.x firmware update.  The initial release had a bug and a I sincerely hope none of you experienced the same issues that we did with it.

Another issue is that they have changed the formatting of the replication log messages.  I had to completely rewrite the script below to allow the results to be captured properly.  In addition I added detection of aborted jobs.

Note the config file has changed.  We now specify DAYS instead of WEEKS to report.

UPDATE: 08-21-18
Note that this script is now located on the MS PowerShell Gallery.  It can be found on my library main page at https://www.powershellgallery.com/profiles/Kcmjr/.  This way I don't need to constantly update this post when versions change.

The script noted below is an OLD VERSION.  See the link for updates...

<#==============================================================================
         File Name : ReplicationReport.ps1
   Original Author : Kenneth C. Mazie (kcmjr @ kcmjr.com)
                   :
       Description : This script will query multiple Tegile Zebi Controllers,
                   : parse the replication logs, and populate an Excel spreadsheet.
                   : The results are pulled from back as far as a preset number of
                   : days.  It then emails the resulting spreadsheet.  X number of
                   : prior runs are retained to avoid filling the disk.
                   :
         Arguments : Named commandline parameters:  (all are optional)
                   :   "-console" - Displays console output during run.
                   :   "-debug" - Switches email recipient and some criteria.
                   :
             Notes : Settings are loaded from an XML file located in the script folder.
                   : See the end of the script for config file example.
                   :
      Requirements : Requires PS v5.  Requires Posh-SSH module.              
                   : 
          Warnings : None
                   :
             Legal : Public Domain. Modify and redistribute freely. No rights reserved.
                   : SCRIPT PROVIDED "AS IS" WITHOUT WARRANTIES OR GUARANTEES OF
                   : ANY KIND. USE AT YOUR OWN RISK. NO TECHNICAL SUPPORT PROVIDED.
                   :
           Credits : Code snippets and/or ideas came from many sources around the web.
                   :
    Last Update by : Kenneth C. Mazie (email kcmjr AT kcmjr.com for comments or to report bugs)
   Version History : v1.0 - 02-03-16 - Original
    Change History : v1.1 - 03-09-16 - Added XML config file
                   : v2.0 - 09-02-16 - Added detection of failed jobs. Retooled parser.
                   : v3.0 - 09-12-16 - Retooled for Pos-SSH v1.7.6.  Eliminated PSEXEC
                   : requirement.  Eliminated work file requirement.  Added save only
                   : the last X copies.  Added debug name change.  Added note on email
                   : that refers to failures found.
                   : v4.0 - 11-04-16 - Complete redesign of time calculation.  Now
                   : uses epoch starting at 01-01-1970 as reference.  This avoids issues
                   : with using the week of the year.  Also corrected for up to 4 different
                   : variations is log formats seen in v3.5 Zebi firmware.
#===============================================================================#>
#requires -version 5.0

Param (
       [bool]$Debug = $false,
       [bool]$Console = $false
)

clear-host

#--[ Store all the start up variables so you can clean up when the script finishes. ]--
if ($startupvariables) { try {Remove-Variable -Name startupvariables  -Scope Global -ErrorAction SilentlyContinue } catch { } }
New-Variable -force -name startupVariables -value ( Get-Variable | ForEach-Object { $_.Name } )

If (!(Get-Module Posh-SSH)){Import-Module "Posh-SSH" -ErrorAction SilentlyContinue}
If ($Debug){$Script:Debug = $true}
If ($Console){$Script:Console = $true}
$ErrorActionPreference = "silentlycontinue"
$Script:Attach = $true
$Script:Datetime = Get-Date -Format "MM-dd-yyyy_HH:mm"              #--[ Current date $ time ]--
$Script:Today = Get-Date -Format "MM-dd-yyyy"                       #--[ Current date ]--
$Script:ThisYear = Get-Date -Format "yyyy"                          #--[ Current year ]--
$EpochDiff = New-TimeSpan '01 January 1970 00:00:00' $(Get-Date)    #--[ Seconds since 01-01-1970 ]--
$EpochSecs = [INT] $EpochDiff.TotalSeconds                          #--[ Rounded ]--
$EpochDays = [INT] (($EpochDiff.TotalSeconds)/86400)                #--[ Converted to days ]--
$Script:StatusFail = $false

#--[ Excel Non-Interactive Fix ]------------------------------------------------
If (!(Test-path -Path "C:\Windows\System32\config\systemprofile\Desktop")){New-Item -Type Directory -Name "C:\Windows\System32\config\systemprofile\Desktop"}
If (!(Test-path -Path "C:\Windows\SysWOW64\config\systemprofile\Desktop")){New-Item -Type Directory -Name "C:\Windows\SysWOW64\config\systemprofile\Desktop"}
#--[ Excel will crash when run non-interactively via a scheduled task if these folders don't exist ]--

#--[ Functions ]----------------------------------------------------------------
Function ResetVariables {
Get-Variable | Where-Object { $startupVariables -notcontains $_.Name } | ForEach-Object {
  try { Remove-Variable -Name "$($_.Name)" -Force -Scope "global" -ErrorAction SilentlyContinue -WarningAction SilentlyContinue}
  catch { }
 }
}

Function SendEmail {
       If ($Script:Debug){$ErrorActionPreference = "stop"}
       $email = New-Object System.Net.Mail.MailMessage
       $email.From = $Script:EmailFrom
       $email.IsBodyHtml = $Script:EmailHTML
       $email.To.Add($Script:EmailTo)
       $email.Subject = $Script:EmailSubject
       $email.Body = $Script:ReportBody
       If ($Script:Attach -eq $true){
              $email.Attachments.Add("$PSScriptRoot\$Script:SaveFile.xlsx")
       }     
       $smtp = new-object Net.Mail.SmtpClient($Script:SmtpServer)
       $smtp.Send($email)
       If ($Script:Log){Add-content -Path "$PSScriptRoot\debug.txt" -Value "-------------[ Sending Email ]--------------"}
       If ($Script:Console){Write-Host "`nEmail sent...`n"}
}

#--[ Read and load configuration file ]-----------------------------------------
If (!(Test-Path "$PSScriptRoot\Configuration.xml")){                            #--[ Error out if configuration file doesn't exist ]--
       Write-host "MISSING CONFIG FILE.  Script aborted." -forgroundcolor red
       break
}Else{
       [xml]$Script:Configuration = Get-Content "$PSScriptRoot\Configuration.xml"  #--[ Load configuration ]--
       $Script:DebugEmail = $Script:Configuration.Settings.Email.Debug
       $Script:EmailTo = $Script:Configuration.Settings.Email.To
       $Script:EmailHTML = $Script:Configuration.Settings.Email.HTML
       $Script:EmailSubject = $Script:Configuration.Settings.Email.Subject
       $Script:EmailFrom = $Script:Configuration.Settings.Email.From
       $Script:SmtpServer = $Script:Configuration.Settings.Email.SmtpServer
       $Script:UserName = $Script:Configuration.Settings.Credentials.Username
       $Script:Password = $Script:Configuration.Settings.Credentials.Password
       [array]$Script:Targets = $Script:Configuration.Settings.General.Targets
       $Script:DaysBack = $Script:Configuration.Settings.General.Days
       $Script:ReportsToKeep = $Script:Configuration.Settings.General.Reports        #--[ How many reports to retain ]--
       $Script:FileName = $Script:Configuration.Settings.General.FileName
}     
#-------------------------------------------------------------------------------
#$Credential = New-Object PSCredential($UserName, (ConvertTo-SecureString $Password.SubString(64) -k ($Password.SubString(0,64) -split "(?<=\G[0-9a-f]{2})(?=.)" | % { [Convert]::ToByte($_,16) })))
#$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $UserName, ($Password | ConvertTo-SecureString)

Get-ChildItem -Path "$PSScriptRoot\" | Where-Object {-not $_.PsIsContainer -and $_.Name -like "*.xlsx"} | Sort-Object -Descending -Property LastTimeWrite | Select-Object -Skip $Script:ReportsToKeep | Remove-Item     
If ($Script:Debug){
       [string]$Script:TodayDate = Get-Date -Format MM-dd-yyyy_hhmm    #--[ Adds hour and min to filename for debugging ]--
}Else{
       [string]$Script:TodayDate = Get-Date -Format MM-dd-yyyy
}
[string]$Script:SaveFile = $Script:FileName+"-"+$Script:TodayDate

If (Test-Path "$PSScriptRoot\$Script:SaveFile.xlsx"){     #--[ If not in debug mode will rename exisiting XLS to allow it to overwritten ]--
       Rename-Item -Path "$PSScriptRoot\$Script:SaveFile.xlsx" -NewName "$PSScriptRoot\$Script:SaveFile-old.xlsx"
}

$Script:ReportBody = "Attached are the replication results from the prior "+$Script:DaysBack+" days."
$Script:ReportBody += "<br>- All Zebi controllers are included regardless of whether they actively participate in replication or not."
$Script:ReportBody += "<br>- Each controller has it's own tab on the spreadsheet."
$Script:ReportBody += "<br>- Only outgoing replication jobs are tracked."

$Color = @{
       "1" = "green"
       "2" = "red"  
       "3" = "yellow"
       "4" = "cyan"
       "5" = "magenta"
}

$MissingType = [System.Type]::Missing
$Excel = ""
$Excel = New-Object -ComObject Excel.Application
If ($Script:Console -or $Script:Debug){
       $Excel.visible = $True
       $Excel.DisplayAlerts = $true
       $Excel.ScreenUpdating = $true
       $Excel.UserControl = $true
       $Excel.Interactive = $true
}Else{
       $Excel.visible = $False
       $Excel.DisplayAlerts = $false
       $Excel.ScreenUpdating = $false
       $Excel.UserControl = $false
       $Excel.Interactive = $false
}

$TargetCount = $Script:Targets.Target.count
$Workbook = $Excel.Workbooks.Add()
$Workbook.Worksheets.Add($MissingType, $Workbook.Worksheets.Item($Workbook.Worksheets.Count), ($TargetCount) - $Workbook.Worksheets.Count, $Workbook.Worksheets.Item(1).Type) | Out-Null
#--[ Note that new sheets are always adding in reverse ]--

$SheetCount = 1
$Target = @()
Foreach ($Target in $Script:Targets.Target){
       if ($Console){Write-Host "`n--[ Processing Target: $Target ]-----------------------------------" -ForegroundColor Yellow }                   
       $Count = 1   
       $Worksheet = $workbook.Sheets.Item($SheetCount)
       $Worksheet.Activate()     
       $Worksheet.name = $Target
       $Row = 1
       $Col = 1
       $WorkSheet.Cells.Item($Row,$Col) = "Date:"
       $WorkSheet.Cells.Item($Row,$Col).font.bold = $true
       $WorkSheet.Cells.Item($Row,$Col).HorizontalAlignment = 1
       $Col++
       $WorkSheet.Cells.Item($Row,$Col) = "Source IP:"
       $WorkSheet.Cells.Item($Row,$Col).font.bold = $true
       $WorkSheet.Cells.Item($Row,$Col).HorizontalAlignment = 1
       $Col++
       $WorkSheet.Cells.Item($Row,$Col) = "Source Pool:"
       $WorkSheet.Cells.Item($Row,$Col).font.bold = $true
       $WorkSheet.Cells.Item($Row,$Col).HorizontalAlignment = 1
       $Col++
       $WorkSheet.Cells.Item($Row,$Col) = "Target IP:"
       $WorkSheet.Cells.Item($Row,$Col).font.bold = $true
       $WorkSheet.Cells.Item($Row,$Col).HorizontalAlignment = 1
       $Col++
       $WorkSheet.Cells.Item($Row,$Col) = "Target Pool:"
       $WorkSheet.Cells.Item($Row,$Col).font.bold = $true
       $WorkSheet.Cells.Item($Row,$Col).HorizontalAlignment = 1
       $Col++
       $WorkSheet.Cells.Item($Row,$Col) = "Status:"
       $WorkSheet.Cells.Item($Row,$Col).font.bold = $true
       $WorkSheet.Cells.Item($Row,$Col).HorizontalAlignment = 1
       $Col++
       $Col++
       $WorkSheet.Cells.Item($Row,$Col) = "Parsed Data:"
       $WorkSheet.Cells.Item($Row,$Col).font.bold = $true
       $WorkSheet.Cells.Item($Row,$Col).HorizontalAlignment = 1
       $Col++
       $WorkSheet.Cells.Item($Row,$Col) = "Raw Data:"
       $WorkSheet.Cells.Item($Row,$Col).font.bold = $true
       $WorkSheet.Cells.Item($Row,$Col).HorizontalAlignment = 1
       $Col++
       $WorkSheet.application.activewindow.splitcolumn = 0
       $WorkSheet.application.activewindow.splitrow = 1
       $WorkSheet.application.activewindow.freezepanes = $true
       $Resize = $WorkSheet.UsedRange
       [void]$Resize.EntireColumn.AutoFit()

       $Cmd = 'cat /var/log/tegile/zebi/zebirep.log | egrep "has completed|has failed"'     #--[ The command sent to the Target controller ]--
      
       #--[ Some optional other commands that could be used ]--
       #      'uname -a '
       #      '/usr/sbin/zfs list -o name'
       #      'pwd'
       #      'df -h'
       #      '/usr/sbin/dladm show-link –S'
       #      '/usr/sbin/fmadm faulty'
       #      '/usr/sbin/zpool list'
       #      '/usr/sbin/zfs list -o name'
       #      'zfs list -t snapshot'
       #      'zfs list -o space -r hostname$/Local'
       #      'zpool iostat 2'                                                                                        #--[ Display ZFS I/O statistics every 2 seconds ]--
       #      'zpool iostat -v 2'                                                                                     #--[ Display detailed ZFS I/O statistics every 2 seconds ]--
      
       Remove-SSHSession -SessionId 0 -ErrorAction SilentlyContinue | Out-Null     #--[ Clear out previous session if it still exists ]--

       $SecPassword = ConvertTo-SecureString $Script:Password -AsPlainText -Force
       $Creds = New-Object System.Management.Automation.PSCredential ($Script:UserName, $SecPassword)
       $SSH = New-SshSession -ComputerName $Target -Credential $Creds -AcceptKey:$true #| Out-Null  #--[ Open new SSH session ]--
       $Script:Return = $(Invoke-SSHCommand -SSHSession $SSH -Command $Cmd).Output   #--[ Invoke SSH command and capture the output as a string ]--

       $Row = 2
       $Col = 1
       $SourcePool = ""
       $DestPool = ""
       $TargetPool = ""
       $TargetIP = ""
       $SourceIP = ""
       $Line = ""
       $RepJob = ""
       $Status = ""
      
       #--[ Process each line of the input file ]--
       ForEach ($Line in $Script:Return){
              $JobDiffDays = "99"
              $JobDate = ""
              $JobDay = ""
              $JobMonth = ""
              $JobYear = ""

              $RepJob = $Line.Split("]")[2]
              If ($RepJob.Contains('null')){
                     $JobDate = (($RepJob.Split('@')[2]).split('-')[1]).split(' ')[0]
              }Else{
                     $JobDate = ($RepJob.Split('@')[1]).split('-')[1]
              }
              If ($JobDate -ne ""){
                     $JobYear = $JobDate.substring(0,4)
                     $JobMonth = $JobDate.substring(4,2)
                     $JobDay = $JobDate.substring(6,2)
                     $JobDate = $JobMonth+'-'+$JobDay+'-'+$JobYear
                     If (!([string]$JobDate -as [DateTime])){
                           $JobDate = (($RepJob.Split('@')[2]).split('-')[1]).split('=')[0]
                           $JobYear = $JobDate.substring(0,4)
                           $JobMonth = $JobDate.substring(4,2)
                           $JobDay = $JobDate.substring(6,2)
                           $JobDate = $JobMonth+'-'+$JobDay+'-'+$JobYear
                     }
                     $JobDiff = New-TimeSpan $JobDate $(Get-Date)                               #--[ Days since date of job ]--
                     $JobDiffDays = [int] (($JobDiff.TotalSeconds)/86400)                #--[ Rounded & Converted to days ]--
                          
                     If ($Line -like "*Abort*"){
                           $RepStatus = "ABORTED"
                           $SourcePool = ($RepJob.Split('@')[0]).Split(' ')[2]
                           $TargetPool = ($RepJob.Split(':')[1]).Split(' ')[0]
                           $TargetIP = ($RepJob.Split(':')[0]).Split('>')[1]
                     }Elseif($Line -like "*Abandon*"){
                           $RepStatus = "ABANDONED"
                     }Else{
                           $RepStatus = $Line.Split("]")[3]
                           If (($RepJob.Split('@')[0]).Split(' ')[2] -eq "from"){
                                  $SourcePool = ($RepJob.Split('@')[0]).Split(' ')[3]
                                  $TargetPool = ($RepJob.Split(':')[1])#.Split('/')[0]
                                  $TargetIP = ($RepJob.Split(':')[0]).Split('>')[1]
                           }Else{
                                  $SourcePool = ($RepJob.Split('@')[0]).Split(' ')[2]
                                  $TargetPool = ($RepJob.Split(':')[1])#.Split('/')[0]
                                  $TargetIP = ($RepJob.Split(':')[0]).Split('>')[1]
                           }
                     }
                                               
                     If($RepJob -like "*@Auto*"){
                           #$RepStatus = $Line.Split("]")[3]
                           $SourcePool = ($RepJob.Split('@')[0]).Split(' ')[2]
                           $TargetPool = (($RepJob.Split('>')[1]).Split(':')[1]).split(" ")[0]
                           $TargetIP = ($RepJob.Split('>')[1]).Split(':')[0]
                     }
              }
             
              #--[ Dump collected data into spreadsheet ]--
              if ([int]$JobDiffDays -le [int]$Script:DaysBack){
                     if ($Console){
                           Write-Host "`n`n-----------------------------------------------"
                           Write-Host "Today               ="$Script:Today -ForegroundColor Yellow
                           Write-Host "Date of Job         ="$JobDate -ForegroundColor Yellow
                           Write-Host "Days since job ran  ="$JobDiffDays -ForegroundColor Yellow
                           #Write-Host "Current Week        ="$Script:CurrentWeek -ForegroundColor Yellow
                           #Write-Host "Result              ="$DateMathResult -ForegroundColor Yellow
                           Write-Host "Match Criteria      ="$Script:DaysBack -ForegroundColor Yellow
                           Write-Host "Parsed Data         ="$RepJob -ForegroundColor Yellow
                     }
      
                     if ($Console){Write-Host "-- Matched criteria --" -ForegroundColor magenta }
                     if ($Console){Write-Host "Writing to tab ="$Target -ForegroundColor magenta }
                     if ($Console){Write-Host "Writing to row ="$row -ForegroundColor magenta }
                     #--[ Job run date ]-----------------------------------------------------                
                     $WorkSheet.Cells.Item($Row,$Col) = "$JobDate"
                     Clear-Variable $JobDate
                     $Col++
                     #--[ Source IP ]--------------------------------------------------------         
                     $WorkSheet.Cells.Item($Row,$Col) = $Target
                     $Col++       
                     #--[ Source Pool ]------------------------------------------------------  
                     if ($Console){Write-Host "Source Pool    ="$SourcePool -ForegroundColor Yellow}
                     $WorkSheet.Cells.Item($Row,$Col) = $SourcePool
                     $Col++
                     #--[ Target IP ]--------------------------------------------------------
                     #$dest = ($a.Split("]")[0]).split(" ")[3]
                     $destpool = (($a.Split("]")[0]).split(" ")[3]).Split(":")[1]
                     if ($Console){Write-Host "Target IP      ="$TargetIP}
                     $WorkSheet.Cells.Item($Row,$Col) = $TargetIP
                     $Col++
                     #--[ Destination Pool ]-------------------------------------------------
                     if ($Console){Write-Host "Target Pool    ="$TargetPool -ForegroundColor Yellow}
                     $WorkSheet.Cells.Item($Row,$Col) = $TargetPool
                     $Col++
                     #--[ Job Status ]-------------------------------------------------------  
                     If ($RepStatus.Contains("has failed")){
                           $Status = "FAILED"
                           $Script:StatusFail = $true
                           if ($Console){Write-Host "Job Status     ="$Status -ForegroundColor Red}
                           $WorkSheet.Cells.Item($Row,$Col).Interior.ColorIndex = 3
                     }
                                         If ($RepStatus -eq "aborted"){
                           $Status = "ABORTED"
                           #$Script:StatusFail = $true
                           if ($Console){Write-Host "Job Status     ="$Status -ForegroundColor Red}
                           $WorkSheet.Cells.Item($Row,$Col).Interior.ColorIndex = 6
                     }
                                         If ($RepStatus -eq "abandoned"){
                           $Status = "ABANDONED"
                           #$Script:StatusFail = $true
                           if ($Console){Write-Host "Job Status     ="$Status -ForegroundColor Red}
                           $WorkSheet.Cells.Item($Row,$Col).Interior.ColorIndex = 6
                     }
                     If ($RepStatus.Contains("has completed")){
                           $Status = "GOOD"
                           if ($Console){Write-Host "Job Status     ="$Status -ForegroundColor Green}
                     }     
                     $WorkSheet.Cells.Item($Row,$Col) = $Status
                     $Col++
                     #--[ Parsed Data ]------------------------------------------------------         
                     $Col++
                     $WorkSheet.Cells.Item($Row,$Col) = $RepJob
                     $Col++
                     #--[ Raw Data ]---------------------------------------------------------         
                     if ($Console){Write-Host "Raw data       = "$Line -ForegroundColor Cyan }
                     $WorkSheet.Cells.Item($Row,$Col) = $Line
      
                     $Line = ""
                     $RepJob = ""
                     $Status = ""
                     $Col = 1
                     $Row++
                     $Resize = $WorkSheet.UsedRange
                     [void]$Resize.EntireColumn.AutoFit()
              }
       }
       $SheetCount++
}

If (Test-Path "$PSScriptRoot\$Script:SaveFile-old.xlsx"){Remove-Item -Path "$PSScriptRoot\$Script:SaveFile-old.xlsx" -Confirm:$false -Force:$true}
$Workbook.SaveAs("$PSScriptRoot\$Script:SaveFile.xlsx")
$Excel.Quit()
$Excel = $Null

If($Script:StatusFail){
       $Script:ReportBody += "<br><br><font color=red>A replication failure was detected and is noted on the report</font><br>"
}Else{
       $Script:ReportBody += "<br><br><font color=green>No replication failures were detected.</font><br>"
}

$Script:ReportBody += "<br>Script executed on "+$Datetime+"<br><br>"
Sleep -Seconds 2
SendEmail

[gc]::Collect()
[gc]::WaitForPendingFinalizers()
ResetVariables

if ($Console){Write-Host "--- Completed ---" -ForegroundColor Red }

<#-----------------------------[ Config File ]---------------------------------

The configuration file must be named "Configuration.xml" and must reside in
the same folder as the script.  Below is the format and element list:

<!-- Settings & Configuration File -->
<Settings>
       <General>
              <ScriptName>ReplicationReport</ScriptName>
              <FileName>ZebiReplLogs</FileName>
              <DebugTarget>test-server</DebugTarget>
              <Targets>
                     <Target>10.100.1.1</Target>
                     <Target>10.100.1.2</Target>
                     <Target>10.100.1.3</Target>
                     <Target>10.100.1.4</Target>
                     <Target>10.100.1.5</Target>
                     <Target>10.100.1.6</Target>
                     <Target>10.100.1.7</Target>
                     <Target>10.100.1.8</Target>
              </Targets>
              <Days>14</Days>
              <Reports>20</Reports>
       </General>
       <Email>
              <From>WeeklyReports@mydomain.com</From>
              <To>me@mydomain.com</To>
              <Subject>Weekly Zebi Replication Status Report</Subject>
              <HTML>$true</HTML>
              <SmtpServer>10.100.1.10</SmtpServer>
       </Email>
       <Credentials>
              <UserName>zebiadminuser</UserName>
              <Password>zebiadminpwd</Password>
       </Credentials>
</Settings>  


#>