Zebi-Utilization-Tracker.ps1

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

<#==============================================================================
         File Name : Zebi-Utilization-Tracker.ps1
   Original Author : Kenneth C. Mazie (kcmjr AT kcmjr.com)
                   :
       Description : This script will query multiple Tegile Zebi Controllers,
                   : (or any Linux host) and run the "df -h" command capturing the
                   : results and parsing them into a spreadsheet. Each tab is a
                   : different day. Each tab is a list of all target controllers.
                   : A new spreadsheet is created each month. Multiple runs in
                   : the same day over-write the existing data.
                   :
         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 Posh-SSH v1.7.6
                   : Requires Powershell v5
                   :
          Warnings : None. All operations are read only.
                   :
             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.00 - 09-12-16 - Original
    Change History : v2.00 - 09-27-16 - Major rewrite. Reordered tabs. Added free and used tabs
                   : and moved them to the left. Converted data from whatever to GB.
                   : v2.10 - 03-02-18 - Minor notation tweak for PS Gallery upload
                   : v2.20 - 01-11-19 - Fixed config file load issue.
                   : v2.21 - 01-11-19 - Forgot to remove a few lines for testing.
                   : v2.30 - 01-22-19 - Added ability to select day email goes out (see xml file).
                   : v4.30 - 01-22-19 - Version bump.
                   :
#===============================================================================#>

<#PSScriptInfo
.VERSION 4.30
.GUID a10af5d7-a89e-43f5-8680-c5fdb962d417
.AUTHOR Kenneth C. Mazie (kcmjr AT kcmjr.com)
.DESCRIPTION
This script will query multiple Tegile Zebi Controllers, (or any Linux host) and run the "df -h" command capturing the
results and parsing them into a spreadsheet. Each tab is a different day. Each tab is a list of all target controllers.
A new spreadsheet is created each month. Multiple runs in the same day over-write the existing data.
#>
 

#requires -version 5.0

clear-host
If (!(Get-Module Posh-SSH)){Import-Module "Posh-SSH" -ErrorAction SilentlyContinue}
If ($Debug){$Script:Debug = $true}
If ($Console){$Script:Console = $true}
$Script:ScriptName = ($MyInvocation.MyCommand.Name).split(".")[0] 
$Script:LogFile = $PSScriptRoot+"\"+$ScriptName+"_{0:MM-dd-yyyy_HHmmss}.log" -f (Get-Date)
$Script:ConfigFile = "$PSScriptRoot\$ScriptName.xml"  
$ErrorActionPreference = "stop" #silentlycontinue"
$Script:DateToday = Get-Date -Format "MM-dd-yyyy"
$Script:ThisMonth = ((Get-Date -Format y) -replace (', ', '-')) -replace (' ','-')
$Script:ThisDay = (Get-Date -Format f).Split(",")[0]
$Script:Targets = @()
$Script:PoolList = @()

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

#--[ Functions ]----------------------------------------------------------------
Function SendEmail {
    If ($Script:Debug){$ErrorActionPreference = "stop"}
    If ($Script:EnableEmail){
        $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 = "Attached is the weekly update of the monthly SAN Storage Volume utilization report.<br>"
        $email.Body += "Reports only go out on the following days: $Day2Email "
        If ($Script:EmailAttach){
            $email.Attachments.Add("$PSScriptRoot\$Script:ThisMonth.xlsx")
        }    
        $smtp = new-object Net.Mail.SmtpClient($Script:SmtpServer)
         
        $Active = $false
        $Script:Day2Email | foreach {if ($Script:ThisDay -match $_){$Active = $true}}  #--[ Activate email send if today matches config ]--
          
        If ($Active){
            $smtp.Send($email)
            If ($Script:Console){Write-Host "`nEmail sent...`n"}
        }
    }    
}

Function Convert ($Incomming){    #--[ Convert TG and MB to GB ]--
    $Multiplier = $Incomming.substring($Incomming.length - 1, 1)
    $Number = $Incomming -replace ".$" 
    If($Multiplier -eq "T"){$Script:Converted = [int]$Number*1024}
    If($Multiplier -eq "G"){$Script:Converted = [int]$Number*1024}
    If($Multiplier -eq "M"){$Script:Converted = [int]$Number/1024}
    Return $Outgoing
}

#--[ Read and load configuration file ]-----------------------------------------
If (!(Test-Path $Script:ConfigFile)){                            #--[ Error out if configuration file doesn't exist ]--
    Write-host "MISSING CONFIG FILE. Script aborted." -ForegroundColor red
    break
}Else{
    [xml]$Script:Configuration = Get-Content $Script:ConfigFile  #--[ Load configuration ]--
    $Script:EnableEmail = $Script:Configuration.Settings.Email.Enable
    $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:EmailAttach = $Script:Configuration.Settings.Email.Attach 
    $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:WeeksBack = $Script:Configuration.Settings.General.Weeks
    [array]$Script:PoolList = $Script:Configuration.Settings.General.Pools
    [Array]$Script:Day2Email = ($Script:Configuration.Settings.General.Day2Email).Split(",")
}    
#-------------------------------------------------------------------------------

$MissingType = [System.Type]::Missing
$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
}

If (Test-Path "$PSScriptRoot\$Script:ThisMonth.xlsx"){ 
    Rename-Item -Path "$PSScriptRoot\$Script:ThisMonth.xlsx" -NewName "$PSScriptRoot\$Script:ThisMonth-old.xlsx"
    $Workbook = $Excel.Workbooks.Open("$PSScriptRoot\$Script:ThisMonth-old.xlsx")
}Else{
    $Workbook = $Excel.Workbooks.Add()
    $Worksheet = $Workbook.Worksheets.Add()
    foreach ($Worksheet in $Workbook.Worksheets){
        If ($Worksheet.Name -eq "Sheet1"){
            $Workbook.Worksheets.item("Sheet1").Activate() 
            $Worksheet.name = "Used"
        }
        If ($Worksheet.Name -eq "Sheet2"){
            $Workbook.Worksheets.item("Sheet2").Activate() 
            $Worksheet.name = "Free"
        }
    }
}

$ReRun = $false
ForEach ($Worksheet in $Workbook.Worksheets){If ($Worksheet.Name -eq $DateToday){$ReRun = $true}}

If ($ReRun){   #--[ Already run today ]--
    if ($Console){Write-host "-- Re-Running Todays Data -- " -ForegroundColor Yellow } 
    $Worksheet = $workbook.Sheets.Item($DateToday)
    $Worksheet.Activate()        
}Else{ 
    $Workbook.Worksheets.add([System.Reflection.Missing]::Value,$Workbook.worksheets.Item($Workbook.worksheets.count))
    ForEach ($Worksheet in $Workbook.Worksheets){
        If ($Worksheet.Name -like "*Sheet*"){$Worksheet.name = $DateToday}
    }    
}

$TargetCount = $Script:Targets.Target.count

$Row = 1
$Col = 1
$WorkSheet.Cells.Item($Row,$Col) = "Controller:"
$WorkSheet.Cells.Item($Row,$Col).font.bold = $true
$WorkSheet.Cells.Item($Row,$Col).HorizontalAlignment = 1
$Col++
$WorkSheet.Cells.Item($Row,$Col) = "Filesystem:"
$WorkSheet.Cells.Item($Row,$Col).font.bold = $true
$WorkSheet.Cells.Item($Row,$Col).HorizontalAlignment = 1
$Col++
$WorkSheet.Cells.Item($Row,$Col) = "Size GB:"
$WorkSheet.Cells.Item($Row,$Col).font.bold = $true
$WorkSheet.Cells.Item($Row,$Col).HorizontalAlignment = 1
$Col++    
$WorkSheet.Cells.Item($Row,$Col) = "Used GB:"
$WorkSheet.Cells.Item($Row,$Col).font.bold = $true
$WorkSheet.Cells.Item($Row,$Col).HorizontalAlignment = 1
$Col++
$WorkSheet.Cells.Item($Row,$Col) = "Available GB:"
$WorkSheet.Cells.Item($Row,$Col).font.bold = $true
$WorkSheet.Cells.Item($Row,$Col).HorizontalAlignment = 1
$Col++
$WorkSheet.Cells.Item($Row,$Col) = "Capacity:"
$WorkSheet.Cells.Item($Row,$Col).font.bold = $true
$WorkSheet.Cells.Item($Row,$Col).HorizontalAlignment = 1
$Col++
$WorkSheet.Cells.Item($Row,$Col) = "Mount Point:"
$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()

$Target = @()
$Row = 2
Foreach ($Target in $Script:Targets.Target){
    if ($Console){Write-Host "`n--[ Processing Target: $Target ]-----------------------------------" -ForegroundColor Yellow }
    $Count = 1     
    
    $Cmd = 'df -h'             #--[ The command to run over SSH ]--
    #--[ Some optional other commands that could be used ]--
    # '/usr/sbin/zpool list'
    # 'cat /var/log/tegile/zebi/zebirep.log | egrep "has completed|has failed"' #--[ to purge replication logs ]--
    # '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 ]--
    Remove-SSHSession -SessionId 1 -ErrorAction SilentlyContinue | Out-Null     #--[ Clear out previous session if it still exists ]--
    $Script:Converted = ""
    $secpasswd = ConvertTo-SecureString $Script:Password -AsPlainText -Force
    $mycreds = New-Object System.Management.Automation.PSCredential ($Script:UserName, $secpasswd)
    $SSH = New-SshSession -ComputerName $Target -Credential $mycreds -AcceptKey:$true # | Out-Null #--[ Open new SSH session ]--
    
    $Script:Converted = $(Invoke-SSHCommand -SSHSession $SSH -Command $Cmd ).Output      #--[ Invoke SSH command and capture the output as a string ]--

    $Col = 1
    $Counter = 1
    ForEach ($Line in $Script:Converted ){
    $Line = $Line -Replace ('\s+', ' ')
    $Line = $Line -Split " "
    if ($Console){Write-Host " Current input line :"$Line -ForegroundColor cyan }
    if ($Console){write-host " Input pool list :"$Script:PoolList.Pool -ForegroundColor Red }
    if ($Console){write-host " Match criteria :"$Line.split("/")[0] -ForegroundColor Magenta} 
    
    if ($Script:PoolList.Pool -contains($Line.split("/")[0]) -and ($Line.split(" ")[5].split("/")[1] -Like "export" )){
        if ($Console){Write-Host " -- Match found, using line. --" -ForegroundColor yellow}
        if ($Counter -gt 1){                     #--[ Dump collected data into spreadsheet ]--
            #--[ Controller ]-------------------------------------------------------
            $WorkSheet.Cells.Item($Row,$Col) = "$Script:Target"
            $Col++
            #--[ FileSystem ]-------------------------------------------------------
            $WorkSheet.Cells.Item($Row,$Col) = $Line[0]
            $Col++        
            #--[ Size ]-------------------------------------------------------------
            $Return = Convert $Line[1]
            $WorkSheet.Cells.Item($Row,$Col) = $Script:Converted
            #$WorkSheet.Cells.Item($Row,$Col) = $Line[1]
            $Col++
            #--[ Used ]-------------------------------------------------------------
            $Return = Convert $Line[2]
            $WorkSheet.Cells.Item($Row,$Col) = $Script:Converted
            #$WorkSheet.Cells.Item($Row,$Col) = $Line[2]
            $Col++
            #--[ Available ]--------------------------------------------------------
            $Return = Convert $Line[3]
            $WorkSheet.Cells.Item($Row,$Col) = $Script:Converted
            #$WorkSheet.Cells.Item($Row,$Col) = $Line[3]
            $Col++
            #--[ Capacity ]---------------------------------------------------------
            $WorkSheet.Cells.Item($Row,$Col) = $Line[4]
            $Col++
            #--[ MountPoint ]-------------------------------------------------------
            $WorkSheet.Cells.Item($Row,$Col) = $Line[5]
            $Col++

            sleep -milliseconds 100
            $Line = ""
            $RepJob = ""
            $Status = ""
            $Col = 1
            $Resize = $WorkSheet.UsedRange
            [void]$Resize.EntireColumn.AutoFit()
        }
        $Row++
    }Else{
        if ($Console){Write-Host " -- No Match found. --" -ForegroundColor DarkGray }
    }
    $Counter ++    
    }
Remove-SSHSession -SessionId 0 -ErrorAction SilentlyContinue | Out-Null 
Remove-SSHSession -SessionId 1 -ErrorAction SilentlyContinue | Out-Null 
}


#--[ Populate FREE and USED tabs ]-------------------------------------
$Col = ((Get-Date).Day)+1   
$Row = 1
#--[ Copy Filesystem Names ]---
$Worksheet0 = $workbook.WorkSheets.Item($DateToday)
$Range0A = $WorkSheet0.Range("B1").EntireColumn()
#--[ Paste Filesystem Names to USED ]---
$Range0A.Copy()
$Worksheet1 = $Workbook.Worksheets.Item("Used")
$Range1A = $WorkSheet1.cells.Item(1,1)
$Worksheet1.Paste($Range1A)
#$WorkSheet1.cells.Item($Row,$Col) = $Col
#--[ Copy Todays Data, Paste to USED ]--
$Range1B = $WorkSheet0.Range("D2").EntireColumn()
$Range1B.Copy()
$Range1B = $WorkSheet1.cells.Item($Row,$Col)
$Worksheet1.Paste($Range1B)
$WorkSheet1.cells.Item($Row,$Col) = ($Col-1)
#--[ Paste Filesystem Names to FREE ]---
$Range0A.Copy()
$Worksheet2 = $Workbook.Worksheets.Item("Free")
$Range2A = $WorkSheet2.cells.Item(1,1)
$Worksheet2.Paste($Range2A)
#$WorkSheet2.cells.Item($Row,$Col) = $Col
#--[ Copy Todays Data, Paste to FREE ]--
$Range2B = $WorkSheet0.Range("E2").EntireColumn()
$Range2B.Copy()
$Range2B = $WorkSheet2.cells.Item($Row,$Col)
$Worksheet2.Paste($Range2B)
$WorkSheet2.cells.Item($Row,$Col) = ($Col-1)


#--[ Save and Quit ]--
$Workbook.SaveAs("$PSScriptRoot\$Script:ThisMonth.xlsx")
$Excel.Quit()
$Excel = $Null

If (Test-Path "$PSScriptRoot\$Script:ThisMonth-old.xlsx"){Remove-Item -Path "$PSScriptRoot\$Script:ThisMonth-old.xlsx" -Confirm:$false -Force:$true}

Sleep -Seconds 2
#If ($Script:ThisDay -eq "Monday"){
SendEmail #} #--[ Email report on Sundays ]--

[gc]::Collect() | Out-Null 
[gc]::WaitForPendingFinalizers() | Out-Null 

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

<#-----------------------------[ Config File ]---------------------------------
 
The configuration file must be named the same as the script with ".xml" and must reside in
the same folder as the script. Below is the format and element list:
 
<!-- Settings & Configuration File -->
<Settings>
    <General>
        <ScriptName>SAN-Utilization-Tracker</ScriptName>
        <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>
        <Pools>
            <Pool>Pool1</Pool>
            <Pool>Pool2</Pool>
        <Pools>
        <Weeks>2</Weeks>
        <Day2Email>Sunday</Day2Email> <!-- for additional days list the day name seperated by a comma -->
    </General>
    <Email>
        <Enable>$true</Enable>
        <From>SANReports@mydomain.com</From>
        <To>me@mydomain.com</To>
        <Subject>Zebi Utilization Status Report</Subject>
        <HTML>$true</HTML>
        <Attach>$true</Attach>
        <SmtpServer>10.10.1.10</SmtpServer>
    </Email>
    <Credentials>
        <UserName>zebiadminuser</UserName>
        <Password>zebiadminpwd</Password>
    </Credentials>
</Settings>
 
 
#>