Skip to main content

Powershell - Migrate VMs from Hyper-V to vSphere


filipsmeets
Forum|alt.badge.img
[CmdletBinding()]
param (
    # CSv file tab delimited with name and Vlan
    [Parameter(Mandatory = $true)]
    [string[]]
    $CSV,		

    # The datacenter where you want to move the workload to.
    [Parameter(Mandatory = $true)]
    [string]
    $Datacenter,

    # The Cluster where you want to move the workload to.
    [Parameter(Mandatory = $true)]
    [string]
    $Cluster,

    # Errorlog
    [Parameter()]
    [string]
    $ErrorLog = 'E:\Scripts\ErrorLog.txt',

    # Enable error logging
    [Parameter()]
    [switch]
    $LogErrors,

    # Enable error logging
    [Parameter()]
    [Int32]
    $HostIndex = 0
)

#$Global:ErrorLog = 'E:\Scripts\ErrorLog.txt'

function Write-ToLog {
    [cmdletbinding()]
    Param (
        [string]
        [parameter(ValueFromPipeline)]
        $Text
    )

    if ($LogErrors) {
        $timestamp = Get-Date
        Write-Output "$timestamp :: $Text" | Tee-Object $ErrorLog -Append
    }
}

function Move-Workload {
    <#
    .SYNOPSIS
        Performs a migration of VMs running on Hyper-V to vSphere.
    .DESCRIPTION
        Move-Workload uses Veeam's Instant VM Recovery capability to migrate VMs from Hyper-V to Vsphere.
        The script will accept one or multiple Computers/Servers to migrate. It will then lookup the latest restore
        point for each Computer and start a Veeam Instant VM Recovery. The restored VM on vSphere will run
        directly from the Backup Repository. After the Veeam Instant VM Recovery, a storage vMotion will be
        initiated to migrate the VM to production storage and finalize the migration.
    .PARAMETER ComputerName
        One or more computer names or VM names.
    .PARAMETER Datacenter
        The Datacenter to where you want to migrate.
    .PARAMETER Cluster
        The Cluster in the Datacenter to where you want to migrate.
    .PARAMETER ErrorLog
        When used with -LogErrors, specifies the file path and name to which failed computer names will be written.
    .PARAMETER LogErrors
        Specify this switch to create a text log file of computers that could not be migrated.
    .EXAMPLE
        PS C:\> Get-Content "E:\Scripts\Computers.csv" | Move-Workload -Datacenter 'DCR2' -Cluster 'Workloads'
        The command will read the list of ComputerNames/hostnames from the CSV file and then migrate each of them
        to the Datacenter and Cluster provided.
    .EXAMPLE
        PS C:\> Move-Workload MAHD-BRU-946,DCRX-LWS-D908 -Datacenter 'DCR2' -Cluster 'Workloads' -LogErrors -Verbose
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    .NOTES
        General notes
    #>

    [CmdletBinding()]
    param (
        # The name of the computer to migrate and Vlan
        [Parameter(Mandatory = $true)]
        [string[]]
        $ComputerName,

        # The Vlan to connect to
        [Parameter(Mandatory = $true)]
        [string[]]
        $vlan,		

        # The datacenter where you want to move the workload to.
        [Parameter(Mandatory = $true)]
        [string]
        $Datacenter,

        # The Cluster where you want to move the workload to.
        [Parameter(Mandatory = $true)]
        [string]
        $Cluster
    )

    begin {

    }

    process {
        Write-ToLog "Latest available restore point for server $ComputerName was created on $($restorepoint.CreationTime)"

        Write-ToLog "Selecting the most suitable ESXi host and Datastore based on available resources"
        #$vmhost=(Get-Datacenter -Name $Datacenter | Get-Cluster -Name $Cluster |Get-VMHost -State Connected |Sort-Object $_.MemoryUsageMhz -Descending | Select-Object -First 1)
        $vmhost=(Get-Datacenter -Name $Datacenter | Get-Cluster -Name $Cluster |Get-VMHost | Select-Object -Index $HostIndex)
        $server = Get-VBRServer -Name $vmhost.name
        $datastore = Find-VBRViDatastore -Server $server -Name "*HSA*" |Sort-Object -Property FreeSpace -Descending | Select-Object -First 1

        Write-ToLog "Server $ComputerName will be migrated to VMware Host $($server.Name) and stored on Datastore $datastore"

        Write-ToLog "Start Veeam Instant VM Recovery of server $ComputerName"
        Start-VBRViComputerInstantRecovery -RestorePoint $restorepoint -Server $server -CacheDatastore $datastore -PowerOnAfterRestoring:$false -ConnectVMToNetwork:$true -Reason "VM migration"

        Write-ToLog "Veeam Instant VM Recovery for server $ComputerName completed."

        #Migrate to production site:
        Write-ToLog "Starting migration of server $ComputerName to production Datastore"
        $entity = Find-VBRViEntity -Name $ComputerName

        Start-VBRQuickMigration -Entity $entity -server $server -datastore $datastore
        Write-ToLog "Server $ComputerName has been moved to production Datastore"

        #Stop Instant VM Recovery
        Write-ToLog "Stopping Veeam Instant VM Recovery"            
        Get-VBRInstantRecovery |Where-Object{$_.VmName -like $ComputerName} |Stop-VBRInstantRecovery
        Write-ToLog "Server $ComputerName has been successfully migrated"

        #Change to correct VLAN
        $VMwareNetwork = Import-csv ".\vlan.csv" |Where-Object {$_.vlanid -eq $vlan -or $_.system -match $vlan} |Select-Object -Expand Portgroupname
        $NIC = Get-NetworkAdapter -VM $ComputerName
        Set-NetworkAdapter -NetworkAdapter $NIC -Portgroup $VMwareNetwork -Confirm:$false

        Write-ToLog "$ComputerName has been successfully migrated"

        #Start-vm $ComputerName        
    }

    end {        

    }
}

function Set-VMXNET3 {

 <#
    .SYNOPSIS
        Short description
    .DESCRIPTION
        Long description
    .EXAMPLE
        PS C:\> <example usage>
        Explanation of what the example does
    .INPUTS
        Inputs (if any)
    .OUTPUTS
        Output (if any)
    .NOTES
        General notes
 #>   

    [CmdletBinding()]
    param (
        # CSv file tab delimited with name and Vlan
        [Parameter(Mandatory = $true)]
        [string[]]
        $ComputerName,

        # Parameter help description
        [Parameter()]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $GuestCred = [System.Management.Automation.PSCredential]::Empty

        )

    begin {

    }

    process {
        Write-ToLog "Determining if the Guest OS is Windows or Linux"

        $VM = Get-VM -Name $ComputerName

        if ($VM.GuestId -like "*Windows*") {
            Write-ToLog "The Guest OS is Windows"

            Write-ToLog "Starting VM $ComputerName"
            Start-VM $ComputerName -Verbose

            #Waiting for VMTools to be installed and running
            while ($VM.ExtensionData.Guest.ToolsRunningStatus -ne "guestToolsRunning") {
                Write-ToLog "Waiting for VMTools installation via SCCM"
                Start-Sleep 15
                $VM = Get-VM -Name $ComputerName
            }                        

            Write-ToLog "VMWare Tools installed and running on $Computername"            

            try {
                #Backup network configuration
                Write-ToLog "Taking backup of network configuration"
                Invoke-VMScript -VM $ComputerName -ScriptText 'netsh interface dump > "c:\temp\netcfg.dat"' -GuestCredential $GuestCred -Verbose

                $ScriptRun = $true

            }
            catch {
                Write-ToLog "Unable to take network configuration backup"    
            }

            if ($ScriptRun) {
                try {
                    Write-ToLog "Checking for network configuration backup file"
                    Invoke-VMScript -VM $ComputerName -ScriptText 'dir "c:\temp\netcfg.dat"' -GuestCredential $GuestCred -Verbose

                    $ScriptRun = $true
                }
                catch {
                    Write-ToLog "Network configuration backup file not present"
                }
            }

            if ($ScriptRun) {
                Write-ToLog "Network configuration backup created"

                Write-ToLog "Shutting down VM $ComputerName"
                $VM | Shutdown-VMGuest -Confirm:$false                

            }

            while ($VM.PowerState -ne "PoweredOff") {
                Write-ToLog "Waiting for $ComputerName to shutdown"
                Start-Sleep 5
                $VM = Get-VM -Name $ComputerName

            }            

            Write-ToLog "$Computername has been shutdown"

            #Change E1000 to VMXNET3
            Write-ToLog "Change NIC from E1000 to VMXNET3"
            Get-VM -Name $ComputerName |Get-NetworkAdapter |Set-NetworkAdapter -Type Vmxnet3 -Confirm:$false -Verbose

            Write-ToLog "Starting VM $ComputerName"
            Start-VM -VM $ComputerName

            while ($VM.ExtensionData.Guest.ToolsRunningStatus -ne "guestToolsRunning") {
                Write-ToLog "Waiting for $ComputerName to Power On"
                Start-Sleep 5
                $VM = Get-VM -Name $ComputerName

            } 

            #Restore network configuration on new NICs
            Write-ToLog "Restore network configuration to the new VMXNET3 network adapter"
            Invoke-VMScript -VM $ComputerName -ScriptText 'netsh exec "c:\temp\netcfg.dat"' -Verbose

		}			
        else{
            Write-ToLog "The Guest OS is Linux"
            Write-ToLog "Setting VM Guest OS to CentOS7 "
            get-vm -name $ComputerName |set-vm -guestid "centos7_64Guest" -Confirm:$false

            #Change E1000 to VMXNET3
            Write-ToLog "Changing NIC from E1000 to VMXNET3"
            Get-VM -Name $ComputerName |Get-NetworkAdapter |Set-NetworkAdapter -Type Vmxnet3 -Confirm:$false

            #Move VM to LNX folder
            Write-ToLog "Moving VM to LNX folder"
            $VMFolder = Get-Folder -Type VM -Name "LNX"
            Get-VM -Name $ComputerName | Move-VM -Destination $VMFolder

            Write-ToLog "Starting VM $ComputerName"
            Start-VM $ComputerName
        }               
    }

    end {

    }
}

function Set-VMHardware {
    [CmdletBinding()]
    param (
        #
        [Parameter(Mandatory = $true)]
        [string]
        $ComputerName
        )

        begin {

        }

        process {            
            Write-ToLog "Change VM $ComputerName Hardware version"
            Set-VM $ComputerName -HardwareVersion vmx-19 -Confirm:$false -Verbose
    }

    end {

    }
}

function Remove-FromHyperVBackup {
    [CmdletBinding()]
    param (
        #
        [Parameter(Mandatory = $true)]
        [string]
        $ComputerName
        )

        begin {

        }

        process {
            #Exclude VM from Hyper-V Backup job
            Get-VBRJob |Where-Object{$_.SourceType -eq "HyperV"} | Get-VBRJobObject -Name $ComputerName | Remove-VBRJobObject -Verbose
            Write-ToLog "VM $ComputerName has been migrated and excluded from the Veeam Hyper-V Backup Job"

    }

    end {

    }
}

function Start-Migration {
    [CmdletBinding()]
    param (

    )

    begin {
        $StartTime = Get-Date
        Write-ToLog "`n`n"
        Write-ToLog "Start Migration at $StartTime"
        Write-ToLog "Log name is $ErrorLog"
        Write-ToLog "CSV file with list of computers used to migrate is $CSV"
        Write-ToLog ("=" * 80)

        #Connect to local Veeam Backup Server
        Connect-VBRServer

        #Connect to vSphere
        $credVC = Get-Credential -Message "Provide credentials to connect to vCenter"

        Set-PowerCLIConfiguration -InvalidCertificateAction Ignore -Confirm:$false -Scope User -ParticipateInCeip $false
        Connect-VIServer -Server vcenter.labo.be -credential $credVC        

        #Store Windows Guest Credentials
        $GuestCred = Get-Credential -Message "Provide Windows Guest Credentials or cancel if Linux"

    }

    process {

        $computers = Import-csv -delimiter "`t" $csv

        foreach ($Computer in $Computers) {

            $ComputerName = $computer.name
            $vlan = $computer.vlan

            Write-ToLog "Querying computer $ComputerName for restorepoints"
            $VBRBackup = Get-VBRBackup |Where-Object {$_.JobSourceType -eq "HyperV"}
            $restorepoint = $VBRBackup | Get-VBRRestorePoint -Name $ComputerName | Sort-Object creationtime -Descending | Select-Object -First 1

            if ($null -eq $restorepoint) {
                Write-Warning "Could not start migration for server $ComputerName. No restore points found."

                if ($LogErrors) {
                    Write-ToLog "WARNING :: $ComputerName No restore points found"
                }
            }
            else {        
                Move-Workload -ComputerName $ComputerName -vlan $vlan -Datacenter $Datacenter -Cluster $Cluster -Verbose

                Set-VMHardware -ComputerName $ComputerName -Verbose

                Set-VMXNET3 -ComputerName $ComputerName -GuestCred $GuestCred -Verbose

                Remove-FromHyperVBackup -ComputerName $ComputerName -Verbose
            }
        }


    }

    end {
        #Disconnect from local Veeam Backup Server
        Disconnect-VBRServer

        #Disconnect from vCenter
        Disconnect-VIServer -Confirm:$false

    }
}

$data
Start-Migration

Hi guys

I wanted to share a powershell script we made for a customer to migrate VMs from Hyper-V to vSphere. It’s not perfect but it gets the job done.

There are still some issues with the transformation of E1000 to VMXNET3. The Linux team managed to get this working by using puppet. I’m still working on the Windows part.

As the Veeam environment is not domain joined, we cannot use remote powershell.

VMware Tools on Windows machines are pushed by SCCM.

 

 

 

12 comments

JMeixner
Forum|alt.badge.img+17
  • Veeam Vanguard
  • 2650 comments
  • October 18, 2021

Nice script, thank you @filipsmeets :thumbsup_tone3:


Chris.Childerhose
Forum|alt.badge.img+21
  • Veeam Legend, Veeam Vanguard
  • 8395 comments
  • October 18, 2021

Wow another one to add to the library.  Nice one @filipsmeets 


marcofabbri
Forum|alt.badge.img+13
  • On the path to Greatness
  • 990 comments
  • October 18, 2021

Nice share, just to add another info for this thread: vcenter converter / standalone converter do the V2V trick too.


filipsmeets
Forum|alt.badge.img
  • Author
  • New Here
  • 1 comment
  • October 21, 2021

Thx all.

I created another script to automate the E1000 to VMXNET3 change on the Windows machines. You will have to place this script locally on the machine and then schedule a task so that it runs at startup.

You can do this for example with a Group Policy.

$path = "C:\TEMP\NetIP.json"

if (Test-Path $path) {
    Write-Host "IP Configuration backup already exists. Skipping ..."

}
else {
    $NetIP = Get-NetIPConfiguration

    #Check if IP address is not APIPA
    if ($NetIP.IPv4Address.IPAddress -inotlike "169.25*") {
        $myObject = [PSCustomObject]@{
            IPAddress = $NetIP.IPv4Address.IPAddress
            Subnet = $NetIP.IPv4Address.PrefixLength
            Gateway = $NetIP.IPv4DefaultGateway.NextHop
            DNS1 = $NetIP.DNSServer.ServerAddresses[0]
            DNS2 = $NetIP.DNSServer.ServerAddresses[1]
        }

        $myObject | ConvertTo-Json -depth 1 | Out-File $path

    }    
}

$hypervisor = Get-CimInstance -ClassName Win32_ComputerSystem | Select-Object Manufacturer

#Check if VM is running on VMware
if ($hypervisor -match "vmware") {

    ##Get CD-ROM driveletter and path to VMTools installer
    $cddrive = Get-Volume | Where-Object{$_.DriveType -eq "CD-ROM"}
    $installer = $cddrive.DriveLetter + ":\setup64.exe"
    $arg = "/S /v /qn ADDLOCAL=ALL"

    #Check if VMTools ISO is mounted
    if (Test-Path $installer) {
        Start-Process $installer -ArgumentList $arg -Wait
    }

    #Check if there is a backup of the IP configuration and restore to new VMXNET3
    elseif (Test-Path $path) {
        $myObject = Get-Content $path | ConvertFrom-Json
        $Alias = Get-NetIPConfiguration | Where-Object {$_.InterfaceDescription -like "*vmxnet*" } | Select-Object -ExpandProperty InterfaceAlias
        New-NetIPAddress -InterfaceAlias $alias -IPAddress $myObject.IPAddress -PrefixLength $myObject.Subnet -DefaultGateway $myObject.Gateway
        Set-DnsClientServerAddress -InterfaceAlias $alias -ServerAddress ($myObject.DNS1,$myObject.DNS2)
    }
}

For the scheduled task action, use:

Program: powershell.exe

Arguments: -ExecutionPolicy Bypass -file "C:\Temp\InstallVMTools.ps1”

 


Chris.Childerhose
Forum|alt.badge.img+21
  • Veeam Legend, Veeam Vanguard
  • 8395 comments
  • October 21, 2021
filipsmeets wrote:

Thx all.

I created another script to automate the E1000 to VMXNET3 change on the Windows machines. You will have to place this script locally on the machine and then schedule a task so that it runs at startup.

You can do this for example with a Group Policy.

$path = "C:\TEMP\NetIP.json"

if (Test-Path $path) {
    Write-Host "IP Configuration backup already exists. Skipping ..."

}
else {
    $NetIP = Get-NetIPConfiguration

    #Check if IP address is not APIPA
    if ($NetIP.IPv4Address.IPAddress -inotlike "169.25*") {
        $myObject = [PSCustomObject]@{
            IPAddress = $NetIP.IPv4Address.IPAddress
            Subnet = $NetIP.IPv4Address.PrefixLength
            Gateway = $NetIP.IPv4DefaultGateway.NextHop
            DNS1 = $NetIP.DNSServer.ServerAddresses[0]
            DNS2 = $NetIP.DNSServer.ServerAddresses[1]
        }

        $myObject | ConvertTo-Json -depth 1 | Out-File $path

    }    
}

$hypervisor = Get-CimInstance -ClassName Win32_ComputerSystem | Select-Object Manufacturer

#Check if VM is running on VMware
if ($hypervisor -match "vmware") {

    ##Get CD-ROM driveletter and path to VMTools installer
    $cddrive = Get-Volume | Where-Object{$_.DriveType -eq "CD-ROM"}
    $installer = $cddrive.DriveLetter + ":\setup64.exe"
    $arg = "/S /v /qn ADDLOCAL=ALL"

    #Check if VMTools ISO is mounted
    if (Test-Path $installer) {
        Start-Process $installer -ArgumentList $arg -Wait
    }

    #Check if there is a backup of the IP configuration and restore to new VMXNET3
    elseif (Test-Path $path) {
        $myObject = Get-Content $path | ConvertFrom-Json
        $Alias = Get-NetIPConfiguration | Where-Object {$_.InterfaceDescription -like "*vmxnet*" } | Select-Object -ExpandProperty InterfaceAlias
        New-NetIPAddress -InterfaceAlias $alias -IPAddress $myObject.IPAddress -PrefixLength $myObject.Subnet -DefaultGateway $myObject.Gateway
        Set-DnsClientServerAddress -InterfaceAlias $alias -ServerAddress ($myObject.DNS1,$myObject.DNS2)
    }
}

For the scheduled task action, use:

Program: powershell.exe

Arguments: -ExecutionPolicy Bypass -file "C:\Temp\InstallVMTools.ps1”

 

This is great and completes things.  Nice work.


JMeixner
Forum|alt.badge.img+17
  • Veeam Vanguard
  • 2650 comments
  • October 21, 2021

Nice, good addition to your script. :thumbsup_tone3:


marcofabbri
Forum|alt.badge.img+13
  • On the path to Greatness
  • 990 comments
  • October 21, 2021

Nice work man!


  • New Here
  • 1 comment
  • October 27, 2021

Thank you so much! This was such a great help as we are in the exact same process of migrating quite a lot of Hyper-V VMs to Vmware.


  • New Here
  • 1 comment
  • February 24, 2022

Hi, this script looks amazing. I have a use case to migrate VMs the other way - from VMWare to Hyper-V - you don’t happen to have a script for that? Many thanks


vNote42
Forum|alt.badge.img+13
  • On the path to Greatness
  • 1246 comments
  • February 24, 2022

Thanks for sharing, @filipsmeets ! I will take a closer look at, when I have to do this stuff.


victorwu
Forum|alt.badge.img+7
  • Veeam Vanguard
  • 372 comments
  • February 24, 2022

@filipsmeets thanks for sharing. I am working in a VM migration project (HyperV to VMware ESXi). Your script can help me.:wink:


  • New Here
  • 1 comment
  • January 2, 2023
filipsmeets wrote:

Thx all.

I created another script to automate the E1000 to VMXNET3 change on the Windows machines. You will have to place this script locally on the machine and then schedule a task so that it runs at startup.

You can do this for example with a Group Policy.

$path = "C:\TEMP\NetIP.json"

if (Test-Path $path) {
    Write-Host "IP Configuration backup already exists. Skipping ..."

}
else {
    $NetIP = Get-NetIPConfiguration

    #Check if IP address is not APIPA
    if ($NetIP.IPv4Address.IPAddress -inotlike "169.25*") {
        $myObject = [PSCustomObject]@{
            IPAddress = $NetIP.IPv4Address.IPAddress
            Subnet = $NetIP.IPv4Address.PrefixLength
            Gateway = $NetIP.IPv4DefaultGateway.NextHop
            DNS1 = $NetIP.DNSServer.ServerAddresses[0]
            DNS2 = $NetIP.DNSServer.ServerAddresses[1]
        }

        $myObject | ConvertTo-Json -depth 1 | Out-File $path

    }    
}

$hypervisor = Get-CimInstance -ClassName Win32_ComputerSystem | Select-Object Manufacturer

#Check if VM is running on VMware
if ($hypervisor -match "vmware") {

    ##Get CD-ROM driveletter and path to VMTools installer
    $cddrive = Get-Volume | Where-Object{$_.DriveType -eq "CD-ROM"}
    $installer = $cddrive.DriveLetter + ":\setup64.exe"
    $arg = "/S /v /qn ADDLOCAL=ALL"

    #Check if VMTools ISO is mounted
    if (Test-Path $installer) {
        Start-Process $installer -ArgumentList $arg -Wait
    }

    #Check if there is a backup of the IP configuration and restore to new VMXNET3
    elseif (Test-Path $path) {
        $myObject = Get-Content $path | ConvertFrom-Json
        $Alias = Get-NetIPConfiguration | Where-Object {$_.InterfaceDescription -like "*vmxnet*" } | Select-Object -ExpandProperty InterfaceAlias
        New-NetIPAddress -InterfaceAlias $alias -IPAddress $myObject.IPAddress -PrefixLength $myObject.Subnet -DefaultGateway $myObject.Gateway
        Set-DnsClientServerAddress -InterfaceAlias $alias -ServerAddress ($myObject.DNS1,$myObject.DNS2)
    }
}

For the scheduled task action, use:

Program: powershell.exe

Arguments: -ExecutionPolicy Bypass -file "C:\Temp\InstallVMTools.ps1”

 

I hade some problem with only having one dns server. So I made some changes to the script.

https://gist.github.com/andersalavik/2a0954c3c2e8f96b221a30e937d4d6ae

 


Comment