A single PowerCLI script that walks every Organisation and Org VDC in a VMware Cloud Director
instance and exports CPU, RAM and per-storage-profile capacity — to CSV, an
interactive Out-GridView window, or both. This post also covers two
problems you'll likely hit the moment you try this yourself: the StorageProfiles
property silently coming back empty, and numbers rendering with a comma instead of a
period depending on regional settings.
Why a per-storage-profile report
Most quick PowerCLI snippets for VMware Cloud Director capacity reporting pull a single blended storage figure per Org VDC. That's fine until a VDC has more than one storage tier — say a flash-backed "Gold" profile and a capacity-backed "Bronze" profile — at which point one number hides exactly the detail you need for capacity planning or a migration sizing exercise. This script instead emits one row per storage profile, so a VDC with two tiers becomes two rows, each with its own allocated/used/limit figures and percentage used.
It also enumerates every Organisation it can see, not just one — useful if you're managing a multi-tenant estate or auditing capacity across several customer Orgs in one pass.
Prerequisites
- VMware PowerCLI installed, with the
VMware.VimAutomation.Cloudmodule available (Install-Module VMware.PowerCLI). - Network access to your vCD cell's API endpoint on port 443.
- An account with at least read access to the Orgs/VDCs you want reported on. Org Admin is generally enough; System/Provider Admin sees more (and unlocks the admin-level storage profile query the script tries first — see below).
- Optional, for
-GridViewon PowerShell 7+:Install-Module Microsoft.PowerShell.GraphicalToolsand a desktop (non-headless) session.
How the script works, step by step
The script runs in four phases. Each one maps directly to a section of the code, so if you're reading the source alongside this, you'll recognise the boundaries.
The script checks that VMware.VimAutomation.Cloud is installed, imports it, then calls Connect-CIServer against the server you pass in via -VCDServer. If you don't supply -Credential, it prompts interactively — nothing is ever hardcoded.
Before touching any data, it also forces the PowerShell session's number formatting to use a period as the decimal separator for the rest of the run (more on why in step 5), and restores your original setting automatically when the script finishes.
Rather than asking each VDC for its storage profiles one at a time, the script makes a single bulk call to Search-Cloud covering the whole vCD instance, and builds an in-memory lookup table keyed by VDC GUID. This is both faster across a large estate and the foundation of the fallback covered in step 4.
For every Organisation (Get-Org), the script pulls its Org VDCs (Get-OrgVdc), and for each VDC:
- Reads CPU and RAM allocated / limit / used figures.
- Resolves the Network Pool and Provider VDC name from the underlying API object.
- Builds one output row per storage profile on that VDC, trying three data sources in order until one returns something (the fallback in step 4).
Orgs with no VDCs, and VDCs with no resolvable storage profile, still produce a row — clearly labelled — rather than silently disappearing from the report.
All rows are sorted by Org, VDC and storage profile name, then:
- Written to CSV (semicolon-delimited, UTF-8) unless
-NoCsvis passed. - Opened in an interactive
Out-GridViewwindow if-GridViewis passed.
A breakdown of which data source supplied each row is printed to the console before the session disconnects — useful for sanity-checking the report before you send it anywhere.
The StorageProfiles gotcha
The "friendly" Get-OrgVdc object exposes a StorageProfiles
property that, on paper, gives you exactly the per-profile breakdown this report needs. In practice,
against some vCD versions and account types, that property comes back empty for every single
VDC — not because the data doesn't exist, but because the friendly wrapper's link resolution
doesn't always populate it. The deprecated VDC-level aggregate properties
(StorageUsedGB, etc.) still work in many environments, but they collapse
multiple storage tiers into one blended figure and Broadcom has flagged them for removal.
To get a reliable answer regardless of which scenario you're in, the script tries three sources in order and only moves to the next one if the current one comes back empty:
Fast path. Used directly whenever $vdc.StorageProfiles is actually populated.
Queries vCD's search/query API directly with -QueryType AdminOrgVdcStorageProfile, falling back to the lower-privilege OrgVdcStorageProfile query type if the admin one isn't permitted. Matches results back to each VDC by the GUID embedded in the record's href — not by name, since VDC names aren't guaranteed unique across different Orgs.
Falls back to the old blended StorageLimitGB / StorageUsedGB properties so the row still has a number rather than a blank cell.
StorageDataSource column showing exactly which tier produced it. If you ever see "VDC Aggregate (legacy)" rows, that's your signal the account or vCD version couldn't supply per-profile detail for that VDC — worth a closer look rather than assuming it's wrong.
The comma-vs-period decimal bug
[math]::Round() always returns the mathematically correct number. How
that number gets displayed — in Out-GridView, on the console, or
written into the CSV — depends on the current culture/regional settings of the machine running the
script, not on the script's logic. On a system whose region uses a comma as the decimal separator
(common across continental Europe), 12.34 renders as 12,34.
The value was never wrong; only its text rendering was locale-dependent.
Fixing this needed two settings, not one. Setting only the calling thread's culture
fixes the CSV and console output, but Out-GridView opens its window on a
separate UI thread that doesn't inherit that override — it falls back to the system's regional
settings unless the process-wide default is also set. The script sets both
([Threading.Thread]::CurrentThread.CurrentCulture and
[CultureInfo]::DefaultThreadCurrentCulture) to
InvariantCulture for the duration of the run, and restores your original
settings afterwards in a finally block — so it never leaks into the rest
of your session.
$origCulture = [Threading.Thread]::CurrentThread.CurrentCulture
$origUICulture = [Threading.Thread]::CurrentThread.CurrentUICulture
$origDefaultCulture = [CultureInfo]::DefaultThreadCurrentCulture
$origDefaultUICulture = [CultureInfo]::DefaultThreadCurrentUICulture
try {
[Threading.Thread]::CurrentThread.CurrentCulture = [CultureInfo]::InvariantCulture
[Threading.Thread]::CurrentThread.CurrentUICulture = [CultureInfo]::InvariantCulture
[CultureInfo]::DefaultThreadCurrentCulture = [CultureInfo]::InvariantCulture
[CultureInfo]::DefaultThreadCurrentUICulture = [CultureInfo]::InvariantCulture
# ... rest of the script ...
}
finally {
[Threading.Thread]::CurrentThread.CurrentCulture = $origCulture
[Threading.Thread]::CurrentThread.CurrentUICulture = $origUICulture
[CultureInfo]::DefaultThreadCurrentCulture = $origDefaultCulture
[CultureInfo]::DefaultThreadCurrentUICulture = $origDefaultUICulture
}
Usage & parameters
Server names and credentials below are placeholders — swap in your own vCD cell's address.
| Parameter | Required | Description |
|---|---|---|
-VCDServer | Yes | FQDN or IP of the vCD cell, e.g. vcloud.contoso.com. |
-Credential | No | A PSCredential. Omit it to be prompted interactively. |
-OutputPath | No | CSV destination. Defaults to a timestamped file in the current folder. |
-GridView | No | Switch. Also opens the results in an interactive, sortable/filterable grid window. |
-NoCsv | No | Switch. Skips writing the CSV — handy with -GridView for a quick on-screen check. |
.\Export-vCDOrgCapacityReport.ps1 -VCDServer vcloud.contoso.com
$cred = Get-Credential admin@automatewithravi.com .\Export-vCDOrgCapacityReport.ps1 -VCDServer vcloud.contoso.com -Credential $cred -GridView -NoCsv
Sample output
An illustrative excerpt — Org, VDC and customer names below are all generic placeholders, not real environments:
| OrgName | VDCName | AllocationModel | RAM Allocated (GB) | StorageProfileName | Storage Limit (GB) | Storage Used (GB) | StorageDataSource |
|---|---|---|---|---|---|---|---|
| Contoso-Prod | Contoso-Prod-VDC01 | AllocationPool | 512.00 | Gold-NVMe | 2000.00 | 843.50 | Search-Cloud (AdminOrgVdcStorageProfile) |
| Contoso-Prod | Contoso-Prod-VDC01 | AllocationPool | 512.00 | Bronze-SAS | 5000.00 | 1240.25 | Search-Cloud (AdminOrgVdcStorageProfile) |
| Fabrikam-DR | Fabrikam-DR-VDC01 | PayAsYouGo | 256.00 | (aggregate) | 1500.00 | 410.00 | VDC Aggregate (legacy) |
Troubleshooting
- Every StorageDataSource shows "VDC Aggregate (legacy)". Both
Search-Cloudquery types came back empty for that account — usually a permissions limitation rather than a script issue. Confirm the account can see storage profiles in the vCD UI for the affected Org. Connect-CIServerfails with "No Cloud server was found on .../api/". This happens before authentication — it means the initial/api/versionsnegotiation failed. TestInvoke-WebRequest https://<your-vcd-server>/api/versionsdirectly: clean XML back means a PowerCLI/version mismatch (try updating PowerCLI); an HTML/login page back means the FQDN you have is a tenant portal address, not the actual API endpoint.- Numbers still show commas in the grid after the culture fix. Check
$PSVersionTable.PSVersion— on PowerShell 7+,Out-GridViewcomes from theMicrosoft.PowerShell.GraphicalToolsmodule, which is a different implementation to classic Windows PowerShell's and may need the column values formatted as invariant-culture strings explicitly rather than relying on thread culture alone. PowerCLI scripts should not use the 'Client'/'Uid' propertywarnings. These come from inside the module itself duringConnect-CIServer/Get-Org, not from this script — safe to ignore.
Get the full script
Export-vCDOrgCapacityReport.ps1 on GitHub