Abstract

Monitoring disk space utilization of server(s) is the critical and important job for any administrator. Keeping things organized might improve application availability and server availability. This article takes us through the in-detail steps to read each drive and report every drive details based on threshold values and output data written into a CSV file. The step by step process quickly take us through the disk space utilization details of server(s). You'll basically feed a list of servers to watch over, and it will report back on these for you, meaning you could also use it as a more general "daily server disk space report"


↑ Return to Top


Introduction

This article talks about the use of credentials. The credentials can be used to query external servers which have the trust relationship between the domains. Also, list various methods to secure the password. The process iterates through a list of servers and drives that you have listed in a csv file. Checking for disk space status of every listed drive and its status may fall under one of the four statuses that is defined as critical, warning, low and good. If the disk in a question of below the threshold then the corresponding status is updated. The nice thing about this script is that it will consolidate health status of each listed disks and gives a summary that needs your attention (you set the threshold as per requirement because the size of the drive may vary from server to server). 

↑ Return to Top


Highlights

  • CSV input  - The file contains server, disk information and threshold values
  • The WMI class, Win32_LogicalDisk querying with credentials and without credentials
  • Activity logging in a log file
  • The output has status columns which give a clear indication of status of each drive
  • CSV output 
↑ Return to Top

Querying - Win32_LogicalDisks

  • Using Credentials 
  • Without using Credentials 

Using Credentials 

Get-credential always pop-up dialog box for entering a password, however, you can save your securestring password to a file or directly feed the password. The problem with this is that the password will be exposed to anyone with access to the file.

  • Using Get-Credential cmdlet - Pop up dialog box
  • Directly using password
  • Using secured file

Using Get-Credential cmdlet - Pop dialog box

The Get-Credential displays a window to enter credential details. This will appear every time when you run the script.The $credential variable store the username and password. It's then fed to the respective queries for further processing.

clear
$credential = Get-Credential 
foreach ( $args in get-Content c:\server.txt ) {
get-WmiObject win32_logicaldisk -Credential $credential -ComputerName $args -Filter "Drivetype=3"  |
ft SystemName,DeviceID,VolumeName,@{Label="Total SIze";Expression={$_.Size / 1gb -as [int] }},@{Label="Free Size";Expression={$_.freespace / 1gb -as [int] }} -autosize
}


Hard code the credentials 

The password is hard coded in the script. Of course, the problem with this is that your password will be exposed to anyone with access to the script file.

$User = 'hqnt\abcd'
 $Pass = ConvertTo-SecureString 'abcd@2015' -AsPlainText -Force
 $Credentials = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $User,$Pass
 foreach ( $args in get-Content c:\server.txt ) {
get-WmiObject win32_logicaldisk -ComputerName $args -Credential $Credentials -Filter "Drivetype=3"  |
ft SystemName,DeviceID,VolumeName,@{Label="Total SIze";Expression={$_.Size / 1gb -as [int] }},@{Label="Free Size";Expression={$_.freespace / 1gb -as [int] }} -autosize
}


Using Secured file

First, Password has to be written to a file

ps:\>read-host -AsSecureString |ConvertFrom-SecureString |Out-File C:\SecurePassword.txt


Second, The credentials are read from the file using PSCredential class. You don't need to re-enter the password over and over again.

clear
$User = 'hqnt\abcdv'
$pass= cat C:\passwordstring.txt |ConvertTo-SecureString
$Credentials = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $User,$Pass
foreach ( $args in get-Content c:\server.txt ) {
get-WmiObject win32_logicaldisk -ComputerName $args -Credentials $cred -Filter "Drivetype=3"  |
 
ft SystemName,DeviceID,VolumeName,@{Label="Total SIze";Expression={$_.Size / 1gb -as [int] }},@{Label="Free Size";Expression={$_.freespace / 1gb -as [int] }} -autosize
 
}


Without using Credentials

You don't need to use the credential parameter in any of the cmdlet execution. 


clear
foreach ( $args in get-Content c:\server.txt ) {
get-WmiObject win32_logicaldisk -ComputerName $args -Filter "Drivetype=3"  |
ft SystemName,DeviceID,VolumeName,@{Label="Total SIze";Expression={$_.Size / 1gb -as [int] }},@{Label="Free Size";Expression={$_.freespace / 1gb -as [int] }} -autosize
}




Download 

Download the source code from the Technet Gallery
https://gallery.technet.microsoft.com/PowerShell-CSV-Disk-Space-5992fb49

Code in detail

This section describes the coding and other details

Input File

The template of server.csv is given below. Change the content as per your requirement/environment

Server,Drive,LowTh,WarnTh,CritTh
HQDBSP008,E:,8,5,3
HQDBSP008,F:,8,20,3
HQDBSP0018,G:,8,5,3
HQSPDB9901,E:,8,5,3
HQSPDB0901,F:,20,5,3
HQSPDB8001,G:,8,5,3



Write-Log

Write-Log writes a message to a specified log file along with the current time stamp also writes state of the message(Information, Warning or Error).

For example, The first example writes a simple message with a default state to a log file abc.log. In the second example, a message along with "Error" state details are entered into the log file.

1..EXAMPLE 1
2.PS:\> Write-Log  -Message "Server is reachable and starting the process " -Logfile c:\PowerSQL\abc.log
3..EXAMPLE 2
4.PS:\> Write-Log  -level Error -Message "Server is not reachable " -Logfile c:\PowerSQL\abc.log

The below function can be reused to in any of the PoSH code. Also, the output file will be used for troubleshooting and activity progress tracking.


01.Function Write-Log {
02.    [CmdletBinding()]
03.    Param(
04.    [Parameter(Mandatory=$False)]
05.    [ValidateSet("INFO","WARN","ERROR")]
06.    [String]
07.    $Level = "INFO",
08. 
09.    [Parameter(Mandatory=$True)]
10.    [string]
11.    $Message,
12. 
13.    [Parameter(Mandatory=$False)]
14.    [string]
15.    $logfile
16.    )
17. 
18.    $Stamp = (Get-Date).toString("yyyy/MM/dd HH:mm:ss")
19.    $Line = "$Stamp $Level $Message"
20.    If($logfile) {
21.    Add-Content $logfile -Value $Line
22.    }
23.    Else {
24.        Write-Output $Line
25.    }
26.}


Output and filename 

This below code defines the output file location, output log file location, and directory to save the output. The $date variable hold the date formatting part. It's then appended to the $logfilename and $outputfile name to generate a more meaningful filename. For example, DiskSpaceLog_2016-10-10 and DiskSpace_CSV_2016-10-5

$date=Get-Date -format "yyyy-MM-d"

#Prepare log file and output CSV file
$LogFileName="DiskSpaceLog_$($date)"
$Filename="DiskSpace_CSV_$($date)"


Password 

This portion of code decides whether to pass credentials or not.  The Get-credential always pop-up dialog box for entering a password, however, you can save your securestring password to a file or directly feed the password. The problem with this is that the password will be exposed to anyone with access to the file. If you want to use the default login credentials then you don't need to mention anything in the code. You can comment the line of code.

$User = 'abcd'
$Pass = ConvertTo-SecureString ''abcd@#2016' -AsPlainText -Force
$Credentials = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $User,$Pass




Import Server and Drive details


This part of the code read values from CSV file. The CSV file has five columns Server, Drive, LowTh, WarnTh, CritTh. The server.csv file shown in the below figure has a specific set of values for each server as well as for each drive. The code reads values and loop through every server.After reading from CSV file, the values are assigned to a local variable and then it's used to query WMI query by filtering on -computername and -DeviceID which is marked green in color. The credentials parameter would be really helpful when you are querying different domains which has trust relationship  in between. If you are querying the servers that are under the same domain and want to use the default logon credentials then you can ignore the credential parameter.


The next part is to calculate the "free %". The status column is populated based on input threshold values. The $percentageFree will be used to identify the status of the drive. The four status of each drives are Critical, Warning, Low and Good. The three input parameters $clowth, $cwarnth,$ccritth compared with $percentageFree variable to yield a result for status columns. 


01.$diskinfo= Get-WmiObject -Class Win32_LogicalDisk -ComputerName $cserver  -Filter "DeviceID='$cdrivelt'"
02.ForEach ($disk in $diskinfo)
03.{
04.#Calculate the % free. This parameter will be compared with various thresholds to derive the status of the drive
05.If ($diskinfo.Size -gt 0) {$percentFree = [Math]::round((($diskinfo.freespace/$diskinfo.size) * 100))}
06.Else {$percentFree = 0}
07.    #Determine if disk needs to be flagged for warning or critical alert
08.    If ($diskinfo.Size -gt 0) {$percentFree = [Math]::round((($diskinfo.freespace/$diskinfo.size) * 100))}
09.    Else {$percentFree = 0}
10. If ($percentFree -le  $ccritth) { $status = "Critical" }
11. ElseIf ($percentFree -gt $ccritth -AND $percentFree -le $cwarnth) { $status = "Warning" }
12. ElseIf ($percentFree -ge $cwarnth -AND $percentFree -lt $clowth) { $status = "Low"                 
13.    } Else { $status = "Good" }
14.            
15.#Prepare the output with a custom object
16. 
17.    $mydisk +=New-Object PSObject -Property @{
18.    Server=$_.Server
19.    DeviceID= $disk.DeviceID
20.    VolumeName= $disk.VolumeName
21.    Size= [math]::Round(($disk.Size /1GB),2)
22.    Freespace= [math]::Round((($disk.Size - $disk.FreeSpace)/1GB),2)
23.    Percentage= ("{0:P}" -f ($disk.FreeSpace / $disk.Size))
24.    status=$status
25.    }
26.  
27.# The below code is used to preserve the order in the csv file
28. $mydisk |Select-Object  @{Name="Server";Expression={$_.Server}},@{Name="DeviceID";Expression={$_.DeviceID}},
29. @{Name="VolumeName";Expression={$_.VolumeName}},
30. @{Name="Size";Expression={$_.Size}},
31. @{Name="FreeSpace";Expression={$_.FreeSpace}},
32. @{Name="% Free";Expression={$_.Percentage}},
33. @{Name="Status";Expression={$_.status}}|Export-Csv $Filename -NoTypeInformation
34. #>
35. 
36. $mydisk |Export-Csv $Filename -NoTypeInformation




Known Issues


The issue with an output formatting i.e. is to ensure the output of an object to be displayed in a certain order because PoSH rearranges the output as per available metadata.
  • Control the output format using Custom accelerator
For example, Change the computer name in the below code. 
Line #18

By default, the columns are ordered depending on the available internal data. We can expect the order the output column.

Line #19

Manually forcing the output to maintain the order by creating a hash table, so that properties stay in the order as they were declared

01.#Creating PowerShell custom objects
02.$Mydisk=@()
03.$diskinfo= Get-WmiObject -Class Win32_LogicalDisk -ComputerName <computername> | where {$_.driveType -eq 3}
04.ForEach ($disk in $diskinfo)
05.{
06.#Prepare the output with a custom object
07.If ($diskinfo.Size -gt 0) {$percentFree = [Math]::round((($diskinfo.freespace/$diskinfo.size) * 100))}
08.Else {$percentFree = 0}
09.    $mydisk +=New-Object PSObject -Property @{
10.    Server=$_.Server
11.    DeviceID= $disk.DeviceID
12.    VolumeName= $disk.VolumeName
13.    Size= [math]::Round(($disk.Size /1GB),2)
14.    Freespace= [math]::Round((($disk.Size - $disk.FreeSpace)/1GB),2)
15.    Percentage= ("{0:P}" -f ($disk.FreeSpace / $disk.Size))
16.    }
17.}
18.$mydisk |format-table -AutoSize
19.$mydisk |Format-Table Server,DeviceID,VolumeName,Size,FreeSpace,Percentage -AutoSize


  • Control using field width and alignment
The below code is used to preserve the order in the csv file.



Line #22

Control the order of the columns using width and alignment



01.#Creating PowerShell custom objects
02.$Mydisk=@() 
03.$diskinfo= Get-WmiObject -Class Win32_LogicalDisk -ComputerName hqdbsp18| where {$_.driveType -eq 3}
04. 
05.ForEach ($disk in $diskinfo)
06.{
07.          
08.#Prepare the output with a custom object
09.If ($diskinfo.Size -gt 0) {$percentFree = [Math]::round((($diskinfo.freespace/$diskinfo.size) * 100))}
10.Else {$percentFree = 0}
11. 
12.    $mydisk +=New-Object PSObject -Property @{
13.    Server=$_.Server
14.    DeviceID= $disk.DeviceID
15.    VolumeName= $disk.VolumeName
16.    Size= [math]::Round(($disk.Size /1GB),2)
17.    Freespace= [math]::Round((($disk.Size - $disk.FreeSpace)/1GB),2)
18.    Percentage= ("{0:P}" -f ($disk.FreeSpace / $disk.Size))
19.     
20.    }
21.}
22.$mydisk |Select-Object  @{Name="Server";Expression={$_.Server}},@{Name="DeviceID";Expression={$_.DeviceID}},
23. @{Name="VolumeName";Expression={$_.VolumeName}},
24. @{Name="Size";Expression={$_.Size}},
25. @{Name="FreeSpace";Expression={$_.FreeSpace}},
26. @{Name="% Free";Expression={$_.Percentage}},
27. @{Name="Status";Expression={$_.status}}|Export-Csv $Filename -NoTypeInformation
28. 
29.  
30.$mydisk |format-table -AutoSize


In simple words, when you create a new custom object using "New-Object PSObject -Properties", the order of the properties were added is lost.  This is important because when you are writing a quick script, you probably aren't going to write a format file for it.  This means the order of the properties in the object is important and shouldn't be ignored.


New-Object PSObject -Property @{one=1;two=2;three=3}
Produces:
two three one
--- ----- ---
  2     3   1


As you can tell, the ordering of the properties is not the same as entered.  In order to get them in the correct order, I basically have to create the object twice.
New-Object PSObject -Property @{one=1;two=2;three=3} | Select one, two, three
Produces:
one two three
--- --- -----
  1   2     3


I understand that the reason for this is the use of a hash table for the Property parameter.  But just because there is a reason for something doesn't mean it should stay that way.  This really needs to be changed in the next version. 

 Output







Code



<#
 
.SYNOPSIS       
    Name :  Disk Space Utilization Report (Get-DiskSpaceExcel.ps1)
    Description : Get disk space usage information from remote server(s) with WMI and output CSV file
   
    Author : Prashanth Jayaram
      
    * Select list of servers from a CSV file
    * Get remote Servers informations with WMI and Powershell
    * Disk (Disk type, letter, capacity in GB, free space in GB, % free , Status + display a CSV output)
    * Log the details in activity log file
     
      
.INPUT
    .csv file with servers to activate
  
.OUTPUTS
    Console outputs : You can alter the code to write the data to file or console   
  
.NOTES
    Version:        1.0
    Author:         Prashanth Jayaram
    Creation Date:  2016-10-05
    Purpose/Change: Initial script development
    
.EXAMPLE
    .\Get-DiskSpaceCSV.ps1
#>
 
 
#########################################################################################
 
#### Input CSV, output CSV File, Log file Location
 
param (
    [Parameter(Mandatory=$true)][string]$inputFile,
    [Parameter(Mandatory=$true)][string]$DirectorytoSave
 )
  
#Assign arguments to variables
 
 $InputCSV=$inputFile
 $DirectoryToSaveTo = $DirectorytoSave
  
# formatting the date
 
 $date=Get-Date -format "yyyy-MM-d"
  
 #Prepare log file and output CSV file
  
 $LogFileName="DiskSpaceLog_$($date)"
 $Filename="DiskSpace_CSV_$($date)"
  
# before we do anything else, are we likely to be able to save the file?
# if the directory doesn't exist, then create it
 
if (!(Test-Path -path "$DirectoryToSaveTo")) #create it if not existing
  {
  New-Item "$DirectoryToSaveTo" -type directory | out-null
  }
    
# Check if the output file CSV exist, if exists then delete it.
 
$filename = "$DirectoryToSaveTo$filename.csv"
if (test-path $filename ) { rm $filename }  
 
# check the existence of log file, If the log file doesn't exist, then create it  
 
$logfile = "$DirectoryToSaveTo$LogFileName.log"
 
if (!(Test-Path -path "$logfile")) #create it if not existing
  {
   New-Item -ItemType file $logfile -Force
  }
 
# Prepare headers for the log file for each execution of script
 
Add-Content $logfile "#################################################################"
Add-Content $logfile "Disk Space Details"
Add-Content $logfile "Generated $(get-date)"
Add-Content $logfile "Generated from $(gc env:computername)"
Add-Content $logfile "#################################################################"
  
<#
.Synopsis
   Write-Log writes a message to a specified log file along with the current time stamp also writes state of the message.
.DESCRIPTION
   The Write-Log function is designed to add logging capability to other scripts.
   In addition to writing output and/or verbose you can write to a log file for
   later debugging.
.NOTES
   Created by: Prashanth Jayaram
    
.PARAMETER Message
   Message is the content that you wish to add to the log file. 
.PARAMETER Level
   Specify the criticality of the log information being written to the log (i.e. Error, Warning, Informational)
.PARAMETER logfile
   The path to the log file to which you would like to write. By default the function will create the path and file if it does not exist. 
 .EXAMPLE 1
 PS:\> Write-Log  -Message "Server is reachable and starting the process " -Logfile c:\PowerSQL\abc.log
 .EXAMPLE 2
 PS:\> Write-Log  -level Error -Message "Server is not reachable " -Logfile c:\PowerSQL\abc.log
 
#>
 
 
Function Write-Log {
    [CmdletBinding()]
    Param(
    [Parameter(Mandatory=$False)]
    [ValidateSet("INFO","WARN","ERROR")]
    [String]
    $Level = "INFO",
 
    [Parameter(Mandatory=$True)]
    [string]
    $Message,
 
    [Parameter(Mandatory=$False)]
    [string]
    $logfile
    )
 
    $Stamp = (Get-Date).toString("yyyy/MM/dd HH:mm:ss")
    $Line = "$Stamp $Level $Message"
    If($logfile) {
    Add-Content $logfile -Value $Line
    }
    Else {
        Write-Output $Line
    }
}
 
 
#Creating PowerShell custom objects
 
$Mydisk=@()  
 
#Import the file to get the drives status and other usage details
   
Import-Csv $inputCSV|%{
$cserver = $_.Server
$cdrivelt = $_.Drive
$clowth = $_.LowTh
$cwarnth = $_.WarnTh
$ccritth = $_.CritTh
If (!(Test-Connection $_.Server -count 1 -quiet)) {
#Write the message to the log file
Write-Log  -level ERROR -Message "$($_.Server) is not reachable" -Logfile $Logfile
}
else
{
#Write the Progress to log file
Write-Log  -Message "$($_.Server) is reachable and starting the process " -Logfile $Logfile
 
$diskinfo= Get-WmiObject -Class Win32_LogicalDisk -ComputerName $cserver  -Filter "DeviceID='$cdrivelt'"
ForEach ($disk in $diskinfo)
{
#Calculate the % free. This parameter will be compared with various thresholds to derive the status of the drive
If ($diskinfo.Size -gt 0) {$percentFree = [Math]::round((($diskinfo.freespace/$diskinfo.size) * 100))}
Else {$percentFree = 0}
    #Determine if disk needs to be flagged for warning or critical alert
    If ($diskinfo.Size -gt 0) {$percentFree = [Math]::round((($diskinfo.freespace/$diskinfo.size) * 100))}
    Else {$percentFree = 0}
 If ($percentFree -le  $ccritth) { $status = "Critical" }
 ElseIf ($percentFree -gt $ccritth -AND $percentFree -le $cwarnth) { $status = "Warning" }
 ElseIf ($percentFree -ge $cwarnth -AND $percentFree -lt $clowth) { $status = "Low"                 
    } Else { $status = "Good" }
            
#Prepare the output with a custom object
 
    $mydisk +=New-Object PSObject -Property @{
    Server=$_.Server
    DeviceID= $disk.DeviceID
    VolumeName= $disk.VolumeName
    Size= [math]::Round(($disk.Size /1GB),2)
    Freespace= [math]::Round((($disk.Size - $disk.FreeSpace)/1GB),2)
    Percentage= ("{0:P}" -f ($disk.FreeSpace / $disk.Size))
    status=$status
    }
  
<# The below code is used to preserve the order in the csv file
 $mydisk |Select-Object  @{Name="Server";Expression={$_.Server}},@{Name="DeviceID";Expression={$_.DeviceID}},
 @{Name="VolumeName";Expression={$_.VolumeName}},
 @{Name="Size";Expression={$_.Size}},
 @{Name="FreeSpace";Expression={$_.FreeSpace}},
 @{Name="% Free";Expression={$_.Percentage}},
 @{Name="Status";Expression={$_.status}}|Export-Csv $Filename -NoTypeInformation
 #>
 
 $mydisk |Export-Csv $Filename -NoTypeInformation
 
}
}
 
}
   
invoke-item $Filename 


↑ Return to Top


Conclusion

  1. CSV input - Easy to maintain and manage 
  2. Customization can be done at the each drive level as the threshold value may vary on every server and most of the drive depending the size of each drive
  3. Simplest way to keep a cap on every drive to set threshold value
  4. Proactive monitoring and alerting respective teams may avoid unforeseen disk space issues

References

Technet 

  1. PoSH : Disk Space Utilization Report
  2. PoSH : CSV - Disk Space Report - HTML
  3. PoSH : CSV - Disk Space Report - Excel
  4. PoSH : DiskSpace GUI Tool
  5. PoSH : MultiServer(s) Disk Space GUI Tool
  6. PoSH & SQL : Monitoring Disk Space with SQL Server and PowerShell via SQL Agent