Get-VM inventory reporting in PowerCLI: complete, accurate and fast

Get-VM is the first PowerCLI cmdlet everyone learns and the one most inventory reports are built on. Piping it into Select-Object and Export-Csv produces a spreadsheet in about ten seconds, which is why so many estates have one.

The trouble starts when someone acts on that spreadsheet. The VM count does not match the vSphere Client. The operating system column is blank for a third of the rows. Provisioned and used space are confused with each other, so the storage forecast is wrong. And on an estate of two thousand VMs the script takes twenty minutes, so nobody runs it.

Every one of those is a known property of how Get-VM works, not a bug. This page walks through building a report that is complete, states capacity correctly, and still finishes quickly at scale. For the wider cmdlet set, the VMware PowerCLI cheat sheet is the lookup reference.

Applies to: VMware PowerCLI 12 and later, against vCenter Server 7.0 and 8.0


Quick answer

Everything on this page needs an open connection to vCenter. Connect-VIServer opens one and PowerCLI holds it for the rest of the session, so it is done once per shell.

# Prompting for the credentials keeps the password out of the session history
Connect-VIServer -Server SRV-VC-01 -Credential (Get-Credential)

# Confirm what you are connected to before running a report against the wrong estate
$global:DefaultVIServer

With that in place, this is the report to start from. It answers the questions an infrastructure review actually asks, and it is correct on the two things most reports get wrong: it separates provisioned from used space, and it reads the guest OS from the configuration rather than from VMware Tools.

Get-VM | Select-Object Name, PowerState, NumCpu, MemoryGB,
    @{N='VMHost';       E={ $_.VMHost.Name }},
    @{N='Cluster';      E={ $_.VMHost.Parent.Name }},
    @{N='GuestOS';      E={ $_.ExtensionData.Config.GuestFullName }},
    @{N='ProvisionedGB';E={ [math]::Round($_.ProvisionedSpaceGB, 1) }},
    @{N='UsedGB';       E={ [math]::Round($_.UsedSpaceGB, 1) }} |
    Sort-Object Name |
    Export-Csv -Path C:\reports\vm-inventory.csv -NoTypeInformation -Encoding UTF8

Everything below explains why each of those lines is written the way it is, and what to do when the estate gets large enough that this version is too slow.


What Get-VM actually returns

Get-VM returns VirtualMachine objects. Most of what a report needs is a direct property; the rest is one level down in a related object, or in ExtensionData, which is the raw vSphere API object PowerCLI wrapped.

PropertyWhat it gives youWatch out for
Name, PowerState, NumCpu, MemoryGBThe four columns every report hasMemoryGB is a decimal, not an integer
VMHostThe host object; .Name for the FQDN, .Parent.Name for the clusterNull for a VM that is not currently placed
ProvisionedSpaceGBEverything the VM could consume, thin growth includedNot what the datastore is using today
UsedSpaceGBWhat the VM occupies now, across all datastoresIncludes snapshots and swap, so it moves
HardwareVersionVirtual hardware version as a string, such as vmx-19String, so sorting it alphabetically puts vmx-9 after vmx-19
GuestVMGuest object: OSFullName, IPAddress, HostName, ToolsVersion, StatePopulated by VMware Tools, so empty when Tools is not running
Folder, ResourcePool, Notes, CustomFieldsWhere it lives and what people wrote about itNotes and custom fields are often the only owner record that exists
ExtensionDataThe full API object: Config, Guest, Runtime, SummaryProperty names follow API casing, not PowerShell casing

Practical examples

1. Get the placement columns right

The problem: the report has a VMHost column that reads VMHost-esx01.lab.local or, worse, the object type name, and no cluster column at all. Capacity planning needs the cluster.

The solution: VMHost is an object. Take .Name from it, and take the cluster from the host’s parent rather than querying clusters separately.

# .Parent on a VMHost is the cluster it sits in - no second lookup needed
Get-VM | Select-Object Name, PowerState,
    @{N='VMHost'; E={ $_.VMHost.Name }},
    @{N='Cluster';E={ $_.VMHost.Parent.Name }},
    @{N='Folder'; E={ $_.Folder.Name }}
Name        PowerState VMHost              Cluster    Folder
----        ---------- ------              -------    ------
SRV-APP-01  PoweredOn  esx01.lab.local     PROD-CL01  Applications
SRV-DB-01   PoweredOn  esx02.lab.local     PROD-CL01  Databases
SRV-WEB-01  PoweredOff esx01.lab.local     PROD-CL01  Web
Warning: a standalone host that is not in a cluster has a datacenter or a folder as its parent, so the Cluster column will quietly contain something that is not a cluster. If that matters, test the parent type rather than trusting the name.

2. State capacity correctly

The problem: a storage forecast built from a single “disk size” column is wrong in both directions. Thin disks are over-counted if you use provisioned space, and under-counted if you use used space and then buy for today.

The solution: report both, and report the gap between them, because the gap is the exposure if every thin disk inflates.

The two numbers come from the API storage summary. UsedSpaceGB maps to committed, which Broadcom defines as the storage space committed to the VM across all datastores. ProvisionedSpaceGB adds uncommitted, the additional space the VM could potentially use. Provisioned minus used is therefore the unrealised thin growth.

# Round at the point of display - the raw properties are decimals with long tails
Get-VM | Select-Object Name,
    @{N='ProvisionedGB'; E={ [math]::Round($_.ProvisionedSpaceGB, 1) }},
    @{N='UsedGB';        E={ [math]::Round($_.UsedSpaceGB, 1) }},
    @{N='ThinGrowthGB';  E={ [math]::Round($_.ProvisionedSpaceGB - $_.UsedSpaceGB, 1) }} |
    Sort-Object ThinGrowthGB -Descending
Name       ProvisionedGB UsedGB ThinGrowthGB
----       ------------- ------ ------------
SRV-APP-01         200.0   48.0        152.0
SRV-DB-01          520.0  505.5         14.5
SRV-WEB-01          80.0   71.2          8.8
Note: used space includes snapshot deltas and the swap file, so a VM with an old snapshot looks larger than its disks. If a row surprises you, check snapshots before you check the disks: see managing vSphere snapshots with PowerCLI.

3. Get an operating system for every row

The problem: the GuestOS column is blank for every powered-off VM and for every VM whose Tools is stopped. Those are exactly the VMs an audit cares about.

The solution: there are two different operating system fields, and reports should carry both. Guest.OSFullName is what Tools reports from inside the guest, so it is empty without Tools. ExtensionData.Config.GuestFullName is what the VM is configured as, and it is always present.

# ConfiguredOS is always populated. RunningOS is the truth when Tools is up,
# and the two disagreeing is itself a finding worth chasing.
Get-VM | Select-Object Name, PowerState,
    @{N='ConfiguredOS'; E={ $_.ExtensionData.Config.GuestFullName }},
    @{N='RunningOS';    E={ $_.Guest.OSFullName }},
    @{N='ToolsVersion'; E={ $_.Guest.ToolsVersion }},
    @{N='ToolsStatus';  E={ $_.ExtensionData.Guest.ToolsVersionStatus2 }}
Name       PowerState ConfiguredOS                  RunningOS                     ToolsVersion ToolsStatus
----       ---------- ------------                  ---------                     ------------ -----------
SRV-APP-01 PoweredOn  Microsoft Windows Server 2022 Microsoft Windows Server 2022 12325        guestToolsCurrent
SRV-DB-01  PoweredOn  Microsoft Windows Server 2019 Microsoft Windows Server 2019 11296        guestToolsNeedUpgrade
SRV-WEB-01 PoweredOff Red Hat Enterprise Linux 9                                               guestToolsNotInstalled
Common mistake: using ExtensionData.Guest.ToolsStatus. It still returns a value, so nothing appears broken, but the vSphere API has marked it deprecated since version 4.0 in favour of ToolsVersionStatus2 and ToolsRunningStatus. Most sample scripts online still use the old field.

4. One row per disk, without losing the VM name

The problem: a storage team asks for per-disk detail: which disks are thin, which are thick, and which datastore each sits on. A VM has many disks, so the one-row-per-VM shape does not fit.

The solution: -PipelineVariable keeps a handle on the VM while the pipeline moves on to its disks, so each disk row can still name its parent.

# -PipelineVariable vm pins the current VM so the inner Select can still reach it.
# Each disk becomes its own row and the VM name repeats down the column.
Get-VM -PipelineVariable vm |
    Get-HardDisk |
    Select-Object @{N='VM';E={ $vm.Name }},
                  Name,
                  @{N='CapacityGB';E={ [math]::Round($_.CapacityGB, 1) }},
                  StorageFormat,
                  @{N='Datastore';E={ $_.Filename.Split(']')[0].TrimStart('[') }} |
    Export-Csv -Path C:\reports\vm-disks.csv -NoTypeInformation -Encoding UTF8
VM         Name        CapacityGB StorageFormat Datastore
--         ----        ---------- ------------- ---------
SRV-APP-01 Hard disk 1       60.0 Thin          DS-PROD-01
SRV-APP-01 Hard disk 2      500.0 Thick         DS-PROD-02
SRV-DB-01  Hard disk 1       80.0 Thin          DS-PROD-01
Warning: a VM with no hard disks contributes no row at all, so this report is not a VM list. Reconcile the row count against a plain Get-VM count before treating it as complete.

5. Make it fast on a large estate

The problem: the report is correct but takes twenty minutes across two thousand VMs, because every calculated property that reaches into a related object triggers another round trip to vCenter.

The solution: Get-View asks the API for exactly the properties you name, in one call. It is the single biggest speed win available in PowerCLI reporting.

The cost is that you work with raw API objects: property paths use API casing, and the host comes back as a reference rather than a name. Resolve the host names once into a lookup table and the cost disappears.

# One call for the host names, keyed by managed object reference value
$hostName = @{}
Get-View -ViewType HostSystem -Property Name | ForEach-Object { $hostName[$_.MoRef.Value] = $_.Name }

# One call for the VMs. -Property is what makes this fast: nothing else is fetched.
# -Filter excludes templates, which Get-View returns and Get-VM does not.
Get-View -ViewType VirtualMachine -Filter @{'Config.Template'='false'} -Property `
    Name, Runtime.PowerState, Runtime.Host, Config.Hardware.NumCPU,
    Config.Hardware.MemoryMB, Config.GuestFullName, Config.Version,
    Guest.ToolsVersionStatus2, Summary.Storage.Committed, Summary.Storage.Uncommitted |
  Select-Object Name,
    @{N='PowerState';   E={ $_.Runtime.PowerState }},
    @{N='VMHost';       E={ $hostName[$_.Runtime.Host.Value] }},
    @{N='NumCpu';       E={ $_.Config.Hardware.NumCPU }},
    @{N='MemoryGB';     E={ $_.Config.Hardware.MemoryMB / 1024 }},
    @{N='ConfiguredOS'; E={ $_.Config.GuestFullName }},
    @{N='HwVersion';    E={ $_.Config.Version }},
    @{N='UsedGB';       E={ [math]::Round($_.Summary.Storage.Committed / 1GB, 1) }},
    @{N='ProvisionedGB';E={ [math]::Round(($_.Summary.Storage.Committed + $_.Summary.Storage.Uncommitted) / 1GB, 1) }} |
  Export-Csv -Path C:\reports\vm-inventory-fast.csv -NoTypeInformation -Encoding UTF8
Result: the same columns as the Quick answer report, built from two API calls instead of several per VM. Measure the difference on your own estate with Measure-Command before and after; the gap widens with every VM.
Note: the backtick at the end of the -Property line is the PowerShell line continuation character. It must be the last character on the line, with no trailing space, or the command breaks.

Hidden gems

Get-VM and Get-View disagree on the VM count, and both are right. Get-VM does not return templates; Get-View -ViewType VirtualMachine does. If a report built with Get-View shows more machines than the vSphere Client’s VM count, templates are the reason. Filter them out with -Filter @{'Config.Template'='false'}, or ask for them deliberately with Get-Template.

Notes and custom attributes are already an ownership record. $_.Notes and $_.CustomFields come back with every Get-VM call at no extra cost. On most estates somebody has been typing an owner or a ticket number into the notes field for years, and nobody has ever exported it.

Hardware version sorts wrong. HardwareVersion is a string like vmx-19, so an alphabetical sort puts vmx-9 after vmx-19. To find the genuinely old VMs, sort on the number.

# Split off the numeric part and cast it, otherwise vmx-9 sorts as the newest
Get-VM | Sort-Object { [int]($_.HardwareVersion -replace 'vmx-','') } |
    Select-Object Name, HardwareVersion -First 10

CreateDate answers questions nothing else does. It is a direct property, and it is how you find the machines somebody spun up for a project that ended two years ago. Combine it with power state and a report of stale candidates writes itself.


When there is no vCenter

PowerCLI can connect straight to an ESXi host. It is the same cmdlet, pointed at the host and authenticating against a host-local account rather than a vCenter one.

# root works, but a read-only local account is the right choice for a reporting script
Connect-VIServer -Server esx01.lab.local -Credential (Get-Credential)

# ESXi ships a self-signed certificate, so the first attempt usually fails with an SSL
# error. Scope Session applies the exception to this shell only, not to the machine.
Set-PowerCLIConfiguration -Scope Session -InvalidCertificateAction Ignore

# Close it when you are done - a stale connection changes what the next cmdlet targets
Disconnect-VIServer -Server esx01.lab.local -Confirm:$false

Get-VM works against that connection, and so do NumCpu, MemoryGB, provisioned and used space. What stops working is everything vCenter owns: Get-Cluster returns nothing, so the Cluster column is empty; tags are not available; and Get-Template comes back silent, because templates are a vCenter construct. Get-View still works, but only over the objects that one host knows about.

Note: the certificate error and the credential store are covered in full in install PowerCLI for vSphere. If you connect to the same hosts regularly, Connect-VIServer -SaveCredentials puts them in the credential store so later sessions do not prompt.

On a standalone host the equivalent inventory can also come from the host itself.

# In the ESXi Shell: registered VMs with their World IDs and config paths
esxcli --formatter=csv vm process list

That returns only VMs that are powered on, since it lists running worlds. For the full registered set on a standalone host, vim-cmd vmsvc/getallvms is the counterpart. The esxcli syntax and formatters guide covers why the formatter matters here too.


Where this matters

  • Storage forecasting. The provisioned-minus-used gap is the number that decides whether a thin-provisioned datastore is a saving or a future outage.
  • Licensing audits. Configured guest OS plus CPU and socket counts, exported once, answers most of what a licence review asks for.
  • Upgrade planning. Hardware version and Tools status together tell you how much work a vSphere upgrade actually implies.
  • Decommission campaigns. Powered off, created long ago, no notes: that combination is the shortlist, and all three come from one Get-VM call.
  • Handover and documentation. When you inherit an estate, this report plus the notes field is usually more accurate than the documentation you were given.

Tips and limitations

  • Windows PowerShell 5.1 writes a #TYPE header line into every CSV unless you pass -NoTypeInformation. PowerShell 7 dropped that behaviour, so a script that works on one version can produce a differently shaped file on the other.
  • If Excel opens the CSV as one column, the delimiter does not match the machine’s locale. Export with -UseCulture so the separator matches what Excel expects.
  • -Encoding UTF8 writes a byte order mark on Windows PowerShell 5.1 and none on PowerShell 7. Excel needs the mark to read non-ASCII VM names correctly, so on PowerShell 7 use -Encoding utf8BOM.
  • A read-only vCenter role is enough for every command on this page, and is what a scheduled reporting account should use.
  • Get-VM -Location accepts a resource pool, vApp, host, folder, cluster or datacenter. Use it to scope a report instead of pulling everything and filtering afterwards.
  • Get-View returns raw API objects with no PowerCLI conveniences. Use it for volume, and plain Get-VM when you are working interactively on a handful of machines.

Official documentation


Related tools


Related guides