View Host Allocation by Cluster in PowerCLI

Viewing resource allocation at a cluster level is something we don’t do enough. In the past I would look at a specific host and see if there was an issue of over allocating memory or CPUs or when budgeting for the next year I would gather stats, but rarely did I save most of that information. One of the bigger mistakes I would make is looking at the cluster as a whole and grabbing the total RAM and CPU for the cluster and compare that against the total RAM and CPU allocated to the VMs that reside on that cluster and assume that the average was the number to base my calculations on. What that doesn’t take into account is things like DRS rules where certain VMs are pinned to a host, separated from each other, or DRS being disabled all together.

This started me down a path of creating a report to show what the current utilization was for each one of my clusters and then breaking that down to the hosts in each cluster so I could get an idea of how well my VMs were spread across a cluster.

I have a decent number of clusters I’m working with so the first thing we’ll do is get all the clusters in vCenter and sort them by name. I prefer doing a name sort so they appear in the order I’m used to looking at them in vCenter

$allClusters = Get-Cluster | Sort Name

Now we’ll open our ForEach loop for all the clusters. We make this a variable so we can see all the output once the script is completed. Then we’ll create an empty array.

$clusterOutput = ForEach ($cluster in $allClusters) {
$report = @()

We’ll get all the hosts in each cluster one cluster at a time and open another ForEach loop for each one of those hosts as well.

$allHosts = $cluster | Get-VMHost | Sort name
ForEach ($vmHost in $allHosts) {

We’re going to get all the VMs on each host now. I’m only concerned about powered on VMs, but depending on your environment you may want to omit the PowerState clause. Once we get all the VMs on a host, we want to calculate how much Memory is allocated and how many CPUs are allocated. The “Measure-Object -sum” will add those numbers together for us and we’ll call that number in the report.

$vms = $vmHost | Get-VM | Where {$_.PowerState -eq "PoweredOn"}
$vmMemSum = $vms.memoryGB | Measure-Object -sum
$vmCpuSum = $vms.NumCpu | Measure-Object -sum

Now that we have the total VM memory and CPU allocated for the host, we want to see the ratio of CPUs allocated to available on the host. We use the $ratio variable to capture this value, then use PowerShell math to divide the number of vCPUs allocated to the VMs by the number of pCPUs available on the host. We then round that number to 2 decimal places.

$ratio = [math]::round($vmCpuSum.sum/$vmhost.NumCpu,2)

With all the numbers captured we can start creating the table view by defining the column names. Host name, Host State, Host memory, VM Memory, Host CPUs, VM CPUs, and VM CPU to Host CPU value are what we’re interested in.

$row = "" | Select VMHost, State, "Host Memory", "VM Memory", "Host CPU", "VM CPU", "vCPU per pCPU"

To populate this table we use $row.<Column Name> and give it the value using =. Because of the ForEach loop we’re repeating this for every single VM Host in a cluster.

$row.VMhost = $vmhost.Name
$row.State = $vmhost.ConnectionState
$row."Host Memory" = [math]::round($vmhost.MemoryTotalGB,2)
$row."VM Memory" = [math]::round($vmMemSum.sum,2)
$row."Host CPU" = $vmhost.NumCpu
$row."VM CPU" = $vmCpuSum.sum
$row."vCPU per pCPU" = $ratio

Once that has been completed we need to add the rows to our empty array and then close the ForEach loop for the hosts.

$report += $row}

At this point we now have a completed table view of the resource allocation. Since we’ll be running this in PowerShell we’ll need to display the name of the cluster between each report otherwise you might not be able to immediately recognize what cluster is being referenced. Use “Write-Output” instead of “Write-Host” so this displays in the correct order in the script. When using Write-Output with a variable in the output it needs to be wrapped in $( ) otherwise the variable name will be displayed instead.

Write-Output "$($cluster.Name) Resource Allocation"

In order to have this display per cluster we’ll call the cluster output here and then close the ForEach loop on the clusters.

$report | Format-Table -Autosize}

We can then display the output using the $clusterOutput variable we created in step 2.

$clusterOutput

This is what the output will look like:

Instead of just displaying this in the console you could export this to CSV to save it for reference. Below is the full script.

$allClusters = Get-Cluster | Sort Name
$clusterOutput = ForEach ($cluster in $allClusters) {
$report = @()
$allHosts = $cluster | Get-VMHost | Sort Name
ForEach ($vmhost in $allHosts) {
$vms = $vmhost | Get-VM | Where {$_.PowerState -eq "PoweredOn"}
$vmMemSum = $vms.memoryGB | Measure-Object -sum
$vmCpuSum = $vms.NumCpu | Measure-Object -sum
$ratio = [math]::round($vmCpuSum.sum/$vmhost.NumCpu,2)
$row = "" | Select VMHost, State, "Host Memory", "VM Memory", "Host CPU", "VM CPU", "vCPU per pCPU"
$row.VMhost = $vmhost.Name
$row.State = $vmhost.ConnectionState
$row."Host Memory" = [math]::round($vmhost.MemoryTotalGB,2)
$row."VM Memory" = [math]::round($vmMemSum.sum,2)
$row."Host CPU" = $vmhost.NumCpu
$row."VM CPU" = $vmCpuSum.sum
$row."vCPU per pCPU" = $ratio
$report += $row}
Write-Output "$($cluster.Name) Resource Allocation"
$report | Format-Table -Autosize}
$clusterOutput

Table View of Datastores Mounted in a Cluster

I really wish I knew more about PowerShell and even the correct terminology because then maybe this wouldn’t have been so difficult to figure out. I spent 2 days trying to get this to work properly and it wasn’t until I was writing this post with the garbage version of this script that I discovered a more efficient way of doing things. Not knowing what the different functions I normally use are called made it difficult to google and discover alternate methods to accomplish what I was trying to accomplish.

With that out of the way, let’s talk about what it is I’m trying to accomplish. A while ago I was working with a customer that wanted to manage all their standard Portgroups with PowerShell/PowerCLI. I had the thought back then that a table view that listed all the portgroups in a cluster in columns then all the hosts in that cluster in rows with X’s marking what hosts had which portgroups. This seemed like a good idea, but I had no idea how to accomplish it at the time. Here we are a year later and the idea popped up at work again this time to see what datastores were mounted on which hosts. With a lot more PowerShell’ing under my belt I thought I was up to the task.

Normally I would accomplish something like that using a PowerShell array. That would normally be accomplished by doing the following:

$report = @()
$row = "" | Select Hostname, Datastore01, Datastore02
$row.Hostname = $hostname
$row.Datastore01 = $datastore01.name
$row.Datastore02 = $datastore02.name
$report += $row

This would have been the ideal solution except I needed to pass a dynamic number of datastores per cluster and I wouldn’t know ahead of time the names of these datastores. The goal of any script I write is re-use given that I have multiple clusters and multiple vCenters to manage. What I wasn’t able to figure out with this approach was how to pass an array of datastore names on the “$row = “” | Select Hostname, Datastore01…” line. No matter what I did I couldn’t make it work. This lead me down another, very inefficient path. What I didn’t realize at the time was that I could accomplish the same thing with “New-Object PSObject” and “Add-Member”.

I got this to work, but it would only record the values of the first (or last) host depending on how I added values. This brought me to the point of doing a blank array then creating a second array that I would update values on for each host and add that array to my initial array. This felt sloppy and inefficient because I was repeating lines in the script to create the second array and it felt like it could be done better. Then I thought about doing the same thing, but this time using a count to create a new array then after the first run adding the entries to the first array. A few tests and eventually I figured it out.

Now for the script.

1. Here we define the cluster name, gather all the datastores in that cluster, and then get all the hosts in the cluster as well. I added the check for NFS type because that is what I use in my environment and it eliminates any local datastores that may be present on a host from appearing in the cluster check.

$cluster = "ClusterName"
$datastores = Get-Cluster $cluster | Get-Datastore | Where {$_.Type -eq "NFS"} | Sort Name
$allHosts = Get-Cluster $cluster | Get-VMHost

2. After that we are opening a ForEach loop. Inside that loop we use $count++ to test how many times we’ve run this loop. Since we aren’t using the $count variable anywhere else this has no value. $count++ will increase by one each time starting with the number 1 on the first run.

ForEach ($vmhost in $allHosts) {
$count++

3. The next lines are creating our array and populating some of the data. New-Object PSObject is creating a blank object. We reference this blank object and add a new column with “Add-Member” and a name of “HostName” with the name of the first ESXi host in the cluster being set as the value. Then we’re going to open another ForEach loop to add a column name with each of the datastore names.

$report = New-Object PSObject
$report | Add-Member -MemberType NoteProperty -Name "HostName" -Value $vmhost.Name
ForEach ($ds in $datastore) {

4. If the host doesn’t have the datastore present we are leave the value blank, but if the datastore is present we mark it with an “X”. We have to create a new variable ($getDS in this case) and check for the datastore. Adding “-ErrorAction SilentlyContinue” will allow us to run the script and not see any errors if the datastore is missing, but still capture the data. The “IF (!$getDS)” is checking if the $getDS variable is empty. Once the host has been checked for that datastore we perform another “Add-Member” to add the datastore as a column and add the value if the datastore is present or not.

$getDS = $vmhost | Get-Datastore $ds.Name -ErrorAction SilentlyContinue
IF (!$getDS) {$present = " "} ELSE {$present = "X"}
$report | Add-Member -MemberType NoteProperty -Name $ds.Name -Value $present}

5. At this point we have collected data on only 1 of our hosts. If we ended the loop here all we’d do is overwrite the data we just wrote over and over until we finished with all the hosts. The next part is where we use that $count++ from step 2. If $count equals 1 (the first run) then we create a new object called $newReport based on $report which contains the data from 1 host. On the next loop we increase $count by one (now it’s value is “2”), replace all the data that existed in $report previously with a new host, and take that new object, $newReport, and add $report to it.

IF ($count -eq "1"){$newReport = New-Object PSObject $report} ELSE {[array]$newReport += $report}}

6. Now that all the data is combined we can view it by running $newReport | Format-Table. This gives us the view below and we can see that we have a few datastores not present on some of our hosts.

$newReport | Sort Hostname | Format-Table

a. This data can also be exported to a CSV file when there are more datastores than can be displayed in your powershell console

$newReport | Export-CSV "C:\datastoreReport.csv" -NoTypeInformation

Below is the full code for this script. You can even wrap this in another ForEach loop for every cluster to see them all at once, but if you do that you’ll have to clear out the $report table by doing “$report = ”” and set the count of your count variable to 0 by doing “$count = 0” once your open the ForEach cluster loop.

$cluster = "ClusterName"
$datastores = Get-Cluster $cluster | Get-Datastore | Where {$_.Type -eq "NFS"} | Sort Name
$allHosts = Get-Cluster $cluster | Get-VMHost
ForEach ($vmhost in $allHosts) {
$count++
$report = New-Object PSObject
$report | Add-Member -MemberType NoteProperty -Name "HostName" -Value $vmhost.Name
ForEach ($ds in $datastores) {
$getDS = $vmhost | Get-Datastore $ds.Name -ErrorAction SilentlyContinue
IF (!$getDS) {$present = " "} ELSE {$present = "X"}
$report | Add-Member -MemberType NoteProperty -Name $ds.Name -Value $present}
IF ($count -eq "1"){$newReport = New-Object PSObject $report} ELSE {[array]$newReport += $report}}

$newReport | Sort HostName | Format-Table