Guest OS disk usage script revisited.

Shares

I was recently asked to re-write my good ol’ script that retrieves disk usage from virtual machines and formats this information into nice’n’tidy CSV report.

My “customer” wanted to have some more data included in the report, pretty generic stuff like name of vSphere Cluster (and host – I dunno why?) where the VM was running at the moment of report creation, name of vSphere datastore holding the .vmdk file(s) and some of the information, that this “customer” puts in virtual machine annotations (not tags… at least not yet 😉 ).

On top of that I was asked to change the layout of the report, so that one line represents information about a single disk (filesystem) of VM.

At first I was like: “But what is wrong with old layout!? The one introduced by Alan Renouf, with one VM per line and disk information in columns?”.

Then I realized that creating “one disk per line” report makes perfect sense actually.
With a layout like this (using any spreadsheet application of your choice) you can group your information by vSphere cluster, or by datastore, you can retrieve “grand total” of disk space used (wasted?) in your virtual infrastructure, you can even group information “per C:\ drive”, to see if your sizing for Windows “system drives” is correct!

W/o much further ado – let’s have a look at the script itself:

<#
.SYNOPSIS
   Script generates guest_diskspace_report.csv file containing information about diskspace usage as seen from guest OS
.DESCRIPTION
   Script connects to vCenter server passed as parameter and enumerates virtual machines from cluster passed as parameter.
   VM Templates and vms without Vmware Tools running are excluded cause it is impossible to retrieve disk usage from them
   For remaining vms disk capacity, free space and percent of free space are retrieved and saved to .csv file, one disk (filesystem) per line.
   Additional information like datastore name or selected annotations (creator, service name) are also provided so that you can group data as needed.
   
.PARAMETER vCenterServer
   Mandator parameter indicating vCenter server to connect to (FQDN or IP address)
.PARAMETER ClusterName
   Mandator parameter indicating host cluster to generate report for, default value is "all" which triggers report for all clusters defined in given vCenter
.EXAMPLE
   check-guestdiskspace.ps1 -vCenterServer vcenter.seba.local -ClusterName Production-Cluster
   
   Generate report for specified cluster and vCenter passed by FQDN.
   
.EXAMPLE
   check-guestdiskspace.ps1 -vcenter 10.10.10.1
   
   Generate report for all clusters for vCenter passed as IP address
   
.EXAMPLE
   check-guestdiskspace.ps1
	
   Generate report with default parameters
#>

[CmdletBinding()]
Param(
  [Parameter(Mandatory=$false,Position=1)]
   [string]$vCenterServer = "vcenter.seba.local",
	
   [Parameter(Mandatory=$false)]
   [string]$ClusterName = "all"
)

Function Write-And-Log {

[CmdletBinding()]
Param(
   [Parameter(Mandatory=$True,Position=1)]
   [ValidateNotNullOrEmpty()]
   [string]$LogFile,
	
   [Parameter(Mandatory=$True,Position=2)]
   [ValidateNotNullOrEmpty()]
   [string]$line,

   [Parameter(Mandatory=$False,Position=3)]
   [int]$Severity=0,

   [Parameter(Mandatory=$False,Position=4)]
   [string]$type="terse"

   
)

$timestamp = (Get-Date -Format ("[yyyy-MM-dd HH:mm:ss] "))
$ui = (Get-Host).UI.RawUI

switch ($Severity) {

        {$_ -gt 0} {$ui.ForegroundColor = "red"; $type ="full"; $LogEntry = $timestamp + ":Error: " + $line; break;}
        {$_ -eq 0} {$ui.ForegroundColor = "green"; $LogEntry = $timestamp + ":Info: " + $line; break;}
        {$_ -lt 0} {$ui.ForegroundColor = "yellow"; $LogEntry = $timestamp + ":Warning: " + $line; break;}

}
switch ($type) {
   
        "terse"   {Write-Output $LogEntry; break;}
        "full"    {Write-Output $LogEntry; $LogEntry | Out-file $LogFile -Append; break;}
        "logonly" {$LogEntry | Out-file $LogFile -Append; break;}
     
}

$ui.ForegroundColor = "white" 

}


#variables
$ScriptRoot = Split-Path $MyInvocation.MyCommand.Path
$now = get-date
$StartTime = $now.ToString("yyyyMMddHHmmss_")

$logdir = $ScriptRoot + "\CheckGuestDiskSpaceLogs\"
$logfilename = $logdir + $StartTime + "Check-GuestDiskSpace.log"
$transcriptfilename = $logdir + $StartTime + "Check-GuestDiskSpace_Transcript.log"
$csvfile = $logdir + "guest_diskspace_report_for_$($ClusterName)_clusters.csv"

$all_guestdisks_info =@()
$filter = "Contact", "Project", "Service"

#test for log directory, create if needed
if ( -not (Test-Path $logdir)) {
			New-Item -type directory -path $logdir 2>&1 > $null
}

$vmsnapin = Get-PSSnapin VMware.VimAutomation.Core -ErrorAction SilentlyContinue
$Error.Clear()
if ($vmsnapin -eq $null) 	
	{
	Add-PSSnapin VMware.VimAutomation.Core
	if ($error.Count -eq 0)
		{
		write-and-log $logfilename "PowerCLI VimAutomation.Core Snap-in was successfully enabled." 0 "full"
		}
	else
		{
		write-and-log $logfilename "Could not enable PowerCLI VimAutomation.Core Snap-in, exiting script" 1 "full"
		Exit
		}
	}
else
	{
	write-and-log $logfilename "PowerCLI VimAutomation.Core Snap-in is already enabled" 0 "full"
	}

$Error.Clear()
#connect vCenter from parameter
Connect-VIServer -Server $vCenterServer -ErrorAction SilentlyContinue 2>&1 > $null

#execute only if connection successful
if ($error.Count -eq 0){
	
	write-and-log $logfilename "vCenter server $vCenterServer successfully connected" 0 "full"
	
	if ($ClusterName -eq "all") {
		$all_clusters = get-cluster | sort-object -Property Name
		write-and-log $logfilename "Processing guest OS disk information for all clusters" 0 "full"
	}
	else {
		$all_clusters = get-cluster -name $ClusterName
		write-and-log $logfilename "Processing guest OS disk information for $ClusterName cluster" 0 "full"
	}
	
	$all_clusters | 
	select-object @{N="ClusterName"; E= {$_.Name}}, @{N="VMs"; E= {@($_ | get-vm | where-object { (-not $_.Config.Template) -and ($_.ExtensionData.Guest.ToolsRunningStatus -match "guestToolsRunning") })}} |
	select-object ClusterName -ExpandProperty VMs |
	select-object @{N="VirtualMachineName"; E= {$_.Name}}, ClusterName, @{N="VMHostName"; E= {$_.VMHost.Name}}, @{N="DatastoreName"; E= {(get-view -id $_.DatastoreIdList[0]).name}}, @{N="Annotations"; E={@($_ | get-annotation | where-object {$filter -contains $_.name})} } -ExpandProperty Guest |
	select-object * -ExpandProperty Disks | Sort-Object VirtualMachineName, Path |
	select-object VirtualMachineName, ClusterName, VMHostName, DatastoreName, @{N="DiskPath"; E= {$_.Path}}, @{N="DiskCapacity(GB)"; E= {([math]::Round($_.Capacity/ 1GB))}}, @{N= "DiskFreeSpace(GB)"; E= {([math]::Round($_.FreeSpace / 1GB))}}, @{N="DiskFreeSpace(%)"; E= {([math]::Round(((100* ($_.FreeSpace))/ ($_.Capacity)),0))}}, @{N="Contact"; E= {$_.Annotations[0].Value}}, @{N="Project"; E= {$_.Annotations[1].Value}}, @{N="Service"; E= {$_.Annotations[2].Value}} |
	Export-Csv -Path $csvfile -NoTypeInformation
	
	write-and-log $logfilename "Report successfully created in $($csvfile)" 0 "full"

	#disconnect vCenter
	Disconnect-VIServer -Confirm:$false -Force:$true 2>&1 > $null
}
else{

write-and-log $logfilename "Error connecting vCenter server $vCenterServer, exiting" 1 "full"

}

$filter array that I define in Line 94 is just a set of names of annotation fields that I was asked to put into report.

The real kung-fu happens between Line 139 and Line 145 and this is probably the longest and least readable “one liner” I’ve ever committed. (and I don’t really like one liners, alright?).
The thing is – the for-each loop from original script was taking ages when extended with retrieving annotations etc., so I was looking for a way to speed it up and take advantage of PowerShell’s (attempted) parallel processing during pipe “execution”.

And it helped… A little…

Now it takes takes round 30 minutes, to put this report together in an “example infrastructure” of 1000 VMs (it was close to one hour before).
But it is still a lot of time and I really have to try to use Get-View somehow, to bring execution time to some reasonable levels… (Any hints? Please provide them in the comments!)

I owe you some explanation on how I retrieve the datastore name in  Line 142 (just because I’m “cheating” here a little).
The data structure $vm.DatastoreIdList is an array of “Managed Object References” for datastores holding VM files.
To obtain “human readable” name, I reach for the “Name” property of first element in this array. And as you probably noticed I do this only once per VM (not – per disk!).
There is no issue with this method, as long as your VM resides on a single datastore (which is 80% of the cases, I think), but if you (for whatever reason) decided to spread .vmdk files of your “monster VM” across many datastores, only the name of first datastore in this array will be included in the report (and I honestly hope this is the datastore where .vmx file is located).
Matching OS filesystem (disk), to .vmdk and then to datastore is (surprisingly!) not so easy task (you can find script of this kind in an excellent post from Arnim van Lieshout) and this routine would unnecessarily complicate my script and resulted in even longer execution times. So I decided against incorporating it here, especially that (in my opinion at least) .vmdk to datastore relation is not the most important information, we are looking for with this script.

The sample of “rearranged” Guest OS disk utilization report might look like that:

"VmName","ClusterName","VmHostName","DatastoreName","Disk path","Disk Capacity(GB)","Disk FreeSpace(GB)","Disk FreeSpace(%)","Contact","Project","Service"
"WINAD11","TestCluster02","esx02.seba.local","iSCSI-BiG","C:\","25","11","45","John Doe","","Windows AD"
"WINAD11","TestCluster02","esx02.seba.local","iSCSI-BiG","D:\","1","1","94","John Doe","","Windows AD"
"WINAD11","TestCluster02","esx02.seba.local","iSCSI-BiG","G:\","1","1","94","John Doe","","Windows AD"
"WINAD11","TestCluster02","esx02.seba.local","iSCSI-BiG","H:\","1","1","91","John Doe","","Windows AD"
"WINAD11","TestCluster02","esx02.seba.local","iSCSI-BiG","I:\","1","0","43","John Doe","","Windows AD"
"linsqd01","TestCluster02","esx02.seba.local","iSCSI-BiG","/","16","11","69","","",""
"linsqd01","TestCluster02","esx02.seba.local","iSCSI-BiG","/boot","2","2","93","","",""
"linsqd01","TestCluster02","esx02.seba.local","iSCSI-BiG","/global","10","9","89","","",""
"linsqd01","TestCluster02","esx02.seba.local","iSCSI-BiG","/tmp","4","4","95","","",""
"linsqd01","TestCluster02","esx02.seba.local","iSCSI-BiG","/var","8","6","80","","",""

As you can see even John Doe himself does not fill all the annotations required by the report 😉

That’s it for this episode – I hope you will find this post useful, feel free to share it and let me know, if you have any comments!

0 0 votes
Article Rating

Sebastian Baryło

Successfully jumping to conclusions since 2001.

You may also like...

Subscribe
Notify of
guest
7 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments

[…] You might be also interested in an revised version of this script, that I posted here […]

[…] OS disk usage script revisited This is a very nice PowerCLI script that shows you the disk in use inside a VM along with other info.  Definitely a script in my […]

Dave Begic

Great script, any chance you can specify multiple clusters? Or on the other hand filter clusters from the ‘all’ list?

Steve Buckley

Many thanks for the script. FYI, it took about an hour to return 4976 vdisks worth of information (about 1500 VMs). Cheers!

Paul Webb

Would it be possible to add a column indicating if the disk in Thick or Thin provisioned?

Rob Stickland

Split-Path : Cannot bind argument to parameter ‘Path’ because it is null. At line:84 char:26 + $ScriptRoot = Split-Path $MyInvocation.MyCommand.Path + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidData: (:) [Split-Path], ParameterBindingValidationException + FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.SplitPathCommand Add-PSSnapin : No snap-ins have been registered for Windows PowerShell version 5. At line:105 char:2 + Add-PSSnapin VMware.VimAutomation.Core + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidArgument: (VMware.VimAutomation.Core:String) [Add-PSSnapin], PSArgumentException + FullyQualifiedErrorId : AddPSSnapInRead,Microsoft.PowerShell.Commands.AddPSSnapinCommand Major Minor Build Revision —– —– —– ——– 5 1 14409 1012 Script 6.7.0.8… VMware.DeployAutomation {Add-DeployRule, Add-ProxyServer, Add-ScriptBundle, Copy-DeployRule…} Binary 6.5.1.5… VMware.DeployAutomation {Add-DeployRule, Add-ProxyServer, Add-ScriptBundle, Copy-DeployRule…} Script 6.7.0.8… VMware.ImageBuilder {Add-EsxSoftwareDepot, Add-EsxSoftwarePackage, Compare-EsxImageProfil… Binary 6.5.1.5… VMware.ImageBuilder {Add-EsxSoftwareDepot,… Read more »

7
0
Would love your thoughts, please comment.x
()
x

FOR FREE. Download Nutanix port diagrams

Join our mailing list to receive an email with instructions on how to download 19 port diagrams in MS Visio format.

NOTE: if you do not get an email within 1h, check your SPAM filters

You have Successfully Subscribed!

Pin It on Pinterest