Skip to content

Instantly share code, notes, and snippets.

@MarcelMeurer
Last active September 8, 2025 14:46
Show Gist options
  • Select an option

  • Save MarcelMeurer/aab2ef8f36d7c634beebea771f54bf30 to your computer and use it in GitHub Desktop.

Select an option

Save MarcelMeurer/aab2ef8f36d7c634beebea771f54bf30 to your computer and use it in GitHub Desktop.
Create a Azure VM Genertation V2 from an existing V1 virtual machine (opt. including Trusted launch, secure boot, and vTPM)
param(
[ValidateNotNullOrEmpty()]
[ValidateSet('Default', 'StartInternalTask-1', 'StartInternalTask-2', 'StartInternalTask-3', 'CheckInternalTask')]
[string] $mode = "Default"
)
$ErrorActionPreference = "Stop"
#region Configuration
$subscriptionId="xxxxxxxx-xxxxx-xxxx-xxxx-xxxxxxxxxxxx"
# source VM (generation V1)
$sourceVmName = "T-WVD-Basic-20"
$sourceVmResourceGroup = "WVD_TEMPLATES"
# target VM to be build (generation V2) - This wil be a copy of the source VM but as V2 generation
$targetVmName = "T-WVD-Basic-30" # max. length is 15
$targetVmResourceGroup = "WVD_TEMPLATES"
$enabledTrustedLaunch = $true
$usePremium = $false # if $true, this speeds up the process but can only be done, if the VM size suppports premium disks
$tempDiskSizeGb = 512 # must be larger to store the wim file of the original disk of the source
#endregion
#region InternalMethods
$scriptPath = $PSCommandPath
if (-not $PSCommandPath) {$scriptPath="C:\1Drive\OneDrive - sepago GmbH\Desktop\Convert-VmV1toV2.ps1"}
$localPath = Split-Path $scriptPath -Resolve
$logFileName = "$(Split-Path $scriptPath -Leaf).log"
function LogWriter($message) {
$message = "$(Get-Date ([datetime]::UtcNow) -Format "o") $message"
write-host($message)
if ([System.IO.Directory]::Exists($env:temp)) { try { write-output($message) | Out-File "$localPath\$logFileName" -Append } catch {} }
}
#endregion InternalMethods
#region MainApp
LogWriter ("Starting in mode: $mode")
#region CreateTheAzureResources
if ($mode -eq "Default") {
if(!(Get-AzContext)) {
# Connect to Azure if no connection exists
Connect-AzAccount
}
# select subscription
Get-AzSubscription -SubscriptionId $subscriptionId | Select-AzSubscription
# check, if some of the new resources exist
if (Get-AzDisk -ResourceGroupName $targetVmResourceGroup -DiskName "$($sourceVmName)-Disk-Copy" -ErrorAction SilentlyContinue) {LogWriter "The target disk $sourceVmName exist. Please delete it first."; break}
if (Get-AzDisk -ResourceGroupName $targetVmResourceGroup -DiskName "$($targetVmName)-Disk-Converted" -ErrorAction SilentlyContinue) {LogWriter "The converted disk $($targetVmName)-Disk-Converted exist. Please delete it first."; break}
if (Get-AzSnapshot -ResourceGroupName $targetVmResourceGroup -SnapshotName "$($sourceVmName)-Disk-Snap" -ErrorAction SilentlyContinue) {LogWriter "The snapshot $($sourceVmName)-Disk-Snap exist. Please delete it first."; break}
if (Get-AzVm -ResourceGroupName $targetVmResourceGroup -Name $targetVmName -ErrorAction SilentlyContinue) {LogWriter "The target VM $targetVmName exist. Please delete it first."; break}
# read data of the existing VM
LogWriter ("Getting data of the source VM")
$sourceVm=Get-AzVm -ResourceGroupName $sourceVmResourceGroup -Name $sourceVmName
$sourceNic=Get-AzNetworkInterface -ResourceId $sourceVm.NetworkProfile.NetworkInterfaces[0].Id
$location=$sourceNic.Location
$subnetId=$sourceNic.IpConfigurations[0].Subnet.Id
$sourceDisk = Get-AzDisk -ResourceGroupName $sourceVm.StorageProfile.OsDisk.ManagedDisk.Id.Split("/")[4] -DiskName $sourceVm.StorageProfile.OsDisk.ManagedDisk.Id.Split("/")[8]
if ($sourceDisk.HyperVGeneration -like "V2") {
Write-Host "Source VM is still a V2 VM"
exit
}
# create target VM with a temporary Windows 11 to do the migration
LogWriter ("Creating the target VM (V2)")
$psc = New-Object System.Management.Automation.PSCredential("vmAdmin", (ConvertTo-SecureString "Sup+rTempS+cret123---" -AsPlainText -Force))
$nic = New-AzNetworkInterface -Name "nic-$($targetVmName)" -ResourceGroupName $targetVmResourceGroup -Location $location -SubnetId $subnetId -Force
$vmConfig = New-AzVMConfig -VMName $targetVmName -VMSize $sourceVm.HardwareProfile.VmSize
$vmConfig = Set-AzVMBootDiagnostic -VM $vmConfig -Enable
$vmConfig = Set-AzVMSourceImage -VM $vmConfig -PublisherName "MicrosoftWindowsServer" -Offer "WindowsServer" -Skus "2022-Datacenter-g2" -Version "latest" # must be a V2 image
$vmConfig = Set-AzVMOSDisk -VM $vmConfig -DiskSizeInGB $tempDiskSizeGb -CreateOption FromImage
$vmConfig = Set-AzVMOperatingSystem -VM $vmConfig -ComputerName $targetVmName -Windows -EnableAutoUpdate -Credential $psc
$vmConfig = Add-AzVMNetworkInterface -VM $vmConfig -Id $nic.Id
if ($enabledTrustedLaunch) {
$vmConfig = Set-AzVMSecurityProfile -VM $vmConfig -SecurityType TrustedLaunch
$vmConfig = Set-AzVmUefi -VM $vmConfig -EnableVtpm $true -EnableSecureBoot $true
} else {
$vmConfig = Set-AzVMSecurityProfile -VM $vmConfig -SecurityType Standard
}
if ($sourceVm.LicenseType -eq $null) {
New-AzVM -VM $vmConfig -ResourceGroupName $targetVmResourceGroup -Location $location
} else {
New-AzVM -VM $vmConfig -ResourceGroupName $targetVmResourceGroup -Location $location -LicenseType $sourceVm.LicenseType
}
$targetVm = Get-AzVM -ResourceGroupName $targetVmResourceGroup -Name $targetVmName
$targetDiskOrg = Get-AzDisk -ResourceGroupName $targetVmResourceGroup -DiskName $targetVm.StorageProfile.OsDisk.Name
# copy the source disk with a snapshot
LogWriter ("Creating a copy of the source VM")
$snapShotConfig = New-AzSnapshotConfig -SourceUri $sourceVm.StorageProfile.OsDisk.ManagedDisk.Id -Location $location -CreateOption copy
$sourceDiskSnap = New-AzSnapshot -ResourceGroupName $targetVmResourceGroup -SnapshotName "$($sourceVmName)-Disk-Snap" -Snapshot $snapShotConfig # clean-up after use
$diskConfig = New-AzDiskConfig -Location $location -SourceResourceId $sourceDiskSnap.Id -CreateOption Copy -SkuName $sourceDisk.Sku.Name
if ($usePremium) {$diskConfig.Sku=[Microsoft.Azure.Management.Compute.Models.DiskSku]::new('Premium_LRS')}
$sourceDiskCopy = New-AzDisk -Disk $diskConfig -ResourceGroupName $targetVmResourceGroup -DiskName "$($sourceVmName)-Disk-Copy"
# create the new target disk to hold the data of the source disk (but as a V2 type)
LogWriter ("Create an empty V2 disk as destion for the data")
$diskConfig = New-AzDiskConfig -Location $location -SkuName $sourceDisk.Sku.Name -OsType Windows -HyperVGeneration V2 -DiskSizeGB $sourceDisk.DiskSizeGB -CreateOption "Empty"
if ($enabledTrustedLaunch) {$diskConfig = Set-AzDiskSecurityProfile -Disk $diskConfig -SecurityType "TrustedLaunch"}
if ($usePremium) {$diskConfig.Sku=[Microsoft.Azure.Management.Compute.Models.DiskSku]::new('Premium_LRS')}
$targetDisk = New-AzDisk -Disk $diskConfig -ResourceGroupName $targetVmResourceGroup -DiskName "$($targetVmName)-Disk-Converted"
# attach the copy of the source disk (V1)
LogWriter ("Attaching the copied source disk to the target VM: Lun 6")
$targetVm = Add-AzVMDataDisk -VM $targetVm -Name $sourceDiskCopy.Name -CreateOption Attach -ManagedDiskId $sourceDiskCopy.Id -Lun 6 -Caching ReadWrite
Update-AzVM -VM $targetVm -ResourceGroupName $targetVmResourceGroup
# attach the later target disk (V2)
LogWriter ("Attaching the empty target disk to the target VM: Lun 7")
$targetVm = Add-AzVMDataDisk -VM $targetVm -Name $targetDisk.Name -CreateOption Attach -ManagedDiskId $targetDisk.Id -Lun 7 -Caching ReadWrite
Update-AzVM -VM $targetVm -ResourceGroupName $targetVmResourceGroup
# target VM is ready with all attached disks
# now we have to work with the partitions inside of the VM - Invoking this script with the parameter -mode StartInternalTask-1
LogWriter ("Run the first part of the converting process on the target VM - this can last hours")
Invoke-AzVMRunCommand -ResourceGroupName $targetVmResourceGroup -Name $targetVmName -CommandId "RunPowerShellScript" -ScriptPath $scriptPath -Parameter @{"-mode" = "StartInternalTask-1"}
# While the script is running internally, we have to wait for completion - looping every 2 minutes (the task can take seeral hours)
$completed = $false
$failed = $false
do {
Start-Sleep -Seconds 120
try {
$rv = Invoke-AzVMRunCommand -ResourceGroupName $targetVmResourceGroup -Name $targetVmName -CommandId "RunPowerShellScript" -ScriptPath $scriptPath -Parameter @{"-mode" = "CheckInternalTask"}
} catch {
LogWriter ("The remote operation failed: $_")
$failed = $true
$completed = $true
}
LogWriter ("Waiting for the remote task - long running operation")
if ($rv.Value[0].Message.Contains("###:COMPLETE:###")) {
$completed = $true
LogWriter ("The remote opertion completed")
}
} while (-not $completed)
# 2nd step inside the VM: use dism to write the image to the new disk
if (-not $failed) {
# now we have to work with the partitions inside of the VM - Invoking this script with the parameter -mode StartInternalTask-1
LogWriter ("Run the second part of the converting process on the target VM - this can last hours")
Invoke-AzVMRunCommand -ResourceGroupName $targetVmResourceGroup -Name $targetVmName -CommandId "RunPowerShellScript" -ScriptPath $scriptPath -Parameter @{"-mode" = "StartInternalTask-2"}
# While the script is running internally, we have to wait for completion - looping every 2 minutes (the task can take seeral hours)
$completed = $false
$failed = $false
do {
Start-Sleep -Seconds 120
try {
$rv = Invoke-AzVMRunCommand -ResourceGroupName $targetVmResourceGroup -Name $targetVmName -CommandId "RunPowerShellScript" -ScriptPath $scriptPath -Parameter @{"-mode" = "CheckInternalTask"}
} catch {
LogWriter ("The remote operation failed: $_")
$failed = $true
$completed = $true
}
LogWriter ("Waiting for the remote task - long running operation")
if ($rv.Value[0].Message.Contains("###:COMPLETE:###")) {
$completed = $true
LogWriter ("The remote opertion completed")
}
} while (-not $completed)
}
# last step inside the VM: create UEFI partition
if (-not $failed) {
LogWriter ("Run the last part of the converting process on the target VM")
Invoke-AzVMRunCommand -ResourceGroupName $targetVmResourceGroup -Name $targetVmName -CommandId "RunPowerShellScript" -ScriptPath $scriptPath -Parameter @{"-mode" = "StartInternalTask-3"}
}
if (-not $failed) {
# ready with the internal work
# stop the target VM
LogWriter ("Deallocate the target VM")
Stop-AzVM -ResourceGroupName $targetVmResourceGroup -Name $targetVmName -Force
# detach the copy of the source disk (V1)
LogWriter ("Detach the copied source disk")
$targetVm = Remove-AzVMDataDisk -VM $targetVm -Name $sourceDiskCopy.Name
Update-AzVM -VM $targetVm -ResourceGroupName $targetVmResourceGroup
# detach the later target disk (V2)
LogWriter ("Detacht the target disk")
$targetVm = Remove-AzVMDataDisk -VM $targetVm -Name $targetDisk.Name
Update-AzVM -VM $targetVm -ResourceGroupName $targetVmResourceGroup
# swap os-disk
LogWriter ("Swap OS disk to have the converted disk as the OS disk")
$targetVm = Get-AzVM -ResourceGroupName $targetVmResourceGroup -Name $targetVmName
$targetVm = Set-AzVMOSDisk -VM $targetVm -ManagedDiskId $targetDisk.Id -Name $targetDisk.Name
Update-AzVM -VM $targetVm -ResourceGroupName $targetVmResourceGroup
LogWriter ("Starting the converted target VM")
Start-AzVM -ResourceGroupName $targetVmResourceGroup -Name $targetVmName
# clean-up
LogWriter ("Cleaning up")
$sourceDiskSnap | Remove-AzSnapshot -Force
$sourceDiskCopy | Remove-AzDisk -Force
$targetDiskOrg | Remove-AzDisk -Force
LogWriter ("We are ready. The new V2 VM $targetVmName is ready and a copy of the original VM (which could be removed if everything works as expected")
} else {
# something went wrong - stopping the process to let the admin doing some debugging
LogWriter ("Error: Something went wrong - stopping the process to let the admin doing some debugging. Remember to clean-up manually (disks, snapshots, VM)")
}
}
#endregion CreateTheAzureResources
#region RunOnTheNewVmAndHandleDisks
if ($mode -eq "StartInternalTask-1") {
LogWriter("Preparing local VM")
try {Set-MPPreference -ScanAvgCPULoadFactor 5} catch{}
try {
$defragSvc = Get-Service -Name defragsvc -ErrorAction SilentlyContinue
Set-Service -Name defragsvc -StartupType Manual -ErrorAction SilentlyContinue
$supportedSize = (Get-PartitionSupportedSize -DriveLetter "c" -ErrorAction Stop)
if ((Get-Partition -DriveLetter "c").Size -lt $supportedSize.SizeMax) {
LogWriter("Resize C: partition to fill up the disk")
Resize-Partition -DriveLetter "c" -Size $supportedSize.SizeMax
}
Set-Service -Name defragsvc -StartupType $defragSvc.StartType -ErrorAction SilentlyContinue
}
catch {
LogWriter("Resize C: partition failed: $_")
}
LogWriter ("Preparing the disks and partitions")
$targetDiskNumber = (Get-Disk | Where-Object {$_.Path -like "*&000007#*"}).Number # lun=7
$sourceDiskNumber = (Get-Disk | Where-Object {$_.Path -like "*&000006#*"}).Number # lun=6
$diskPath=(Get-Disk -Number $targetDiskNumber).Path
Get-Disk -Number $targetDiskNumber | Initialize-Disk -PartitionStyle GPT -ErrorAction SilentlyContinue
# exclude defender
LogWriter ("Set defender excludes to speed up the imaging and apply process")
New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender\Exclusions" -Name "Paths" -Force -ErrorAction Ignore
New-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender\Exclusions\Paths" -Name "1" -Value "S:\" -force -ErrorAction Ignore
New-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows Defender\Exclusions\Paths" -Name "2" -Value "T:\" -force -ErrorAction Ignore
New-Item -Path "HKLM:\SOFTWARE\Microsoft\Windows Defender\Exclusions" -Name "Paths" -Force -ErrorAction Ignore
New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows Defender\Exclusions\Paths" -Name "S:\" -Value 0 -force -ErrorAction Ignore
New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows Defender\Exclusions\Paths" -Name "T:\" -Value 0 -force -ErrorAction Ignore
# delete all partion if needed
Remove-Partition -DiskNumber $targetDiskNumber -PartitionNumber 2,3,4,5,6,7,8,9 -Confirm:$false -ErrorAction SilentlyContinue
# Create UEFI partition
LogWriter ("Create UEFI partition")
$uefi=New-Partition -DiskNumber $targetDiskNumber -Size 100MB -GptType "{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}" -IsHidden
Format-Volume -FileSystem FAT32 -NewFileSystemLabel "SYSTEM" -Path $uefi.DiskPath
# Create recovery partition
LogWriter ("Create recovery partition")
$recovery=New-Partition -DiskNumber $targetDiskNumber -Size 450MB -GptType "{de94bba4-06d1-4d40-a16a-bfd50179d6ac}" -IsHidden
$null = @"
select disk $targetDiskNumber
select partition $($recovery.PartitionNumber)
gpt attributes=0x8000000000000001
exit
"@ | diskpart.exe
# Create windows partition
LogWriter ("Create Windows partition")
$windows=New-Partition -DiskNumber $targetDiskNumber -UseMaximumSize
# mount partitions
LogWriter ("Mount partitions")
Remove-PartitionAccessPath -DiskNumber $sourceDiskNumber -PartitionNumber 2 -AccessPath (Get-Partition -DiskNumber $sourceDiskNumber -PartitionNumber 2).AccessPaths[0] -ErrorAction SilentlyContinue
Add-PartitionAccessPath -DiskNumber $sourceDiskNumber -PartitionNumber 2 -AccessPath "S:\"
Add-PartitionAccessPath -DiskNumber $targetDiskNumber -PartitionNumber $uefi.PartitionNumber -AccessPath "U:\"
Add-PartitionAccessPath -DiskNumber $targetDiskNumber -PartitionNumber $recovery.PartitionNumber -AccessPath "R:\"
Add-PartitionAccessPath -DiskNumber $targetDiskNumber -PartitionNumber $windows.PartitionNumber -AccessPath "T:\"
# Format drives
LogWriter ("Format partitions")
Format-Volume -FileSystem NTFS -NewFileSystemLabel "Windows Sytem Drive" -DriveLetter T:\ -ErrorAction SilentlyContinue
Format-Volume -FileSystem NTFS -NewFileSystemLabel "SYSTEM" -DriveLetter R:\ -ErrorAction SilentlyContinue
Format-Volume -FileSystem FAT32 -NewFileSystemLabel "SYSTEM" -DriveLetter U:\ -ErrorAction SilentlyContinue
# Capture the source windows
# Dism /Capture-Image /ImageFile:`"C:\Capture.wim`" /CaptureDir:S:\ /Name:Captured
# Start dism but don't wait
LogWriter ("Starting DISM to create an image")
Start-Process -FilePath Dism.exe -ArgumentList "/Capture-Image /ImageFile:`"C:\Capture.wim`" /CaptureDir:S:\ /Name:Captured"
Start-Sleep -Seconds 30
}
if ($mode -eq "StartInternalTask-2") {
# Rollout the image to the target disk
# Dism /Apply-Image /ImageFile:"C:\Capture.wim" /ApplyDir:T:\ /Index:1 /CheckIntegrity
LogWriter ("Starting DISM and apply image to the new Windows partition on the V2 disk")
Start-Process -FilePath Dism.exe -ArgumentList "/Apply-Image /ImageFile:`"C:\Capture.wim`" /ApplyDir:T:\ /Index:1 /CheckIntegrity"
Start-Sleep -Seconds 30
}
if ($mode -eq "StartInternalTask-3") {
# Create UEFI
LogWriter ("Writing UEFI data to UEFI partition")
Start-Process -Wait -FilePath "T:\Windows\System32\bcdboot.exe" -WorkingDirectory "T:\Windows\System32" -ArgumentList "T:\Windows /s U: /f UEFI"
}
#endregion RunOnTheNewVmAndHandleDisks
#region RunOnTheNewVmAndCheckState
if ($mode -eq "CheckInternalTask") {
if (Get-Process -Name DISM -ErrorAction SilentlyContinue) {
# DISM is still running
LogWriter ("Starting DISM")
} else {
# DISM completed to the rest and terminate
write-host "###:COMPLETE:###"
}
}
#endregion RunOnTheNewVmAndCheckState
#endregion MainApp
@emanuel-kautz
Copy link

My script runs for 3-4 hours, and every 2 minutes, I receive the following message:
'2024-05-03T13:37:41.6703738Z Waiting for the remote task - long running operation.'

I attempted to run it a second time, but I encountered the same issue.

@ankurvvvv
Copy link

I think this needs to be changed? The #region InternalMethods script path

if (-not $PSCommandPath) {$scriptPath="C:\1Drive\OneDrive - sepago GmbH\Desktop\Convert-VmV1toV2.ps1"}

@ronaldbok
Copy link

Hello,

i got the following error message:

New-AzVM : Cannot validate argument on parameter 'LicenseType'. The argument is null or empty. Provide an argument
that is not null or empty, and then try the command again.
At C:\temp\Scripts\Create-V2-From-V1-VM.ps1:96 char:107

  • ... mResourceGroup -Location $location -LicenseType $sourceVm.LicenseType
  •                                                 ~~~~~~~~~~~~~~~~~~~~~
    
    • CategoryInfo : InvalidData: (:) [New-AzVM], ParentContainsErrorRecordException
    • FullyQualifiedErrorId : ParameterArgumentValidationError,Microsoft.Azure.Commands.Compute.NewAzureVMCommand

Can you help me ?

Greetings Ronald

@MarcelMeurer
Copy link
Author

MarcelMeurer commented Sep 3, 2024 via email

@ronaldbok
Copy link

Thanks that worked. But next error

New-AzVM : The zone(s) 'Server' for resource 'Microsoft.Compute/virtualMachines/vmprtg001-v2' is not supported. The
supported zones for location 'westeurope' are '1,2,3'
ErrorCode: InvalidAvailabilityZone
ErrorMessage: The zone(s) 'Server' for resource 'Microsoft.Compute/virtualMachines/vmprtg001-v2' is not supported. The
supported zones for location 'westeurope' are '1,2,3'
ErrorTarget:
StatusCode: 400
ReasonPhrase: Bad Request
OperationID : 1e6b9f67-2f4f-4d89-a4dd-99bee270e7eb
At C:\temp\Scripts\Create-V2-From-V1-VM.ps1:96 char:9

  •     New-AzVM -VM $vmConfig -ResourceGroupName $targetVmResourceGr ...
    
  •     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
    • CategoryInfo : CloseError: (:) [New-AzVM], ComputeCloudException
    • FullyQualifiedErrorId : Microsoft.Azure.Commands.Compute.NewAzureVMCommand

@MarcelMeurer
Copy link
Author

Hi @ronaldbok: should we check together via teams? Maybe you can sent me you mail address via linked in or by email.

@zzubedi
Copy link

zzubedi commented Sep 20, 2024

Hi Marcel, thank you for your wonderful script. I do have a question. How do i reach you via MS teams?

@MarcelMeurer
Copy link
Author

Hi Marcel, thank you for your wonderful script. I do have a question. How do i reach you via MS teams?

Hi @zzubedi : You can reach me at marcel.meurer@itprocloud.com

@aventre-bag
Copy link

aventre-bag commented Dec 6, 2024

@MarcelMeurer Would be great if you can implement that the VM Tags from the V1 VM are also set on the V2 VM 😃

@jmarcum-bbins
Copy link

After completing the script my Gen2 machine will not boot. The error message says no OS found on SCSI 0,0
EDKWF12DCj

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment