In my case, this was an important VM to the testing that I was doing, I had absolutely no desire to build a new one. I just wanted my VHD to be smaller so I could use it for the test I intended.
Now, since the VHD has been around we have been telling folks in the forums that there is no programmatic way to reduce the size of a VHD. There have been lots of opinions about this, but it isn’t just a case or truncating a binary file.
The problem becomes difficult because you have no idea how the OS in that VHD has laid data down into that VHD. Are there files way out at the far end, which would be corrupted if you simply chopped it off? Does your program have the right to go looking and figure that out and expose my data? If there is data out there, what should be done with it to make sure the OS that sues the VHD can actually find it – it is not as easy as moving that file, it could be a system file. And there is always the disk layout that must be honored, sectors, blocks, etc are part of how that VHD is made.
So, what have we been telling folks that get into this situation – use disk imaging software to create an image and then apply that to a new VHD of the proper size.
I decided – lets script that. This way I can answer a couple questions and pay attention to other things.
I use diskpart and DISM. DISM is built in to Windows 8 / Server 2012. And so is DiskPart. Without the full Hyper-V Role there is no way to create a VHD using PowerShell. Go figure.
* Disclaimer – I have only tried this with VMs that contain Server 2008 or newer. Older than that will not have a BCD, but the script will make one.
Here is my entire script, hopefully you can follow my comments.
# Server 2012 / Windows 8 VHD resizer. Using DISM.
# Prototyped on Server 2012 not running Hyper-V
# This utilizes PowerShell v3 and cmdlets from Server 2012 / Windows 8 - it will not run on any older OS.
# Copy write – Brian Ehlert
# Ask the path of the VHD and test it
Do {
$imagePath = Read-Host "Please enter the full path to you VHD. i.e. D:\VMs\MyTooBigVhd.vhd "
} until ((Test-Path -Path $imagePath ) -eq $true) # Mount the VHD that will be getting resized
$orgVhd = Mount-DiskImage -ImagePath $imagePath -PassThru
$orgVhd = Get-DiskImage -ImagePath $orgVhd.ImagePath # Get the partitions
$orgParts = Get-Partition -DiskNumber $orgVhd.Number # Use DISM command line to capture the VHD one WIM file. Each partition with a different name.
$wimName = ($orgvhd.ImagePath.Split(".")[0] + ".wim")
foreach ($part in $orgParts) {
if ($part.Size -gt 524288000){ # skip the partition if it is less than 500MB, most likely there is no OS or it is the System Reserved partition.
$capDir = $part.DriveLetter + ":\"
$partNum = $part.PartitionNumber
"Be patient, this could take a long time"
& dism /capture-image /ImageFile:$wimName /CaptureDir:$capDir /Name:$partNum
}
}
# dismount the VHD that was just captured
Dismount-DiskImage -ImagePath $orgvhd.ImagePath
# Get the size of the WIM. As the new VHD must be larger.
$wimFile = Get-ItemProperty $wimName
# Ask the size of the new VHD in GB
Do {
$newSize = [uint32](Read-Host "How large would you like the new VHD (in whole GB)? The WIM is" ([uint32]($wimFile.Length /1024 /1024 /1024))"GB of data ")
} until ($newSize -gt ([uint32]($wimFile.Length /1024 /1024 /1024)))
$newSize = [uint64]$newSize * 1024 # convert GB to MB
foreach ($part in $orgParts)
{
if ($part.Size -gt 524288000){ # skip the partition if it is less than 500MB.
$capDir = $part.DriveLetter + ":\"
$partNum = $part.PartitionNumber
$newVhdPath = $orgvhd.ImagePath.Split(".")[0] + $partNum +"New." + $orgvhd.ImagePath.Split(".")[1]
$diskPart = @"
create vdisk file="$newVhdPath" type=expandable maximum=$newSize
select vdisk file="$newVhdPath"
attach vdisk
create partition primary
active
format fs=ntfs quick
assign
"@
$diskPart | diskpart
$newVhd = Get-DiskImage -ImagePath $newvhdPath
$newVhdDrive = (Get-Partition -DiskNumber $newVhd.Number)
$newVhdLetter = (Get-Partition -DiskNumber $newVhd.Number).DriveLetter + ":"
"Be patient, this could take a long time"
& dism /apply-image /ImageFile:$wimName /ApplyDir:$newVhdLetter /Name:$partNum
New-PSDrive -PSProvider FileSystem -Name $newVhdDrive.DriveLetter -root ($newVhdDrive.DriveLetter + ":\") # Make the new volume known to your PowerShell session
# if \Windows then assume a boot volume and create the BCD
if ((Test-Path -Path ($newVhdLetter + "\Windows") -PathType Container) -eq $true)
{
bcdboot $newVhdLetter\Windows /s $newVhdLetter
}
Remove-PSDrive -PSProvider FileSystem -Name $newVhdDrive.DriveLetter
Start-Sleep 10 # Settling time
Dismount-DiskImage -ImagePath $newVhdPath
}
}