PowerCLI script to generate a VM resource utilization report

Large Australian IT teams often run VMware estates across multiple data centres and business units. Capacity decisions become easier when CPU, memory, disk and network utilisation are collected consistently instead of being checked manually during busy periods.

The script below connects to vCenter Server through PowerCLI, queries the performance manager and exports a CSV. It suits platform engineers, systems administrators and consultants who need a repeatable baseline for capacity planning, consolidation or right-sizing.

Defining the report scope

A useful report defines the workload and measurement period before it collects anything. CPU and memory reveal contention, while disk usage and network throughput help explain whether a VM is idle, steady or unusually busy. A 24-hour view works for an initial check; seven to 30 days provides a better baseline for trends.

Australian teams often span Sydney, Melbourne and Brisbane. Schedule overnight collection to finish before the 9:00 a.m. workday, and record the source timezone. Sydney changes its UTC offset during daylight saving while Brisbane does not, so fixed UTC and fixed local schedules are not interchangeable.

The wider collection on the Virtxpert technology blog also covers VMware automation, visualisation icons and reusable PowerCLI tools.

Preparing the PowerCLI session

Install the current VMware.PowerCLI module from your organisation’s approved repository. For a personal lab, the usual starting point is Install-Module -Name VMware.PowerCLI -Scope CurrentUser -AllowClobber, followed by Import-Module VMware.VimAutomation.Core.

Connect interactively with Connect-VIServer -Server 'vcenter.example.com' -Credential (Get-Credential). Keep credentials out of the script and shell history. A read-only vCenter role with access to the relevant inventory and performance data reduces the risk from a mistyped filter or accidental change.

Reusable patterns for parameters, functions and error handling appear in the custom PowerCLI scripts guide. Test against a development vCenter before using the same account against production.

Collecting vCenter performance data

PowerCLI exposes vCenter performance counters through the PerformanceManager managed object. The following script queries five metrics for each powered-on VM, averages the samples and writes one row per guest.

param(
    [datetime]$StartTime = [datetime]::UtcNow.AddHours(-24),
    [datetime]$EndTime = [datetime]::UtcNow,
    [int]$IntervalSeconds = 300,
    [string]$OutputPath = ".\vm-resource-utilization.csv"
)

$server = Get-VIServer |
    Where-Object { $_.IsConnected } |
    Select-Object -First 1

if (-not $server) {
    throw "Connect to vCenter Server before running this script."
}

function Get-Mean {
    param([double[]]$Values)

    if ($Values.Count -eq 0) {
        return $null
    }

    return [math]::Round(
        ($Values | Measure-Object -Average).Average,
        2
    )
}

function Get-Peak {
    param([double[]]$Values)

    if ($Values.Count -eq 0) {
        return $null
    }

    return [math]::Round(
        ($Values | Measure-Object -Maximum).Maximum,
        2
    )
}

$metricIds = @(
    "cpu.usec",
    "mem.usage.average",
    "virtualDisk.usage.average",
    "net.received.average",
    "net.transmitted.average"
)

$perfManager = Get-View -Server $server -Id "PerformanceManager"

$rows = @(
    foreach (
        $vm in (
            Get-VM -Server $server |
            Where-Object { $_.PowerState -eq "PoweredOn" }
        )
    ) {
        $query = New-Object VMware.VimAutomation.Core.Types.V1.PerfQuerySpec
        $query.entity = $vm.ExtensionData.MoRef
        $query.metricId = $metricIds
        $query.format = "normal"
        $query.startTime = $StartTime
        $query.endTime = $EndTime
        $query.interval = $IntervalSeconds

        $series = @($perfManager.QueryPerf($query))

        $cpu = [double[]]@(
            $series |
            Where-Object { $_.metricId -eq "cpu.usec" } |
            ForEach-Object { if ($null -ne $_.value) { [double]$_.value } }
        )

        $memory = [double[]]@(
            $series |
            Where-Object { $_.metricId -eq "mem.usage.average" } |
            ForEach-Object { if ($null -ne $_.value) { [double]$_.value } }
        )

        $disk = [double[]]@(
            $series |
            Where-Object { $_.metricId -eq "virtualDisk.usage.average" } |
            ForEach-Object { if ($null -ne $_.value) { [double]$_.value } }
        )

        $receive = [double[]]@(
            $series |
            Where-Object { $_.metricId -eq "net.received.average" } |
            ForEach-Object { if ($null -ne $_.value) { [double]$_.value } }
        )

        $transmit = [double[]]@(
            $series |
            Where-Object { $_.metricId -eq "net.transmitted.average" } |
            ForEach-Object { if ($null -ne $_.value) { [double]$_.value } }
        )

        $cpuCount = [math]::Max(
            1,
            [int]$vm.ExtensionData.Summary.Config.NumCpu
        )

        $memoryLimitMB = [double]$vm.ExtensionData.Summary.Config.MemorySizeMB
        $cpuMean = Get-Mean $cpu
        $cpuPeak = Get-Peak $cpu
        $memoryMeanKB = Get-Mean $memory
        $memoryPeakKB = Get-Peak $memory
        $diskMeanKB = Get-Mean $disk
        $receiveMean = Get-Mean $receive
        $transmitMean = Get-Mean $transmit

        [pscustomobject]@{
            VM                  = $vm.Name
            WindowStart         = $StartTime.ToString("o")
            WindowEnd           = $EndTime.ToString("o")
            Samples             = $cpu.Count
            vCPU                = $cpuCount
            CPUAveragePercent   = if ($null -ne $cpuMean) {
                [math]::Round(
                    ($cpuMean / 1000000 / $IntervalSeconds / $cpuCount) * 100,
                    2
                )
            } else { $null }
            CPUMaximumPercent   = if ($null -ne $cpuPeak) {
                [math]::Round(
                    ($cpuPeak / 1000000 / $IntervalSeconds / $cpuCount) * 100,
                    2
                )
            } else { $null }
            MemoryAverageMB     = if ($null -ne $memoryMeanKB) {
                [math]::Round($memoryMeanKB / 1024, 2)
            } else { $null }
            MemoryMaximumMB     = if ($null -ne $memoryPeakKB) {
                [math]::Round($memoryPeakKB / 1024, 2)
            } else { $null }
            MemoryLimitMB       = $memoryLimitMB
            MemoryLimitPercent  = if (
                $null -ne $memoryMeanKB -and $memoryLimitMB -gt 0
            ) {
                [math]::Round(
                    ($memoryMeanKB / 1024) / $memoryLimitMB * 100,
                    2
                )
            } else { $null }
            DiskAverageMB       = if ($null -ne $diskMeanKB) {
                [math]::Round($diskMeanKB / 1024, 2)
            } else { $null }
            NetworkReceiveKBps  = if ($null -ne $receiveMean) {
                [math]::Round($receiveMean / 1024, 2)
            } else { $null }
            NetworkTransmitKBps = if ($null -ne $transmitMean) {
                [math]::Round($transmitMean / 1024, 2)
            } else { $null }
        }
    }
)

if ($rows.Count -eq 0) {
    throw "No powered-on virtual machines were found."
}

$rows |
    Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8

$rows | Format-Table -AutoSize
Write-Host "Report saved to $OutputPath"

Each row includes allocated vCPUs, average and peak CPU, average and peak memory, memory use against the configured limit, disk usage and average network receive and transmit rates. The CSV retains the measurements needed for charting or import into a reporting platform.

Counter names can differ across older vSphere releases. The standard counters used here treat memory and disk values as kilobytes and network values as bytes per second; confirm those units against your environment. A VM without VMware Tools or recent samples can return blanks, so treat an empty result as missing data rather than zero consumption.

Interpreting the results

CPU percentages are calculated from cpu.usec over the five-minute sample interval and divided by allocated vCPUs. A value of 100% means all allocated CPU capacity was used. Memory percentages use configured memory as the denominator; they do not reveal whether a reservation or entitlement is the real constraint.

Treat disk usage as capacity consumed rather than disk performance, and network rates as activity rather than saturation. Compare several reporting periods before changing VM shapes because one interval can reflect patching, backup windows or month-end processing.

If the CSV contains operator names, guest IP addresses or other identifiers, handle it under the Privacy Act 1988 and applicable Australian Privacy Principles. Limit access, avoid unnecessary personal data and define a retention period. VM names alone may still disclose business-sensitive context.

Scheduling reports for Australian operations

Store report timestamps in UTC and add a local-time column for readers. New South Wales, Victoria, South Australia and Tasmania observe daylight saving, whereas Queensland and Western Australia do not. That difference matters when a scheduled job crosses a clock change.

The Australian market includes large VMware estates operated by internal platform groups and managed service providers. A recurring utilisation report can support host consolidation, licensing reviews and renewal planning, but owners should interpret it alongside SLAs and workload criticality.

In production, run the script on a schedule, retain dated CSVs and alert on sustained CPU or memory pressure rather than isolated spikes. Review metric availability after upgrades and before adding clusters, because counter changes and missing VMware Tools data can distort long-term comparisons.


Save the script in your internal Git repository, run it against a test vCenter, validate the first CSV with your platform team and then add the report to the regular capacity review.