Cold-migrate Hyper-V VMs onto a Linux KVM/libvirt host — one small script per VM — preserving each guest's MAC so it keeps its DHCP-reserved IP and nothing downstream has to change.
I used this to retire two Windows Hyper-V servers onto a single Ubuntu box (~20
single-service VMs). It is deliberately small and readable: no agent, no control
plane, just ssh, qemu-img, guestfish and virt-install. Read it before you
run it.
Under Hyper-V the guest's NIC driver (hv_netvsc) presents the interface as
eth0. Move the disk to KVM and the virtio-net NIC comes up as enp1s0
instead. If the guest's netplan matches on interface name, it now matches
nothing — the VM boots with no network and you're on the console wondering why.
The fix, applied offline to the qcow2 before first boot: match the NIC by
MAC address, force set-name: eth0, and disable cloud-init's networking so it
can't clobber the override on boot. That's the heart of migrate-vm.sh.
network:
version: 2
ethernets:
primary:
match:
macaddress: "00:15:5d:..." # the MAC, carried over from Hyper-V
set-name: eth0
dhcp4: true- Ask the Hyper-V host over SSH+PowerShell where the VHDX lives, then
Stop-VM. qemu-img convert -f vhdx -O qcow2into the libvirt pool on the KVM host.guestfishrewrites the guest's netplan offline (the trap above) and disables cloud-init networking.virt-install --importonto a bridge with the original MAC, disk and NIC on virtio, andvirsh autostart.- Ping the expected IP until it answers.
Preserving the MAC means a MAC-keyed DHCP reservation hands the VM its old address, so DNS, firewall rules and every service that talked to it keep working untouched.
KVM host: libvirt, qemu-utils, libguestfs-tools (guestfish), virtinst,
a bridge (e.g. br0), and the Hyper-V VHDX stores mounted somewhere readable
(CIFS is fine).
Hyper-V hosts: OpenSSH server + PowerShell, your public key in the
administrator's authorized_keys.
Runner (your workstation): SSH access to both, and one key (SSH_KEY below).
Edit the config block at the top of migrate-vm.sh (hosts, user, bridge, SSH key,
SOURCES map), then:
# one VM
KVM_HOST=kvmhost.lan KVM_USER=ubuntu \
./migrate-vm.sh MyServer hosta 00:15:5d:01:02:03 4096 4 10.0.0.42
# a whole fleet from an inventory file
cp examples/vms.tsv vms.tsv # fill in from lib/hyperv-inventory.ps1 output
./examples/run-migration.sh vms.tsvBuild the inventory (MAC, MemGB, ProcessorCount, VHDX paths) by running
lib/hyperv-inventory.ps1 on each Hyper-V host.
If sudo on the KVM host needs a password, export ZPW=...; otherwise passwordless
(NOPASSWD) sudo is assumed.
vm-backup-to-nas.sh is the companion for once the VMs live on KVM — a nightly
live backup (snapshot + blockcommit, no downtime) that writes zstd-compressed
qcow2 images to a CIFS share and prunes by age. Point a systemd timer at it.
- Cold migration. Each VM is down while its disk converts (minutes for a small disk, longer for large ones). This is not live migration — it's the simple, reliable path for a one-shot move you do once.
- Ubuntu/netplan guests. The network fix targets netplan. Other distros need
the equivalent (udev
.link,NetworkManager,/etc/network/interfaces). - Crash-consistent backups. Install
qemu-guest-agentin the guests for filesystem-consistent (fsfreeze) snapshots — recommended for databases. - Gen-2 / UEFI guests map to the
q35machine type used here; adjust--os-variant(osinfo-query os) to match your guests.
MIT — see LICENSE.