Friday, January 25, 2013

PowerShell for reducing the size of a VHD

Recently I have gotten myself into a situation where a VHD that I created was too large for a situation.  And as many of us know, there are tools to make VHDs smaller, only larger. 
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
        format fs=ntfs quick
        $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