Find orphaned VMs in vCenter with PowerCLI

An orphaned VM is usually a virtual machine configuration file left on a datastore after its vCenter inventory entry has been removed. A .vmx file alone is not proof: it may belong to a powered-off machine, a template, a backup, or a VM registered under another account. The safest workflow compares datastore contents with inventory and reports candidates without changing anything.

The example below uses PowerCLI for VM inventory and the vCenter REST datastore-file endpoint for discovery. It targets PowerShell 7, PowerCLI 13, and vCenter 7.0 Update 2 or later. Test it on a non-production datastore first using an account with read access to both inventory and datastore files.

Understanding orphaned VM candidates

The term orphan is used inconsistently. Here, an unregistered VM candidate has a .vmx file but no matching registered configuration path. An orphaned disk may have only .vmdk, .nvram, snapshot, or log files, so this method cannot reconstruct a VM whose .vmx has already been removed.

Evidence What it indicates Sensible next step
Matching .vmx path The file belongs to a registered VM. Exclude it from cleanup.
VMX path absent from inventory Possible unregistered VM. Verify host, backup, and disk ownership.
VMDK without a matching VMX Possibly orphaned storage rather than an orphaned VM. Review it as a separate recovery case.

Cleanup opportunities also arise from Australian operating patterns. A Sydney team may inherit a datastore from a Melbourne migration, while a Brisbane office can retain a copy after a Perth consolidation. Easter and Christmas shutdowns can provide maintenance windows, but they still require documented change approval.

Comparing registered VMs with datastore files

Registered VMs expose their configuration-file path through the vSphere API. The script normalises those paths, retrieves datastore files, and compares full paths rather than display names, because a folder called payroll-prod may contain a VM named finance-01.

param(
    [Parameter(Mandatory = $true)][string]$Vcenter,
    [string[]]$Datastore,
    [string]$Output = 'vm-orphan-candidates.csv'
)

$ErrorActionPreference = 'Stop'
$cred = Get-Credential -Message 'Enter your vCenter account'
Connect-VIServer -Server $Vcenter -Credential $cred | Out-Null

$api = "https://$Vcenter/api"
$token = $null

try {
    $token = Invoke-RestMethod -Method Post -Uri "$api/session" `
        -ContentType 'application/json' `
        -Body (@{
            username = $cred.UserName
            password = $cred.GetNetworkCredential().Password
        } | ConvertTo-Json)

    $headers = @{ Authorization = "Bearer $token" }

    $registered = Get-VM -Server $Vcenter | ForEach-Object {
        $file = $_.ExtensionData.Config.Files |
            Where-Object { $_.Key -eq 'virtualMachineConfigFile' } |
            Select-Object -First 1

        if ($file -and $file.Value -match '^\[([^\]]+)\]\s+(.+)$') {
            [pscustomobject]@{
                Datastore = $Matches[1]
                Path      = $Matches[2].Replace('\', '/').TrimStart('/')
            }
        }
    }

    $stores = if ($Datastore) {
        Get-Datastore -Server $Vcenter -Name $Datastore
    } else {
        Get-Datastore -Server $Vcenter
    }

    $candidates = foreach ($ds in $stores) {
        $files = Invoke-RestMethod -Method Get `
            -Uri "$api/vcenter/datastore/$([uri]::EscapeDataString($ds.Name))/files" `
            -Headers $headers

        foreach ($file in @($files.value ?? $files)) {
            $path = (($file.path ?? $file.name) -replace '\\', '/').TrimStart('/')
            if ($path -notlike '*.vmx') { continue }

            $known = $registered | Where-Object {
                $_.Datastore -ceq $ds.Name -and $_.Path -ceq $path
            } | Select-Object -First 1

            if (-not $known) {
                [pscustomobject]@{
                    Datastore = $ds.Name
                    VmxPath   = $path
                    Reason    = 'No matching registered VM configuration path'
                }
            }
        }
    }

    $candidates | Sort-Object Datastore, VmxPath |
        Export-Csv -Path $Output -NoTypeInformation -Encoding utf8

    if ($candidates) {
        $candidates | Sort-Object Datastore, VmxPath | Format-Table -AutoSize
    } else {
        Write-Output 'No unmatched .vmx files were found in the selected scope.'
    }
}
finally {
    if ($token) {
        Invoke-RestMethod -Method Delete -Uri "$api/session" `
            -Headers $headers -ErrorAction SilentlyContinue
    }
}

Run the script with -Vcenter vcenter.example.com and optionally -Datastore 'datastore-name'. An empty result covers only accessible datastores selected in that run. If an endpoint request fails, resolve the permission or connectivity error before treating the scan as complete.

Storage changes can have wider effects than a single folder, especially with stretched clusters. Use the resilient vSAN design guide as background when checking whether a candidate datastore belongs to an active VMware vSAN configuration.

Interpreting the PowerCLI output

Treat every unmatched .vmx as a lead, not a verdict. Check ESXi host inventory, backup catalogues, templates, and the ownership of accompanying disks. Another registered VM can point to existing virtual disks while its own configuration file is missing, producing a misleading result.

In the Australian market, many organisations run VMware estates alongside AWS and Azure. Imported artefacts or cloud disk images can therefore resemble local virtual hardware. Data-residency and retention expectations also make a dated, read-only audit trail valuable before records or files are removed.

Recommended audit and remediation workflow

Use the first run to validate visibility, permissions, and path matching rather than to clean up a production estate.

Where cleanup is justified, schedule it through the normal change process, test the selected folder or datastore in a maintenance window, and record who approved each removal.

Turn findings into a safe cleanup plan

Start by running the audit against a test datastore, saving the CSV, and comparing every candidate with your backup catalogue and ESXi records. A clean result should mean verified coverage, not merely an empty output file.

Then extend the same check to the remaining datastores, preserve the evidence, and remove only confirmed artefacts under an approved change. That turns a directory cleanup into a controlled recovery decision rather than a risky guess.