-
-
Save MarcelMeurer/aab2ef8f36d7c634beebea771f54bf30 to your computer and use it in GitHub Desktop.
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 | |
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"}
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
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
Hi @ronaldbok: should we check together via teams? Maybe you can sent me you mail address via linked in or by email.
Hi Marcel, thank you for your wonderful script. I do have a question. How do i reach you via MS teams?
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
@MarcelMeurer Would be great if you can implement that the VM Tags from the V1 VM are also set on the V2 VM 😃
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.