During vCenter migrations or cluster relocations, user-defined attributes (Custom Attributes) are often lost because they are not part of the VM inventory but stored as metadata on vCenter objects. Managing hundreds of VMs manually is impractical. PowerCLI provides a clean automation approach: exporting attribute definitions and values from the source vCenter, creating missing attributes on the target vCenter, and then populating the values.
Export: Save Definitions and Values
First establish a connection to the source vCenter. Attribute definitions (names, target types) can be retrieved using Get-CustomAttribute and saved to a CSV file:
Connect-VIServer -Server vc-source.example.com
$attList = Get-CustomAttribute -TargetType VirtualMachine
$attList | Export-Csv ".\exported-attributes.csv" -NoTypeInformation -Encoding UTF8 -Delimiter ";"
To efficiently iterate over actual values, loop through all VMs in a cluster (or use Get-VM for the entire inventory) and utilize the enumerator of the CustomFields property:
$valueList = foreach ($vm in (Get-Cluster "CL01" | Get-VM)) {
foreach ($cf in $vm.CustomFields.GetEnumerator()) {
[PSCustomObject]@{
VMname = $vm.Name
FieldKey = $cf.Key
FieldValue = $cf.Value
}
}
}
$valueList | Export-Csv ".\exported-attributes-value.csv" -NoTypeInformation -Encoding UTF8 -Delimiter ";"
Disconnect-VIServer -Server * -Confirm:$false
The enumerator avoids array index access and directly provides key-value pairs.
Import: Create Attributes and Set Values
On the target vCenter, first import the definitions. Missing attributes are created with New-CustomAttribute, existing ones remain untouched:
Connect-VIServer -Server vc-dest.example.com
$data = Import-Csv ".\exported-attributes-value.csv" -Delimiter ";" -Encoding UTF8
$existing = Get-CustomAttribute -TargetType VirtualMachine -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty Name
$data.FieldKey | Sort-Object -Unique | Where-Object { $_ -notin $existing } | ForEach-Object {
New-CustomAttribute -Name $_ -TargetType VirtualMachine | Out-Null
Write-Host "Created attribute: $_"
}
Subsequently, values are set per VM. Empty values are skipped, VMs not found are logged:
foreach ($vmGroup in ($data | Where-Object { $_.FieldValue } | Group-Object VMname)) {
$vm = Get-VM -Name $vmGroup.Name -ErrorAction SilentlyContinue
if (-not $vm) {
Write-Warning "VM not found: $($vmGroup.Name)"
continue
}
foreach ($line in $vmGroup.Group) {
Set-Annotation -Entity $vm -CustomAttribute $line.FieldKey -Value $line.FieldValue | Out-Null
}
Write-Host "Done: $($vm.Name)"
}
Disconnect-VIServer -Server * -Confirm:$false
Set-Annotation is the modern equivalent of Set-CustomField and works directly with the VM object, improving performance and error handling. Grouping by VM name minimizes Get-VM calls.
Practical Notes
- Cluster vs. Standalone: Replace
Get-Cluster "CL01" | Get-VMwithGet-VMif no clusters are used. - Permissions: The executing account requires rights to read/write Custom Attributes on both vCenters.
- Encoding: UTF8 with BOM (
-Encoding UTF8) avoids Umlaut issues when importing CSVs into Excel.
The scripts are idempotent: repeated imports only modify values that differ and do not create duplicate attributes. This allows safe execution within migration windows or automation pipelines.
The complete scripts are also available for download below.