Sunday, December 7, 2014

PowerShell domain disk statistics report

Ever needed an automated disk statistics report for your Windows domain?  I did.  I was able to gather bits and pieces from around the Internet and create the script below.

It's designed for my needs but can be easily edited to suit yours.  The basic functionality is to use WMI via PowerShell to poll servers, PCs, or both and grab disk usage for any system within the targeting parameters and generate an easy to read HTML report that is emailed to anyone needed.

Our environment is comprised of both physical and virtual systems.  I need to be able to spot systems running low on disk space with expandable disks (virtual) that can be quickly grown if needed.  The output color codes C: drives below 60 GB in red, that's my low threshold.  It also color codes physical verses virtual.  You can select between a full report of every system, or a brief option for only systems below threshold.  You can also filter by server or PC and turn console output on and off for debugging.

The script is internally documented.  It emails an HTML formatted report that shows each disk, its size, how much is used, and how much is left.

I run it as a daily scheduled job from a service account with domain admin rights.

I also have a variation of this report that details the disk block size.  Very handy when tuning your virtual disks for use on a SAN.  I'll publish that one later.

UPDATE: I altered the commandline arguments to be named.

Below is an example of the output as seen in email:





<#====================================================================
         File Name : DomainDiskReport.ps1
   Original Author : Kenneth C. Mazie
                   :
       Description : This script uses WMI to poll the current domain and extract
                   : disk statistics. Output is gathered in HTML format and emailed
                   : to a list of recipients.  Output can be all systems (full)
                   : or only systems that are below preset threshold (brief).
                   :
         Arguments : Named commandline parameters:  (all are optional)
                   :   "-debug" - Displays console output during run.  Switches email recipient.
                   :   "-mode" - Can be set to "full" or "brief". Default to "brief".
                   :   "-detail" - Can be "all", "pc", or "server".  Defaults to "all" which polls both.
                   :
     Notes : Allows setting a threshold of the lowest remaining space.  Colors all
                   :   others in red.
                   : Numerous parameters are adjustable from within the main function.
                   : !!! -- Best run from a scheduled job as a domain admin user -- !!!
                   :
          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 - 05-03-14 - Original
    Change History : v2.0 - 11-01-14 - Changed HTML formatting. Added commandline options.
                   : v3.0 - 12-10-14 - Changed input arguments to be named
                   :
====================================================================#>

#--[ Set input variables ]--
Param (
   [switch]$Debug = $False,            #--[ Debug mode off unless specified ]--
   [string]$Detail = "brief",          #--[ Defaults to brief output ]--
   [string]$Mode = "all"               #--[ Defaults to reporting both server and PC ]--
 )

Clear-Host


new-variable -force -name startupVariables -value ( Get-Variable | % { $_.Name } )  

$startupVariables = ""

$ErrorActionPreference = "silentlycontinue"      
$Datetime = Get-Date -Format "MM-dd-yyyy_HH:mm"
$Global:eMailRecipient = "your full email here" 
$DebugEmailTarget = "your full email for debugging"
$Global:SMTPServer = "your-email-server"                     #--[ Email server address ]-

if (!(Get-Module -Name ActiveDirectory)){Import-Module ActiveDirectory}     #--[ Load the AD module if needed ]--


If ($Debug){$Global:eMailRecipient = $DebugEmailTarget}          #--[ Destination email address during debug. ]--

Function Cleanup-Variables {
  Get-Variable | Where-Object { $startupVariables -notcontains $_.Name } | % { Remove-Variable -Name "$($_.Name)" -Force -Scope "global" }
}

function Hide-Console {                               #--[ Stops console from displaying ]--
$consolePtr = [Console.Window]::GetConsoleWindow()
[void][Console.Window]::ShowWindow($consolePtr, 0)
}
hide-console

Function SendEmail {
   $email = New-Object System.Net.Mail.MailMessage
   $email.From = "DiskReport@$eMailDomain"
   $email.IsBodyHtml = $True
   $email.To.Add($Global:eMailRecipient)
   $email.Subject = "PowerShell Daily Disk Status Report"
   $email.Body = $Global:ReportBody
   $smtp = new-object Net.Mail.SmtpClient($Global:SMTPServer)
   $smtp.Send($email)
   If ($Debug){Write-Host "`n`rEmail sent..."}
}

Function Process ($Global:Target){
   $Flag = $False
   $Size = "";$Freespace="";$PercentFree="";$SizeGB="";$FreeSpaceGB="";$Win32_Hardware = "";$Win32_OS = ""
   $Global:Target=$Global:Target.Name
   $PercentWarning = "10"                    #--[ Issue warning if free disk space is below this % ]--
   $TargetHash = @{"Target" = $Global:Target}
 
   If(Test-Connection -ComputerName $Global:Target -count 1 -BufferSize 16 -ErrorAction SilentlyContinue ) {
      #--[ Gather system data ]----------------------------------------------------------------------
      $WMIJob = Get-WMIObject win32_logicaldisk -Filter "DriveType = 3" -ComputerName $Global:Target -AsJob
      Wait-Job -ID $WMIJob.ID -Timeout 5 | Out-Null
      $Disks = Receive-Job $WMIJob.ID -ErrorAction SilentlyContinue
      #--------------------------------------------------------------------------------------------
      $WMIJob = Get-WMIObject Win32_OperatingSystem -computer $Global:Target -AsJob
      Wait-Job -ID $WMIJob.ID -Timeout 5 | Out-Null
      $Win32_OS = Receive-Job $WMIJob.ID -ErrorAction SilentlyContinue | select Caption
      $Win32_OS = ($Win32_OS.caption -replace("\(R\)","")).Replace("Microsoft ","")
      $TargetHash.Add("OS",$Win32_OS)
      #--------------------------------------------------------------------------------------------
      $WMIJob = Get-WMIObject -ComputerName $Global:Target -class Win32_ComputerSystemProduct -AsJob
      Wait-Job -ID $WMIJob.ID -Timeout 5 | Out-Null
      $Win32_Hardware = Receive-Job $WMIJob.ID -ErrorAction SilentlyContinue       
      if(([string]::IsNullOrEmpty($Win32_Hardware.Name)) -or ($Win32_Hardware.Name -eq " ")){
         $MFG = "Unknown"
      }ElseIf ($Win32_Hardware.Name -eq "VMware Virtual Platform"){
         $MFG = "VMware Virtual"
      }Else{
         $MFG = $Win32_Hardware.Name.trimend()
      }
      $TargetHash.Add("MFG",$MFG)
      #--------------------------------------------------------------------------------------------
      $Count = 0
      foreach($Disk in $Disks){
         $DeviceID = $Disk.DeviceID;
         [float]$Size = $Disk.Size;
         [float]$Freespace = $Disk.FreeSpace;
         [string]$PercentFree = [Math]::Round(($Freespace / $Size) * 100, 0);
         [string]$SizeGB = [Math]::Round($Size / 1073741824, 0);
         [string]$FreeSpaceGB = [Math]::Round($Freespace / 1073741824, 2);                
         $DriveHash =  @{"Drive" = $DeviceID;"Size" = $SizeGB;"PercFree" = $PercentFree;"FreeSpace" = $FreeSpaceGB}
         $TargetHash.Add($Count,$DriveHash)  
         $Count++
      }  #--[End - ForEach disk ]--      

      #----------------------------------------- START OF OUTPUT -----------------------------------------------------
      If(($Global:Target -like "*whatever*") -and ($TargetHash.Item(0).Item("PercFree") -gt 10 )){   #--[ Bypass a name pattern ]--
      If ($Debug){
         Write-Host "`r`n--[ " -ForegroundColor White -NoNewline
         write-host $Global:Target.Toupper() -ForegroundColor Yellow -NoNewline
         Write-Host " ]-------------------------------------------------------------------".PadRight((110-$Global:Target.length),"-") -ForegroundColor White
         Write-Host $Global:Target " BYPASS MATCH.  c: %free " ($TargetHash.Item(0).Item("PercFree"))-ForegroundColor Magenta}
      }else{
         If ($Debug){
            Write-Host "`r`n--[ " -ForegroundColor White -NoNewline
            write-host $Global:Target.Toupper() -ForegroundColor Yellow -NoNewline
            Write-Host " ]-------------------------------------------------------------------".PadRight((110-$Global:Target.length),"-") -ForegroundColor White
         }         
         $HtmlData += '<td><font color="blue">' + $Global:Target + '</td>'
         If([string]::IsNullOrEmpty($Win32_OS)){$Win32_OS = "Unknown"}
         If (($Win32_OS -eq "Unknown") -and ($MFG -eq "Unknown")){
            If ($Debug){Write-Host "Failed to extract any information from target system..." -ForegroundColor cyan}
            $HtmlData += '<td colspan=2><center><font color="darkcyan">Failed to extract any information from target system.</center></td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td></tr>'
         }Else{
            If (($Win32_OS -like "*R2*") -or ($Win32_OS -like "*2012*") -or ($Win32_OS -like "*Windows 7*") -or ($Win32_OS -like "*Embedded*")){
            If ($Debug){Write-Host "OS = $Win32_OS".PadRight(50," ") -ForegroundColor Green -NoNewline }
            $HtmlData += '<td><font color="green">' + $Win32_OS + "</font></td>"
         }Else{
            #If([string]::IsNullOrEmpty($Win32_OS)){$Win32_OS = "Unknown"}
            If ($Debug){Write-Host "OS = $Win32_OS".PadRight(50," ") -ForegroundColor Red -NoNewline }
            $HtmlData += '<td><font color="red">' + $Win32_OS + "</font></td>"
         }         
                     
         If($MFG -eq "VMware Virtual")                  #--[ Unique coloring for VM ]--
            If ($Debug){Write-Host "Hardware = $MFG" -ForegroundColor cyan}
            $HtmlData += '<td><font color="green">VMware Virtual</font></td>'
         }Else{
            If ($Debug){Write-Host "Hardware = $MFG" -ForegroundColor Magenta}
            $HtmlData += '<td><font color="red">' + $MFG + '  </font></td>'
         }
          
         $Count = 0
         While ($Count -lt $TargetHash.count-3 ){
            #$TargetHash.Item($Count).Item("Drive")     #--[ Extra items if needed ]--
            #$TargetHash.Item($Count).Item("Size")
            #$TargetHash.Item($Count).Item("FreeSpace")
            #$TargetHash.Item($Count).Item("PercFree")
            If ($Debug){Write-Host "Drive =" $TargetHash.Item($Count).Item("Drive").PadRight(15," ") -NoNewline }
                                                          
            If ($Count -gt 0){
               $HtmlData += "<td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>" + $TargetHash.Item($Count).Item("Drive") +"</td>"
            }Else{
               $HtmlData += "<td>" + $TargetHash.Item($Count).Item("Drive") + "</td>"
            }
                                             
            #--[ report if server C: drive is less than 60 gb - #$TargetHash.Item($Count).Item("Size") ]--
            If (($DeviceID -eq "C:") -and ([int]$TargetHash.Item($Count).Item("Size") -lt 59) -and ($Win32_OS -like "*Server*")){
               If ($Debug){Write-Host "Size = "($TargetHash.Item($Count).Item("Size").PadRight(20," ")) -NoNewline -ForegroundColor red}
               $HtmlData += '<td><font color="red">' + $TargetHash.Item($Count).Item("Size") + '</font></td>'
            }Else{
               If ($Debug){Write-Host "Size = "($TargetHash.Item($Count).Item("Size").PadRight(20," ")) -NoNewline -ForegroundColor green}
               $HtmlData += '<td><font color="green">' + $TargetHash.Item($Count).Item("Size") + '</font></td>'
            }
         
            #--[ $TargetHash.Item($Count).Item("SpaceFree") ]--
            If ($Debug){Write-Host "FreeSpace =" $TargetHash.Item($Count).Item("FreeSpace").PadRight(20," ") -NoNewline }
            $HtmlData += "<td>" + $TargetHash.Item($Count).Item("FreeSpace") + "</td>"
                     
            #--[  $TargetHash.Item($Count).Item("PercFree") ]--
            If(([int]$TargetHash.Item($Count).Item("PercFree")) -lt ([int]$PercentWarning)){
               $Flag = $True
               If ($Debug){Write-Host "Percent Free = "(($TargetHash.Item($Count).Item("PercFree").PadLeft(2,"0")) + " %").PadRight(10," ") -ForegroundColor Red} # -NoNewline }
               $HtmlData += '<td><font color="red">' + $TargetHash.Item($Count).Item("PercFree") + ' %</font></td></tr>'
            }Else{
               If ($Debug){Write-Host "Percent Free = "(($TargetHash.Item($Count).Item("PercFree").PadLeft(2,"0")) + " %").PadRight(10," ") -ForegroundColor Green} # -NoNewline }
               $HtmlData += '<td><font color="green">' + $TargetHash.Item($Count).Item("PercFree") + ' %</font></td></tr>'
            }
            $Count++
            }
         }
      }#------------------ END OF OUTPUT ----------------------
      If (($Flag -and ($Detail -eq "brief")) -or ($Detail -ne "brief")){$Global:ReportBody += $HtmlData}
   }Else{
      If ($Debug){
         Write-Host "`r`n--[ " -ForegroundColor White -NoNewline
         write-host $Global:Target.Toupper() -ForegroundColor Yellow -NoNewline
         Write-Host " ]-------------------------------------------------------------------".PadRight((110-$Global:Target.length),"-") -ForegroundColor White
         Write-Host "Failed to sucessfully ping target system..." -ForegroundColor Cyan
      }         
      If($Detail -ne "brief"){   
         $Global:ReportBody += '<tr><td><font color="blue">' + $Global:Target + '</td><td colspan=2><center><font color="darkcyan">Failed to successfully ping target system.</center></td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td></tr>'
      }
      $Global:ReportBody += $HtmlData
   }  #--[ End - test-connection ]--
Return
}
#--[ End of Functions ]---------------------------------------------------------

$ExclusionList = @( #--[ Add systems here that come up as false positives or need to be excluded ]--
"server1",
"server2",
"pc1"
)

#--[ Add header to html log file ]--
$Global:ReportBody = @() | Select Target,Drive,SizeGB,FreeSpaceGB,PercentFree,Manufacturer
$Global:ReportBody += '<style type="text/css">
table.myTable { border:5px solid black;border-collapse:collapse; }
table.myTable td { border:2px solid black;padding:5px;background: #E6E6E6 }, table.myTable th { border:2px solid black;padding:5px;background: #A4A4A4 }
#table.bottomBorder { border-collapse:collapse; }
#table.bottomBorder td, table.bottomBorder th { border-bottom:1px dotted black;padding:5px; }
</style>
The following report displays disks configured on every server and PC in the domain.
<ul>
   <li>If the "brief" option was used only systems with disks in need of attention are listed.</li>
   <li>Operating systems that can be dynamically grown are shown in green, all others in red.</li>
   <li>Under the Hardware heading physical systems are noted in red since the disks cannot be grown.</li>
   <li>Any C: drives smaller than 60 GB are flagged in red.</li>
   <li>All other disks are red only if the remaining free disk space is below 10%.</li>
   <li>Systems that are not reachable for whatever reason are noted as such.</li>'

If($Detail -eq "brief"){
   $Global:ReportBody += '<li><strong>Note: Script was executed with the "brief" option enabled.</strong></li><br><br></ul>'
}Else{
   $Global:ReportBody += '</ul>'
}

$Global:ReportBody += 'Script executed on '+$Datetime+'<br><br>'

If (($Mode -eq "server") -or ($Mode -eq "all")){
   $Computers = Get-ADComputer -Filter {operatingsystem -like "*server*" -and name -notlike "*esx*"} | sort name
   $Global:ReportBody += '<table class="myTable"><tr><th>Target</th><th>Operating System</th><th>Hardware</th><th>Drive</th><th>SizeGB</th><th>FreeSpaceGB</th><th>PercentFree</th></tr>'
   ForEach($Global:Target in $Computers ){
      If ($ExclusionList -notcontains $Global:Target.name){
         Process $Global:Target
      }
   }
$Global:ReportBody += '</table><br><br>'
}

If (($Mode -eq "pc") -or ($Mode -eq "all")){
   $Computers = Get-ADComputer -Filter {operatingsystem -notlike "*server*" -and name -notlike "*esx*"} | sort name
   $Global:ReportBody += '<table class="myTable"><tr><th>Target</th><th>Operating System</th><th>Hardware</th><th>Drive</th><th>SizeGB</th><th>FreeSpaceGB</th><th>PercentFree</th></tr>'
   ForEach($Global:Target in $Computers ){
      If ($ExclusionList -notcontains $Global:Target.name){
         Process $Global:Target
      }
   }
   $Global:ReportBody += "</table>"
}

If ($Debug){$Global:ReportBody | Out-File c:\Scripts\temp.html}
SendEmail

#endregion
If ($Debug){Write-Host "--- COMPLETED ---" -ForegroundColor Red}

#Cleanup-Variables      #--[ Uncomment to reset pre-run variables ]--