Leaving VMware After Broadcom: A Checked Migration Guide
Short answer (checked 2026-11-03): Since buying VMware, Broadcom has stopped selling perpetual licences, folded the product list into two subscription bundles, closed its old cloud-partner programme in Europe and, this autumn, pulled the public download of VDDK, the disk library most migration tools use to read VMware disks. Today The Register reports an open-source, read-only replacement for that library. Not every way out needs VDDK, though: an OVA export plus a disk conversion uses no VMware code at all. Below: the dated changes, realistic targets for an EU team, the move step by step, and a converter we ran on a real VMware-format export.
Source. "Architect creates open-source replacement for VM migration tool VMware has hidden" (Simon Sharwood, The Register, 2026-11-03). Other facts are linked where they appear. Your own contract can differ from the public terms, so confirm licence details with your reseller.
What Broadcom changed, with dates
| Date | Change | Source |
|---|---|---|
| 2023-12-11 | End of sale of perpetual licences and support renewals; offer cut to VMware Cloud Foundation (VCF) and vSphere Foundation (VVF) subscriptions | Broadcom news, Krish Prasad |
| 2025-03-28 | Distributor Arrow tells partners the minimum order rises from 16 to 72 cores per product line from 2025-04-10 | The Register |
| 2026-01-26 | Cloud-provider (VCSP) contracts in the EEA not renewed; open deals to close by 2026-03-31; invite-only programme after that | The Register, 2026-01-31 |
| Aug-Sep 2026 | VDDK removed from public download: "The VDDK is not a customer entitlement", access via select TAP partners | TechTarget, 2026-09-09 |
| 2026-11-03 | OpenVDDK by Andrija Panić: Apache-licensed, read-only drop-in for libvixDiskLib.so.8.0.3 that "contains no VMware code" | The Register, 2026-11-03 |
Two caveats. The 72-core minimum was reported by a distributor, not on a Broadcom page we could find: Broadcom's core-counting article (KB 313548, checked 2026-11-03) says "You must license a minimum of 16 physical cores for each CPU", and some resellers say the 72-core order minimum was dropped again. Ask for the core count on your quote in writing. And the EU side is still open: the cloud trade group CISPE asked the EU General Court in July 2025 to annul the Commission's approval of the merger (case T-503/25); we found no ruling yet.
Why VDDK matters, and which paths avoid it
VDDK's library, libvixDiskLib, lets a tool read the disks of VMs that still live in vSphere. The Register lists virt-v2v, nbdkit, the Forklift / MTV importers, Apache CloudStack and in-house scripts among the tools that load it. Microsoft's VM Conversion extension for Windows Admin Center (vCenter to Hyper-V, with warm sync over Change Block Tracking) is still marked preview in its documentation (updated 2026-02-20), and launch coverage lists VDDK among its prerequisites. OpenVDDK is the new option for all of these, with its author's own warning that tester reports are "not independently audited customer endorsements".
Paths with no VDDK at all, per the virt-v2v VMware input docs (checked 2026-11-03): an OVA export (-i ova), the VM's files on NFS or over SSH (-i vmx, -it ssh), NFC, and the plain vCenter HTTPS path, which the docs call "the slowest method". Proxmox VE's ESXi import wizard, announced on 2024-03-27, reads disks through the ESXi API with its own Rust user-space filesystem and was tested against ESXi 6.5 to 8.0 (Proxmox forum).
Where to move: five realistic targets
| Target | Fits when | Watch out for |
|---|---|---|
| Proxmox VE (KVM) on your hardware | 3+ hosts, a team that wants a GUI and clustering | You own upgrades, storage (ZFS, Ceph) and support |
| OpenStack | Many tenants, everything through an API | Heavy to run; pays off only at scale |
| Hyper-V (Windows Server) | Windows-heavy estate, Datacenter licences already paid | Microsoft's tool needs Hyper-V drivers in Linux guests first |
| Cloud servers (KVM) | A few to a few dozen VMs, no wish to run a hypervisor | One import per VM, new IP addresses |
| Dedicated servers + your own hypervisor | Steady load, you want the whole machine | You run and patch the hypervisor yourself |
Move a VM in five steps
- Inventory and clean up. Note firmware (BIOS or EFI), disk controller, NIC type, static IPs and any licence tied to a MAC address. Delete or consolidate snapshots so you export one current disk.
- Prepare the guest while it still runs on VMware. Windows: install the virtio drivers (virtio-win) now; a Windows guest moved to a virtio disk without them fails to boot. Linux: virtio drivers are in the kernel, but a host-only initramfs may leave them out; check with
lsinitramfsorlsinitrdand rebuild it if needed. - Export or connect. Shut the VM down and export an OVF / OVA (vSphere Client "Export OVF Template", or ovftool), or let virt-v2v or the Proxmox wizard pull it.
- Convert. Disk only with qemu-img, the whole guest with virt-v2v (it also adjusts drivers), or straight into Proxmox from the OVF.
- Boot on the console, then clean up. Check the NIC name and IP, remove VMware Tools or open-vm-tools, install qemu-guest-agent.
qemu-img convert -p -f vmdk -O qcow2 disk1.vmdk disk1.qcow2
virt-v2v -i ova VM.ova -o local -os /var/tmp -of qcow2
qm importovf 120 VM.ovf local-lvm
These are the documented forms from each tool's manual; we did not run these three here. The conversion we did run is below.
Tested: what a conversion actually does (ova2raw.js)
To show the moving parts, we wrote the conversion in about 100 lines of Bun with no dependencies. It opens an OVA (a plain tar), checks the SHA-256 manifest, prints the hardware the OVF asks for, unpacks the streamOptimized VMDK grain by grain into a sparse raw image and checks the GPT. We fed it Ubuntu's official 24.04 OVA, a real VMware-format export: noble-server-cloudimg-amd64.ova, build of 2026-10-17, whose SHA-256 matched Ubuntu's SHA256SUMS. Save as ova2raw.js:
// ova2raw.js - check a VMware OVA export and turn its disk into a raw image for KVM
// usage: bun ova2raw.js <export.ova> <out.raw>
import { openSync, readSync, writeSync, ftruncateSync, closeSync, fstatSync } from 'node:fs'
import { inflateSync } from 'node:zlib'
import { createHash } from 'node:crypto'
const [ova, out] = process.argv.slice(2)
const fd = openSync(ova, 'r')
const read = (pos, len) => {
const b = Buffer.alloc(len)
readSync(fd, b, 0, len, pos)
return b
}
// 1. an OVA is a plain tar: list its members
const files = {}
for (let o = 0; o + 512 <= fstatSync(fd).size; ) {
const h = read(o, 512)
if (h.every(x => x === 0)) break
const name = h.toString('utf8', 0, 100).replace(/\0.*$/s, '')
const size = parseInt(h.toString('ascii', 124, 136).replace(/\0.*$/s, '').trim(), 8)
files[name] = { pos: o + 512, size }
o += 512 + Math.ceil(size / 512) * 512
}
const find = ext => Object.keys(files).find(n => n.endsWith(ext))
const text = n => read(files[n].pos, files[n].size).toString('utf8')
console.log('OVA members:', Object.entries(files).map(([n, f]) => `${n} (${f.size} B)`).join(', '))
// 2. the .mf manifest: every member's SHA-256 must match
const mf = find('.mf')
for (const line of mf ? text(mf).split('\n') : []) {
const m = line.match(/^SHA(1|256)\((.+)\)\s*=\s*([0-9a-f]+)/i)
if (!m || !files[m[2]]) continue
const h = createHash('sha' + m[1])
const f = files[m[2]]
for (let p = 0; p < f.size; p += 1 << 24) h.update(read(f.pos + p, Math.min(1 << 24, f.size - p)))
console.log(` manifest ${m[2]}: ${h.digest('hex') === m[3].toLowerCase() ? 'OK' : 'MISMATCH'}`)
}
// 3. the .ovf descriptor: what the VM expects from its hardware
const ovf = text(find('.ovf'))
const items = [...ovf.matchAll(/<(?:ovf:)?Item\b[\s\S]*?<\/(?:ovf:)?Item>/g)].map(x => x[0])
const tag = (s, t) => s.match(new RegExp(`<(?:rasd:)?${t}>([^<]*)<`))?.[1]
const kind = { 3: 'CPU', 4: 'RAM', 5: 'IDE controller', 6: 'SCSI controller', 10: 'NIC', 17: 'disk', 20: 'SATA controller' }
console.log('OVF:', ovf.match(/vmw:osType="([^"]+)"/)?.[1] ?? '?', '| firmware',
ovf.match(/key="firmware" ovf:value="([^"]+)"/)?.[1] ?? 'bios (default)')
for (const it of items) {
const k = kind[tag(it, 'ResourceType')]
if (k) console.log(` ${k.padEnd(16)} ${tag(it, 'ElementName') ?? ''} ${tag(it, 'ResourceSubType') ?? ''} ${tag(it, 'VirtualQuantity') ?? ''}`.trimEnd())
}
// 4. the streamOptimized VMDK: a header, then compressed grains and markers
const vmdk = files[find('.vmdk')]
const hd = read(vmdk.pos, 512)
if (hd.readUInt32LE(0) !== 0x564d444b) throw new Error('not a sparse VMDK (no KDMV magic)')
const capacity = Number(hd.readBigUInt64LE(12)) * 512
const desc = read(vmdk.pos + Number(hd.readBigUInt64LE(28)) * 512, Number(hd.readBigUInt64LE(36)) * 512)
console.log(`VMDK ${desc.toString().match(/createType="([^"]+)"/)?.[1]}, virtual size ${capacity} B, grain ${Number(hd.readBigUInt64LE(20)) * 512} B`)
const ofd = openSync(out, 'w')
ftruncateSync(ofd, capacity) // sparse: unwritten grains stay holes = zeros
let pos = Number(hd.readBigUInt64LE(64)) * 512
let grains = 0
let written = 0
while (pos < vmdk.size) {
const m = read(vmdk.pos + pos, 512)
const val = Number(m.readBigUInt64LE(0))
const size = m.readUInt32LE(8)
if (size) { // compressed grain: lba, size, zlib data
const raw = inflateSync(read(vmdk.pos + pos + 12, size))
writeSync(ofd, raw, 0, raw.length, val * 512)
grains++
written += raw.length
pos += Math.ceil((12 + size) / 512) * 512
} else if (m.readUInt32LE(12) === 0) break // end-of-stream marker
else pos += 512 + val * 512 // grain table / directory / footer: metadata only
}
closeSync(ofd)
console.log(`Wrote ${out}: ${grains} grains, ${written} B of data in ${capacity} B`)
// 5. check the result: a valid GPT, and the file systems where the partitions say they are
const rfd = openSync(out, 'r')
const at = (p, l) => { const b = Buffer.alloc(l); readSync(rfd, b, 0, l, p); return b }
const gpt = at(512, 92)
const crc = Buffer.from(gpt)
crc.writeUInt32LE(0, 16)
console.log(`GPT "${gpt.toString('ascii', 0, 8)}", header CRC ${(Bun.hash.crc32(crc) >>> 0) === gpt.readUInt32LE(16) ? 'OK' : 'BAD'}`)
const n = gpt.readUInt32LE(80)
const es = gpt.readUInt32LE(84)
const ents = at(Number(gpt.readBigUInt64LE(72)) * 512, n * es)
console.log(` partition array CRC ${(Bun.hash.crc32(ents) >>> 0) === gpt.readUInt32LE(88) ? 'OK' : 'BAD'}`)
for (let i = 0; i < n; i++) {
const e = ents.subarray(i * es, (i + 1) * es)
if (e.subarray(0, 16).every(x => x === 0)) continue
const first = Number(e.readBigUInt64LE(32))
const last = Number(e.readBigUInt64LE(40))
const sb = at(first * 512 + 1024, 136)
const fs = sb.readUInt16LE(56) === 0xef53 ? `ext4 label "${sb.toString('ascii', 120, 136).replace(/\0.*$/s, '')}"`
: at(first * 512, 512).toString('ascii', 82, 87) === 'FAT32' ? 'FAT32 (EFI system partition)'
: e.toString('ascii', 0, 16) === 'Hah!IdontNeedEFI' ? 'BIOS boot (GRUB for legacy BIOS)' : 'unknown'
console.log(` p${i + 1} ${String((last - first + 1) * 512 / 2 ** 20 | 0).padStart(5)} MiB ${fs}`)
}
bun ova2raw.js noble-server-cloudimg-amd64.ova noble.raw
Output, run on a DCXV Cloud server with Bun 1.4.4 on 2026-11-03:
OVA members: ubuntu-noble-24.04-cloudimg.ovf (11186 B), ubuntu-noble-24.04-cloudimg.mf (213 B), ubuntu-noble-24.04-cloudimg.vmdk (590749696 B)
manifest ubuntu-noble-24.04-cloudimg.vmdk: OK
manifest ubuntu-noble-24.04-cloudimg.ovf: OK
OVF: ubuntu64Guest | firmware bios (default)
CPU 2 virtual CPU(s) 2
RAM 1024MB of memory 1024
SCSI controller SCSI Controller 0 VirtualSCSI
IDE controller VirtualIDEController 1
IDE controller VirtualIDEController 0
disk Hard Disk 1
NIC Ethernet 1 VmxNet3
VMDK streamOptimized, virtual size 10737418240 B, grain 65536 B
Wrote noble.raw: 28663 grains, 1878458368 B of data in 10737418240 B
GPT "EFI PART", header CRC OK
partition array CRC OK
p1 2558 MiB ext4 label "cloudimg-rootfs"
p14 4 MiB BIOS boot (GRUB for legacy BIOS)
p15 106 MiB FAT32 (EFI system partition)
p16 913 MiB ext4 label "BOOT"
Three runs took 14.6 to 16.2 s wall clock on that server. The raw file is 10 GiB in size and 1.8 GB on disk.
What the output tells you before cutover
- VmxNet3 and VirtualSCSI are VMware devices. On KVM they become virtio-net and virtio-blk or virtio-scsi. The guest needs drivers for both before the first boot, and the network interface usually gets a new name, so a static IP bound to the old name will not come up. Use the console for the first boot.
- Firmware must match. This OVF sets nothing, so it is BIOS; the disk carries both a BIOS boot and an EFI system partition, so it boots either way. A VM exported with
firmware=efineeds UEFI (OVMF) on the target, otherwise it finds no boot loader. - Thin stays thin only if you keep it so. 10 GiB virtual, 1.8 GB of data. Copy with sparse-aware tools (
cp --sparse=always,rsync -S) or use qcow2, or you move and store zeros. - The manifest is your transfer check. Verify the SHA-256 after every copy between sites: a cut-off upload converts without complaint into a disk that fails later.
Plan the downtime
A cold move costs export + transfer + convert + first boot. Time each on one test VM, then multiply. Transfer arithmetic (ours, before protocol overhead): 1.8 GB at 1 Gbit/s is about 15 s, 1 TB about 2.2 hours. Warm tools copy while the VM runs and send only changed blocks at the end; The Register's testers report cold and warm runs with Forklift and MTV. For databases and busy apps, replication plus a DNS switch beats any disk copy: lower the TTL first (see DNS propagation and TTL) and follow the checklists in how to migrate to a cloud server.
When DCXV cloud or dedicated servers fit
- A handful of VMs, no hypervisor team: our cloud servers run on KVM with virtio disks. In the panel, Reinstall -> "Custom image (URL)" takes a qcow2, raw or vmdk file over https; an image larger than the plan disk raises the disk and is billed as a disk upgrade, and network settings are applied only if the image has cloud-init. Plans start at EUR 15.00/mo (2 vCPU, 4 GB RAM, 40 GB; checked 2026-11-03, excl. VAT).
- Dozens of VMs or your own Proxmox cluster: dedicated servers from EUR 210/mo, with the hypervisor you choose.
- Windows guests: we resell Windows Server licences; ask before the move so the licence is ready on day one.
Both run in our Tier III data centres in Prague and Covilha.
Plan the move with us
Run ova2raw.js on one exported VM and send the hardware lines of its output to sales@dcxv.com, or start a cloud server and test the import yourself.
