I recently needed to migrate all VMs from one vCenter to another. I had planned to use the native Cross vCenter vMotion GUI, but I ran into a couple of issues. Although using it for one VM works fine-ish, using it for multiple VMs at a time becomes quite a burden.

The main problems stem from the GUI wants you to pick the portgroup and datastore on the destination vCenter. In theory, this makes sense as there might be differing storage at the destination vCenter. However, it doesn’t inform you during the process where the VMs currently reside. This is a bit tough to explain, so here is a screenshot:

In the picture, we see we are trying to migrate 2 VMs, and it wants destination storage to be selected. It gives no indication though of where the VMs are coming from. If identical storage is presented at both locations, and you want the VMs to land on the same datastore, there isn’t a way to do it through this GUI. You would need to first document where each VM is located before even beginning the migration process, which is silly.

If you do one VM at a time, you are given a VM Origin link during migration to check the source location:

But this doesn’t exist when migrating in batches.

With all this in mind, I created a script to assist with this process. In my case, identical storage and network port names were presented at each location so for the selection process, I only needed to have the script choose based on the port and datastore names the VM was already utilizing.

Two other caveats for this script. The first being I was paranoid about the VMs landing ok at the new vCenter. This means I have multiple networking checks both before and after the migration. Not all VMs have guest names, or even VMtools to check guest names/IPs, so I am checking networking by VMname, IP resolved by VMname, Guestname(where applicable), and Guest IP. So yeah, a little paranoid. When everything is working all right, the checks look something like this:

We can see it does check the IPv4 IP and VMname, before resolving some of the other stuff to IPv6. Regardless there were enough greens for me to proceed, where it would then do the same checks after the migration. Another thing to note is it would prompt before starting the migration, because paranoid.

The second caveat with the script is it will automatically ignore VMs with multiple portgroups or multiple datastores. In my use case, few VMs would fall into this category, and my paranoia demanded I do them manually anyway.


# Connect to Source and Target vCenter Servers
$sourceVC = "SourcevCenter"
$targetVC = "TargetvCenter"

# Define the list of VM names to migrate
#$vmNames = "SingleVM"  # Replace with the names of VMs you want to migrate
$targetClusterName = "Clustername"  # Replace with the target cluster name in the target vCenter
$vmhost = "VMHost" # Used for clearing out a specific ESXi host

# Connect to both vCenter servers
Connect-VIServer -Server $sourceVC 
Connect-VIServer -Server $targetVC 

# Get VMs hosted on the specified ESXi host from the source vCenter
$vmnames = Get-VMHost $vmhost -Server $sourceVC | Get-VM # Can be modified to get all VMs on vCenter by removing vmhost reference

# Loop through each VM and perform migration
foreach ($vmName in $vmNames) {
    
    # Retrieve the VM from the source vCenter
    $vm = Get-VM -Name $vmName -Server $sourceVC
    "VM"
    $vm

    # Get VM guest information
    $vmGuestInfo = Get-VMGuest -VM $vm
    $ipv4Addresses = $vmGuestInfo.IPAddress | Where-Object { $_ -match '^\d{1,3}(\.\d{1,3}){3}$' } | Select-Object -First 1

    # Initial network connectivity test
    Write-Host "Testing initial connectivity with IP $ipv4Addresses" -ForegroundColor Yellow
    $initialping = Test-NetConnection $ipv4Addresses 

    # Check initial ping response
    if ($ipv4Addresses -eq $null) {
        Write-Warning "No IPv4 Address found"
    } elseif ($initialping.PingSucceeded) {
        Write-Host "Initial ping succeeded" -ForegroundColor Green
    } else {
        Write-Warning "Initial ping failed"
    }

    # Test connectivity by VM Name
    Write-Host "Testing connectivity with VM Name $($vm.Name)" -ForegroundColor Yellow
    $pingtest = Test-NetConnection $vm.Name
    if (-not $pingtest.PingSucceeded) {
        Write-Warning "Ping failed for VM Name $($vm.Name)"
    } else {
        Write-Host "Ping succeeded for VM Name $($vm.Name)" -ForegroundColor Green
    }

    # Test connectivity by VM Guest Name
    Write-Host "Testing connectivity with VM Guest Name $($vmGuestInfo.HostName)" -ForegroundColor Yellow
    $pingtest = Test-NetConnection $vmGuestInfo.HostName
    if (-not $pingtest.PingSucceeded) {
        Write-Warning "Ping failed for VM Guest Name $($vmGuestInfo.HostName)"
    } else {
        Write-Host "Ping succeeded for VM Guest Name $($vmGuestInfo.HostName)" -ForegroundColor Green
    }

    # Retrieve source VM's datastore and portgroup
    $sourceDatastore = (Get-HardDisk -VM $vm | Get-Datastore)
    $sourcePortGroup = Get-VM $vm | Get-VirtualPortGroup -Distributed
    $sourcedatastorecenter = $sourceDatastore.Uid.Split("@")[1].Split("/")[0]

    Write-Host "Source Information:" -ForegroundColor Cyan
    $sourcePortGroup.Name
    $sourceDatastore.Name
    $sourcedatastorecenter

    # Validate VM network settings (Portgroup and Datastore)
    if ($sourcePortGroup.Count -gt 1) {
        Write-Warning "$vm has multiple portgroups, skipping."
        continue
    } elseif ($sourcePortGroup.Count -lt 1 -or $sourcePortGroup -eq $null) {
        Write-Warning "$vm has no portgroups, skipping."
        continue
    }

    if ($sourceDatastore.Count -gt 1) {
        Write-Warning "$vm has multiple datastores, skipping."
        continue
    } elseif ($sourceDatastore.Count -lt 1 -or $sourceDatastore -eq $null) {
        Write-Warning "$vm has no datastores, skipping."
        continue
    }

    # Retrieve corresponding datastore and portgroup in the target vCenter
    $targetDatastore = Get-Datastore -Name $sourceDatastore.Name -Server $targetVC
    $targetPortGroup = Get-VirtualPortGroup -Name $sourcePortGroup -Server $targetVC -Distributed
    $targetdatastorecenter = $targetDatastore.Uid.Split("@")[1].Split("/")[0]
  
    $targetCluster = Get-Cluster -Name $targetClusterName -Server $targetVC
    $targetHost = $targetCluster | Get-VMHost | Get-Random -Count 1

    Write-Host "Destination Information:" -ForegroundColor Cyan
    $targetPortGroup.Name
    $targetPortGroup.Uid.Split("@")[1].Split("/")[0]
    $targetDatastore.Name
    $targetdatastorecenter
    $targetHost.Name

    # Confirm migration
    $response = Read-Host "Are you sure you want to continue? (y/n)"
    if ($response -ne "y") {
        Write-Warning "Aborting migration for $vmName"
        continue
    }

    # Execute Cross vCenter vMotion
    Move-VM -VM $vm `
        -Destination $targetHost `
        -Datastore $targetDatastore `
        -NetworkAdapter (Get-NetworkAdapter -VM $vm) `
        -PortGroup $targetPortGroup `
        -Confirm:$true

    Write-Output "Migrated VM '$vmName' to target vCenter with portgroup '$sourcePortGroup' and datastore '$sourceDatastore'."

    # Post-migration connectivity check
    Start-Sleep -Seconds 2
    Write-Host "Testing post-migration connectivity with IP $ipv4Addresses" -ForegroundColor Yellow
    $pingtest = Test-NetConnection $ipv4Addresses
    if (-not $pingtest.PingSucceeded) {
        Write-Warning "Ping failed for VM, considering rollback."
    } else {
        Write-Host "Post-migration ping succeeded" -ForegroundColor Green
    }

    # Check post-migration connectivity by VM Name and Guest Name
    Write-Host "Testing with VM Name $($vm.Name)" -ForegroundColor Yellow
    $pingtest = Test-NetConnection $vm.Name
    if (-not $pingtest.PingSucceeded) {
        Write-Warning "Ping failed for VM Name $($vm.Name), considering rollback."
    } else {
        Write-Host "VM Name $($vm.Name) connectivity verified" -ForegroundColor Green
    }

    Write-Host "Testing with VM Guest Name $($vmGuestInfo.HostName)" -ForegroundColor Yellow
    $pingtest = Test-NetConnection $vmGuestInfo.HostName
    if (-not $pingtest.PingSucceeded) {
        Write-Warning "Ping failed for VM Guest Name $($vmGuestInfo.HostName), considering rollback."
    } else {
        Write-Host "VM Guest Name $($vmGuestInfo.HostName) connectivity verified" -ForegroundColor Green
    }
}

# Optional: Disconnect from vCenter Servers
# Disconnect-VIServer -Server $sourceVC -Confirm:$false
# Disconnect-VIServer -Server $targetVC -Confirm:$false

Some options for updates if I or someone else is ever interested, would be automatic rollback to source vCenter. In my case, with a good number of VMs “failing” tests because of inaccurate or inaccessible guest info this didn’t make sense, and with me monitoring each migrating I could manually move a VM back if necessary.

Second obvious one would be to update to account for multiple portgroups or datastores.

The script is also available on github, where I am going to start dumping more of the scripts I have written.