2020-04-02

Making VHD Files in QEMU That Work with Hyper-V

For some reason, Mount-VHD doesn't like QEMU-generated .VHD files.

And I didn't know why.

If you download QEMU you will find that it comes with a virtual disk-creating application "qemu-img", which supports multiple different disk formats, one of which is Microsoft's own Virtual Hard Disk (VHD) format.

Aside: The VHD format is pretty elegant (read: stupid simple) and was developed by Connectix, eventually becoming the virtual disk format for MS Virtual PC. The file format spec is free to read and if you ever have some free time, check it out.

The VHD format is simple. I mean, really simple. Stupidly simple. The "fixed" type format is just "How big do you want your disk?" and then you get that many zeroes, plus a 512-byte header that describes some basic metadata.

So there's lots of ways to make a VHD. Probably the easiest is to run New-VHD:

$vhd_properties = @{
  'Path'      = 'my_disk.vhd';
  'SizeBytes' = 10 * [Math]::Pow(2,20); # 10 MiB
  'Fixed'     = $True;
}
New-VHD @vhd_properties

Easy peasy. You may not, however, want to use PowerShell for this. You may not even want to use Windows, and that's fine, too.

But darned if it wouldn't be convenient to be able to have a .VHD file that you can swap back and forth through multiple, compatible virtualization platforms. Making this happen, however, turns out to be harder than you'd think. Making a VHD with QEMu is easy:

"C:\Program Files\qemu\qemu-img.exe" create -f vpc -o subformat=fixed,force_size my_disk.vhd 10M

But Hyper-V doesn't like that:

PS C:\> Mount-VHD .\my_disk.vhd
Mount-VHD : Failed to mount the virtual disk.
The system failed to mount 'c:\my_disk.vhd'.
Failed to mount the virtual disk.
The system failed to mount 'c:\my_disk.vhd': The requested operation could not be completed due to a
virtual disk system limitation.  Virtual hard disk files must be uncompressed and unencrypted and must not be sparse.
(0xC03A001A).
At line:1 char:1
+ Mount-VHD .\my_disk.vhd
+ ~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Mount-VHD], VirtualizationException
    + FullyQualifiedErrorId : OperationFailed,Microsoft.Vhd.PowerShell.Cmdlets.MountVhd

I looked into error 0xC03A001A and it doesn't have a lot of documentation online. Besides, the .VHD file is not compressed and not encrypted, and it is explicitly fixed-size, not sparse... isn't it?

You may have these things enabled at the file system level and not even know it. So I did some digging and found that you can explicitly disable compression and encryption on Windows if you run fsutil.

fsutil behavior set disableencryption 1
fsutil behavior set disablecompression 1

Reboot and you should be set. Except... I wasn't. Same problem. So is it a sparse file? Sparse files are specially-encoded to take up less space on your physical disk drive if they are mostly empty... like full of zeroes.

Bingo.

When you try something and get an error message, always read the error message, kids.

Fortunately, you can use fsutil here, too:

fsutil sparse queryflag my_disk.vhd
This file is set as sparse

This is why Hyper-V can't mount it. You asked for 10M of zeroes. And if you looked at the file in a hex editor, you'd see them. I know. I did. But the zeroes are an illusion, put there to make you lose your mind (and save space writing zeroes to your hard drive that you'd rather were used saving pictures of memes). New-VHD works by explicitly not creating a sparse file when it generates a VHD, and it does this quietly and under the hood because that's exactly how you'd expect it to work and you'd scream bloody murder at the Hyper-V team if it didn't work that way.

So disable the sparsiness of the .VHD file. Is that a word? It is now.

fsutil sparse setflag my_disk.vhd 0
fsutil sparse queryflag my_disk.vhd
This file is NOT set as sparse

And now Mount-VHD is happy:

Mount-VHD -Passthru -Path .\my_disk.vhd

ComputerName            : COMPUTERNAME
Path                    : c:\my_disk.vhd
VhdFormat               : VHD
VhdType                 : Fixed
FileSize                : 10486272
Size                    : 10485760
MinimumSize             :
LogicalSectorSize       : 512
PhysicalSectorSize      : 512
BlockSize               : 0
ParentPath              :
DiskIdentifier          : 3AB4E01A-4E4C-B14E-B39E-5E7B50863170
FragmentationPercentage : 0
Alignment               : 1
Attached                : True
DiskNumber              : 2
IsPMEMCompatible        : False
AddressAbstractionType  : None
Number                  : 2

I originally discovered that making a copy of the QEMU-generated VHD worked, but the original didn't, even when both files were the same size and checksum. Sure enough, the easiest way to un-sparsify a file (Is that a word? It is now.) is to make a copy of it.

So I wrote up a little how-do-ya-do to create a .VHD file with qemu-img, copy it, and then delete the original. But not only is that not solving the problem, it requires twice as much disk space to work around it.

In lieu of calling "create, copy, delete", because "create, move" will preserve the sparsitivity (Is that a wor—?... nevermind), just create your VHD with qemu-img and then run fsutil sparse setflag VHD_FILE_NAME_HERE 0. qemu-img allows you to convert between different virtual disk formats and fine-tune its sparsiness, but you can't control it during virtual disk creation with qemu-img. So stick with fsutil for QEMU VHD files on Windows.

No comments: