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!
[…] 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 […]
Thx for the hints Kevin, great script of yours also!
I will surely (try to) incorporate some of your “tricks” into my script (this one or some others that are currently “in the pipeline”)
Just need to “make some time” 😉
Great script, any chance you can specify multiple clusters? Or on the other hand filter clusters from the ‘all’ list?
Many thanks for the script. FYI, it took about an hour to return 4976 vdisks worth of information (about 1500 VMs). Cheers!
Would it be possible to add a column indicating if the disk in Thick or Thin provisioned?
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 »