Automating Hyper‑V VM Adoption into Azure Local
Quick view on how automating this thing would look for more than a few VMs.
First, let’s split the scripts into 4 pieces - they can be gobbled together if needed, but that makes walkthroughs harder and debugging an all‑in‑one script usually feels like fighting a zergling with a USB cable.
If you’d like, you could also keep things split, mash them together into a monster script, add a fancy VM selection process, or even invoke CLI straight from PowerShell and run it from a single spot.
Basically: use your favorite Clippy 2.0 / HAL 9000 mashup to automate however you like. I’m more on the “keep it modular” side - easier to troubleshoot, tweak, or figure out what the heck I was trying to do three months later.
The 4 pieces:
- Export Hyper‑V VM configs
- Create placeholders
- Swap the disks
- Create the Azure Local VMs (and turn them on if you’re feeling bold)
Note: 1 and 3 are ran on the Hyper-V host. 2 and 4 use Azure CLI (Cloud Shell is easiest).
Below, I’m adopting/moving over 3 VMs - S01/S02/S03. They have a simple setup, with a single OS and single NIC.
1. Export Hyper‑V VM configs
Collect VM info and export as JSON, including NIC config - pretty self‑explanatory.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# PowerShell
$vms = @()
Get-VM | ForEach-Object {
$vm = $_
$cpu = Get-VMProcessor -VMName $vm.Name
$mem = Get-VMMemory -VMName $vm.Name
$hdds = Get-VMHardDiskDrive -VMName $vm.Name | Sort-Object ControllerLocation
$nics = Get-VMNetworkAdapter -VMName $vm.Name
$osDisk = $null
$dataDisks = @()
$nicConfigs = @()
if ($hdds.Count -gt 0) {
$osVhd = Get-VHD -Path $hdds[0].Path
$osDiskBase = [System.IO.Path]::GetFileNameWithoutExtension($hdds[0].Path)
$osDiskExt = [System.IO.Path]::GetExtension($hdds[0].Path)
$osDisk = @{
name = "${osDiskBase}${osDiskExt}"
sizeGB = [int][math]::Ceiling($osVhd.Size / 1GB)
path = $hdds[0].Path
}
foreach ($d in $hdds[1..($hdds.Count-1)]) {
$vhd = Get-VHD -Path $d.Path
$diskBase = [System.IO.Path]::GetFileNameWithoutExtension($d.Path)
$diskExt = [System.IO.Path]::GetExtension($d.Path)
$dataDisks += @{
name = "${diskBase}${diskExt}"
sizeGB = [int][math]::Ceiling($vhd.Size / 1GB)
path = $d.Path
}
}
}
foreach ($nic in $nics) {
$nicConfigs += @{
name = $nic.Name
macAddress = $nic.MacAddress
switchName = $nic.SwitchName
ipAddresses = $nic.IPAddresses
}
}
$vms += @{
VMName = $vm.Name
cpuCount = $cpu.Count
memStartup = [int][math]::Ceiling($mem.Startup / 1GB)
memMin = [int][math]::Ceiling($mem.Minimum / 1GB)
memMax = [int][math]::Ceiling($mem.Maximum / 1GB)
memDynamic = $mem.DynamicMemoryEnabled
osDisk = $osDisk
dataDisks = $dataDisks
nics = $nicConfigs
}
}
$vms | ConvertTo-Json -Depth 4 | Out-File .\vm_inventory.json
2. Create placeholders
Here’s where we pre‑stage resources in Azure Local. Think of it like setting the dinner table before serving the food - except the “food” is a 120GB VHDX you’ll be dragging across a cluster in the next step.
Tip: NIC names aren’t really usually enforced on the Hyper‑V side, so this is a good time to normalize them. Same for disk names - pick a pattern you’ll reuse.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# Azure CLI (run in Cloud Shell or anywhere az is authenticated)
# Note: reading from vm_inventory.json created in step 1
$inventory = Get-Content .\vm_inventory.json | ConvertFrom-Json
$resourceGroup = "RGname"
$customLocationID = "/subscriptions/<subID>/resourceGroups/<RGname>/providers/Microsoft.ExtendedLocation/customLocations/<s-cluster-customlocation>"
$location = "AzLocalRegion"
$storagePathId = "/subscriptions/<subID>/resourceGroups/<RGname>/providers/Microsoft.AzureStackHCI/storageContainers/<yourStoragePath>"
$diskFileFormat = "vhdx"
$subnetId = "lnetname"
foreach ($vm in $inventory) {
$vmName = $vm.VMName
$osDiskName = ($vm.osDisk.name -replace '\.vhdx$', '') +"-"+ $vm.VMName
$dataDisks = $vm.dataDisks
$nics = $vm.nics
# Create NICs
foreach ($nic in $nics) {
$nicName = "${vmName}-${nic.name}"
az stack-hci-vm network nic create `
--resource-group $resourceGroup `
--custom-location $customLocationID `
--location $location `
--name "$vmName-NIC" `
--subnet-id $subnetId
}
# Create OS Disk
az stack-hci-vm disk create `
--resource-group $resourceGroup `
--custom-location $customLocationID `
--location $location `
--size-gb $vm.osDisk.sizeGB `
--name $osDiskName `
--storage-path-id $storagePathId `
--disk-file-format $diskFileFormat
# Create Data Disks
foreach ($d in $dataDisks) {
az stack-hci-vm disk create `
--resource-group $resourceGroup `
--custom-location $customLocationID `
--location $location `
--size-gb $d.sizeGB `
--name "${d.name}-toBeSwapped" `
--storage-path-id $storagePathId `
--disk-file-format $diskFileFormat `
}
Write-Host "AzLocal resources created for $($vm.VMName)"
}
3. Swap the disks
Time to shut down the Hyper‑V VMs and move the bits where Azure Local expects them … or will expect them … until they change them … maybe.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# PowerShell
# Shutdown all VMs first that we are targetting - which are listed in the inventory
Stop-VM -Name (Get-Content .\vm_inventory.json | ConvertFrom-Json).VMName -Force
# Define your subscription and resource group which will be used to create he Disk GUIDoName
$subID = "<subID>"
$rgName = "<RGName>"
# New disk destination folder -- check storage path for a GUID looking folder
$newDiskRoot = "C:\ClusterStorage\<storagePathDirectory>\<diskSpecificGUID>"
$inventory = Get-Content .\vm_inventory.json | ConvertFrom-Json
foreach ($vm in $inventory) {
$vmName = $vm.VMName
# OS Disk
$osDisk = $vm.osDisk
$osDiskSrc = $osDisk.path
$osDiskBase = ($osDisk.name -replace '\.vhdx$', '')
$osDiskGUID = "$subID-$rgName"
$osDiskDest = Join-Path $newDiskRoot "$osDiskBase-$vmName-$osDiskGUID.vhdx"
Move-Item -Path $osDiskSrc -Destination $osDiskDest -Force
# Data Disks
foreach ($d in $vm.dataDisks) {
$dataDiskSrc = $d.path
$dataDiskBase = ($d.name -replace '\.vhdx$', '')
$dataDiskGUID = "$subID-$rgName"
$dataDiskDest = Join-Path $newDiskRoot "$dataDiskBase-$vmName-$dataDiskGUID.vhdx"
Move-Item -Path $dataDiskSrc -Destination $dataDiskDest -Force
}
}
Write-Host "All disks moved and renamed for AzLocal adoption."
4. Create AzLocal VMs
Now for the fun part: adopting it as an AzLocal VM.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# PowerShell
$inventory = Get-Content .\vm_inventory.json | ConvertFrom-Json
$subID = "<subID>"
$rgName = "<RGName>"
$resourceGroup = "<RGName>"
$customLocationID = "/subscriptions/<subID>/resourcegroups/<RGName>/providers/microsoft.extendedlocation/customlocations/<AzLocalRegion>"
$location = "<azLocalLocation>"
$storagePathId = "/subscriptions/<subID>/resourceGroups/<RGName>/providers/microsoft.azurestackhci/storagecontainers/<storagePathDirectory>"
$osType = "windows" # or "linux" as appropriate
foreach ($vm in $inventory) {
$vmName = $vm.VMName
# OS Disk name (matches what you used for Move-Item)
$osDiskName = ($vm.osDisk.name -replace '\.vhdx$', '') + "-$vmName"
$osDiskNameid = "/subscriptions/$subID/resourceGroups/$rgName/providers/Microsoft.AzureStackHCI/virtualHardDisks/$osDiskName"
#to confirm $osdisknameID - go to the AzLocal instance, find Disks, and click the JSON option to show the ID
# NICs (assume first NIC for simplicity, adjust as needed)
$nicNames = @()
foreach ($nic in $vm.nics) {
$nicNames += "${vmName}-NIC"
}
$nicRefs = $nicNames -join ','
$nic1id = "/subscriptions/$subID/resourceGroups/$rgName/providers/Microsoft.AzureStackHCI/networkInterfaces/$nicRefs"
# Data disks (you'll need to add the datadisks after creating the VM - use 'az stack-hci-vm disk attach')
# Create VM
az stack-hci-vm create `
--resource-group $resourceGroup `
--custom-location $customLocationID `
--location $location `
--storage-path-id $storagePathId `
--name $vmName `
--os-disk-name $osDiskNameid `
--os-type $osType `
--nics $nic1id
Write-Host "AzLocal VM created for $vmName"
}