Hello everyone! Today we have a guest author, Cameron Peppers, who is a Cloud Solution Architect working with our On-Prem VMWare Migration program/field team. He has a long background that is focused on Hyper-V and SCVMM datacenter design, along with migrations from VMware for more years that he can cares to admit.
This blog post provides a PowerShell script for automating the migration of a virtual machine (VM) from most versions of VMware to Hyper-V using SCVMM 2022 UR2+ or 2025 and retaining all settings (yes, even the static IP!). With 10GB or better Layer 2 networking and flash storage, expected downtime is approximately 5 minutes per 100GB of disk.
Disclaimer
The sample scripts are not supported under any Microsoft standard support program or service. The sample scripts are provided AS IS without warranty of any kind. Microsoft further disclaims all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of the scripts be liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to use the sample scripts or documentation, even if Microsoft has been advised of the possibility of such damages.
Best Practices for Migration
- Use SCVMM 2022 UR2+ or 2025 for improved migration speeds due to increased transfer chunk size.
- Ensure low latency network between ESX and Hyper-V Host.
- Configure DNS or host files to divert traffic to Layer 2 Live Migration/vMotion Network with JumboPackets enabled.
- SCVMM and vCenter coordinate the job initiation; data flows directly from ESX to Hyper-V Hosts.
- Use dedicated migration hosts for staging clusters and pre-migration vMotion.
- Ensure ESX Host and Datastore are free of other workloads during migration.
- Disable DRS and backups on the host and datastore of migrating VMs.
- Spread simultaneous migrations across multiple destination hosts.
- Convert BIOS boot VMs to EFI using MBR2GPT before migration for better performance.
PowerShell Migration Script
The following annotated PowerShell script automates the migration process. It includes subnet detection, IP pool creation, VM shutdown and startup, and cleanup steps.
## Notes
## vCenter and your VMWare Hosts need to be added into SCVMM
## Look for "###HardCoded Value" to change specific to your Env.
## In this code VM's use Many Subnets/ VLANs, but all use the same Switch, Logical Network and VM Network, this makes migrations simpler and can be swapped after.
## Destination Hyper-V Host needs to be clustered (unless the VMWare VM is not clustered)
## I hard coded the value of 1 host to use for all migrations so you only have to set the network details once, if you have dynamic optimization enabled, VMM will take care of moving the vm and balancing the load after migration
##Current code is for a single VM but can be midified to take a list via Foreach
#Function1 to determine subnet of the vm, this will be the list that is in your logical network, network site in VMM. (the lsit of available subnets to your logical network) generic subnets used below for example
Function Get-IPAddressSubnet{
param([string[]]$ipaddresslist)
function checkSubnet ([string]$cidr, [string]$ip)
{
$network, [int]$subnetlen = $cidr.Split('/')
$a = [uint32[]]$network.split('.')
[uint32] $unetwork = ($a[0] -shl 24) + ($a[1] -shl 16) + ($a[2] -shl 8) + $a[3]
$mask = (-bnot [uint32]0) -shl (32 - $subnetlen)
$a = [uint32[]]$ip.split('.')
[uint32] $uip = ($a[0] -shl 24) + ($a[1] -shl 16) + ($a[2] -shl 8) + $a[3]
$unetwork -eq ($mask -band $uip)
}
###Example Subnets, if you add them to the same logical network the code can be the same for all.
$subnetlist = @()
$subnetlist += "168.14.4.0/22"
$subnetlist += "168.14.8.0/23"
$subnetlist += "168.14.64.0/22"
$subnetlist += "168.14.0.0/22"
$subnetlist += "168.14.96.0/20"
$subnetlist += "168.14.128.0/20"
$subnetlist += "168.14.80.0/20"
$subnetlist += "168.14.248.0/22"
$subnetlist += "168.14.32.0/20"
$subnetlist += "168.14.112.0/22"
$output = @()
ForEach ($ipaddress in $ipaddresslist) {
$found = $false
ForEach ($subnet in $subnetlist) {
If (checkSubnet $subnet $ipaddress) {
$output += $subnet
$found = $true
}
}
If ($found -eq $false) {
throw "Subnet not found for $ipaddress"
}
}
return $output
}
#Function2 to set gateway for each subnet, removes last octet from subnet and changes it to .1 assuming the gateway for each above is .1
Function Get-IPAddressGateway{
param([string]$subnet)
$threeoctets = $subnet.Substring(0,$subnet.Length-4)
return $threeoctets + "1"
}
# Set to stop on error
$ErrorActionPreference = "stop"
# Prompt to enter the VM Name
$VMName = Read-Host -Prompt 'enter vm name'
###Hard Coded Value
# set VMM Server Name
$VMMServerName = Get-SCVMMServer -ComputerName VMMServer.Domain.com
# Get IP Address of the VM
$IPV4Address = Get-SCVirtualMachine -name $VMNAME | Get-SCVirtualNetworkAdapter | select -exp IPV4Addresses
# Get VLAN ID of the VM
$VLANID = Get-SCVirtualMachine -name $VMNAME | Get-SCVirtualNetworkAdapter | select -exp VLANID
# Assigns matching subnet to above IP based on function1
$Subnet = get-ipaddressSubnet $ipv4address
# Determine IP Gateway for VM based on Subnet from Function2
$Gateway = Get-IPAddressGateway $Subnet
# Get the original machine ID, this will be used later to change its name as you will have to VM's with the same name in VMM
$OldVMID = Get-SCVirtualMachine -name $VMName | select -exp ID
# Get VM allocated Ram - there is a bug that resets it to 1gb sometimes when reading destination host wrong as over allocated, this value is reset at the end of migration to ensure the same RAM as original
$VMRam = Get-SCVirtualMachine -name $vmname | select -exp memory
##################################################
# Shutdown the VM
Get-SCVirtualMachine -Name $vmname | Stop-SCVirtualMachine -Shutdown
##################################################
# Adding a wait to make sure VM is fully powered down
Start-Sleep -s 5
##################################################
# Create a temp IP Pool with above variables, this makes an IP Pool with only this VM's IP in it and names the pool the VMName (NOTE: this IP can not already be part of an IP Pool range)
# Use the GUI v2v wizard and hit view script (instead of finish) to get these ID values easily for your destination Host, this will only need to be set once if you use only 1 host for destination of all migrations
###HardCoded Value
# Get Logical Network 'VM_Traffic' this is your network above that has the subnets defined ( get-sclogicalnetwork | ft name, id) to get ID to place below. Example ID used below
$logicalNetwork = Get-SCLogicalNetwork -ID "96d2bcb0-acc8-4d2a-b404-8778399999999"
###HardCoded Value
# Get Logical Network Definition 'VM_Traffic', same with above, this is the network where your subnets are defined. (get-sclogicalnetworkdefinition | ft name, ID) to find the ID for this network
$logicalNetworkDefinition = Get-SCLogicalNetworkDefinition -ID "dbcbe94a-3b36-477d-b5e6-2d949999999"
# Network Routes
$allNetworkRoutes = @()
# Gateways
$allGateways = @()
$allGateways += New-SCDefaultGateway -IPAddress $Gateway -Automatic
###HardCoded Value
# DNS servers / this is based on all of your above subnets using the same DNS Servers. if you have different DNS for different subnets youl have to recode this piece, or set the logic at the begining.
$allDnsServer = @("168.14.0.2", "168.14.0.3")
# DNS suffixes
$allDnsSuffixes = @()
# WINS servers
$allWinsServers = @()
# Creates the IP Pool
New-SCStaticIPAddressPool -Name $VMName `
-LogicalNetworkDefinition $logicalNetworkDefinition -Subnet $Subnet -IPAddressRangeStart $IPV4Address `
-IPAddressRangeEnd $IPV4Address `
-DefaultGateway $allGateways `
-DNSServer $allDnsServer -DNSSuffix "" -DNSSearchSuffix $allDnsSuffixes -NetworkRoute $allNetworkRoutes -RunAsynchronously
##################################################
# V2V the VM
# Get the VM
$vm = Get-SCVirtualMachine -Name $VMName
# Dynamic guid for job group
$jobgroup = [guid]::NewGuid()
###HardCoded Value
# Destination Host ((((if the VM is Highly Available in VMWare, this below host needs to be a member of a cluster or the migration will fail)))).
$vmHost = Get-SCVMHost | where { $_.Name -eq "HyperVHost.Domain.com" }
# Select the NIC
$virtualNetworkAdapter = Get-SCVirtualNetworkAdapter -VM $VMName
###HardCoded Value
# This ID is specific to the host (use gui wizrd/view script to easily get this value, if you start a SCVMM v2v via the gui and hit view script before finish, youl see the ID for the Switch the VM will us)
# Another way to get id (Get-SCVirtualNetwork -VMHost $vmHost | ft name, id)
$virtualNetwork = Get-SCVirtualNetwork -VMHost $vmHost -Name "Hyper-V_Switch" | where { $_.ID -eq "e5bfb52f-27c3-4878-920f-0a7a59999999" }
###HardCoded Value
# This ID is specific to the host, use above GUI/View Script or (get-scvmnetwork | ft name, id) and find the ID of the vm network tied to the above switch
$vmNetwork = Get-SCVMNetwork -ID "3df8a8ce-9c6b-44d3-948b-383d2c999999"
# New v2v to set the adapter properties
New-SCV2V -VM $vm -VMHost $vmHost -VirtualNetworkAdapter $virtualNetworkAdapter -VirtualNetwork $virtualNetwork `
-VMNetwork $vmNetwork -VLanEnabled $false -JobGroup $jobgroup -RunAsynchronously
### Gen1 default HardCoded Storage path (example used is SMB Storage, for a LUN(CSV) replace with "c:\clusterstorage\CSVName)
New-SCV2V -VM $vm -VMHost $vmHost -JobGroup $jobgroup -StartVM -Trigger -RunAsynchronously `
-Path "\\SMBStorageDevice.domain.com\VMShare" -StartAction "TurnOnVMIfRunningWhenVSStopped" -StopAction "SaveVM" -JobVariable "job"
###GEN 2 Version
#New-SCV2V -VM $vm -VMHost $vmHost -JobGroup $jobgroup -StartVM -Trigger -generation 2 -RunAsynchronously `
#-Path "\\SMBStorageDevice.domain.com\VMShare" -StartAction "TurnOnVMIfRunningWhenVSStopped" -StopAction "SaveVM" -JobVariable "job"
## Track the job in a loop until complete before continuing to the next step
## You can also see these in SCVMM Running Jobs
$JobNameString = $job.CmdletName+" "+$Job.ResultName
while ($Job.Status -eq "running")
{
Write-Progress -Activity "$JobNameString" -Status $Job.CurrentStep -PercentComplete $Job.ProgressValue;
Start-Sleep -Seconds 60;
}
## take action after job completes
Write-Host $JobNameString $Job.Status
if ($Job.Status -eq "failed")
{
# Job Failed
Write-Output $Job.ErrorInfo;
}
else
{
# Job Succeded
write-host "the next job will start"
}
##################################################
# Rename the VMWare VM to *_migrated + date, this is how it appears in VMM and VSphere, does not effect actual machine name
$date = get-date -Format "MMdd"
Get-SCVirtualMachine -id $OldVMID | Set-SCVirtualMachine -Name ($VMName + "_migrated" + "$date")
##################################################
# Adding a wait to ensure machine fully boots
Start-Sleep -s 240
##################################################
# Set the Static IP address back to the VM
# This is defined above
$VM = Get-SCVirtualMachine -Name $VMName
###HardCoded Value
# Select The VM Network "vm_network" hardcoded in here.
$VMNetwork = Get-SCVMNetwork -VMMServer $VMMServerName -Name VM_Network
# Select the IP Address Pool
$IPPool = Get-SCStaticIPAddressPool -VMMServer $VMMServerName -Name $VMName
# Select which Virtual NIC you want to apply Static IP Mode
$NIC = Get-SCVirtualNetworkAdapter -VMMServer $VMMServerName -VM $VM.Name
# Get free IP address from the IP Pool
$IPAddress = Grant-SCIPAddress -GrantToObjectType VirtualNetworkAdapter -GrantToObjectID $VM.VirtualNetworkAdapters[($NIC.SlotID)].ID -StaticIPAddressPool $IPPool -Description $VM.Name
# Set the vmNIC with a IP Address from the IP Pool and set the VLAN ID if needed
If ($vlanID -eq 0) {
Set-SCVirtualNetworkAdapter -VMMServer $VMMServerName -VirtualNetworkAdapter $VM.VirtualNetworkAdapters[($NIC.SlotID)] -VMNetwork $VMNetwork -IPv4AddressType Static -IPv4Addresses $IPAddress.Address
}
else {
Set-SCVirtualNetworkAdapter -VMMServer $VMMServerName -VirtualNetworkAdapter $VM.VirtualNetworkAdapters[($NIC.SlotID)] -VMNetwork $VMNetwork -IPv4AddressType Static -IPv4Addresses $IPAddress.Address -VLanEnabled $true -VLanID $VLANID
}
##################################################
# Adding a wait to ensure setting have applied
Start-Sleep -s 15
##################################################
# Shutdown the VM
Get-SCVirtualMachine -Name $vmname | Stop-SCVirtualMachine -Shutdown
##################################################
# Enable processor compatibility and reset ram to same from vmware (sometimes it will warning and default to 1gb, this is to ensure it sets correct)
Set-SCVirtualMachine -VM $vmname -CPULimitFunctionality $false -CPULimitForMigration $true -MemoryMB $VMRam
##################################################
# Start the VM
Get-SCVirtualMachine -Name $vmname | Start-VM
##################################################
## Clean Up Temp IP Pools
# Revoke the IP address from the pool
$IP = Get-SCIPAddress -IPAddress $IPAddress
$IP | Revoke-SCIPAddress
##################################################
# Adding a wait of 15 seconds
Start-Sleep -s 15
##################################################
# Delete the temp IP Pool
Remove-SCStaticIPAddressPool -StaticIPAddressPool $IPPool
##################################################
# Make sure VM stays online at end (noticed it shuts down after first reboot on some OS's after some time up, vmwaretools causes this also.
Start-Sleep -s 300
# Start the VM if stopped
$VirtualMachineState = Get-SCVirtualMachine -Name $vmname | select -exp VirtualMachineState
If ($VirtualMachineState -eq "Running") {
write-host "Migration Complete"
}
else {
Get-SCVirtualMachine -Name $vmname | Start-VM
write-host "Migration Complete - start added"
}
##Done
Additional References
- Microsoft Virtualization Migration Options - Microsoft Community Hub
- Azure Arc | Microsoft Learn (PDF download: azure azure-arc | Microsoft Learn)
- Hyper-V on Windows Server | Microsoft Learn (PDF download: windows-server virtualization | Microsoft Learn)
- Azure Arc-enabled System Center Virtual Machine Manager | Microsoft Learn
- Virtual Machine Manager documentation | Microsoft Learn (PDF download: system-center vmm-sc-vmm-2022 | Microsoft Learn)
- Windows Admin Center Overview | Microsoft Learn (PDF download: windows-server manage windows-admin-center | Microsoft Learn)