I used VMware Workstation for a while, which can automatically compact the virtual hard disk after shutting down the VM, an excellent way of freeing up space on my laptop hard drive. But I switched to using only Hyper-V now. Compacting virtual hard disks is not something you can enable in Hyper-V to run automatically. In this blog post, I will show you how to do this using a script that you can use to do that easily. 🙂
Limitations
Let me first start with things that this script can’t do. The optimize-vhd cmdlet has some limitations, and what the limitations are:
- Disks with checkpoints (Snapshots), the VM will have a .avhdx disk attached, which should not be compacted
- VMs that use differencing disks that have child virtual disks associated with them
- Virtual hard disks are associated with a virtual machine that has replication enabled and is currently involved in initial replication, resynchronization, test failover, or failover.
- I only tested this on my machine, not a stand-alone Hyper-V server or a Hyper-V server running in a Fail-Over cluster.
- The script only compacts .vhdx files, not .vhd files
- It only compacts dynamic .vhdx files. The optimize-vhd cmdlet does not support fixed disk files
- The VM needs to be turned off. Disks can’t be compacted which belong to a running VM
- The script will only run locally on the computer running Hyper-V. It can’t connect to a remote Hyper-V instance.
Running the script
After running, the Compact-VHDX function is available in your PowerShell session. You can run it without the -VMName parameter, and it will try to compact all .vhdx files associated with the registered VMs on your computer. If you run it with the -VMName parameter and specify one or more specific VMs (separated by a comma), it will only compact the .vhdx(s) on those machine(s).
Note: Requires to be run as a user with Administrator rights.
Example of running on all VMs
In this example, I ran the script on my laptop, and it compacted .vhdx files on three VMs. You can see the space that it recovered in the column on the right: (1.656Gb)

Example of running on one VM
I ran the script on my laptop and specified the ‘Exam’ VM in this example. You can see the space it recovered in the column on the right, also 1.656Gb. 🙂 (I used the same file that I copied into the VM and removed it later so that there was some space to recover.)

The script
Below is the script. You can save this locally and add this to your PowerShell profile by:
notepad $profile add ". c:\data\compact-vhdx.ps1" Close/Save and start a new PowerShell session
It has error checking/handling for:
- Is the Hyper-V PowerShell module installed? If not… Install it before continuing.
- Is the VMName valid if specified?
- Is the disk Dynamic?
- Is the VM running?
- Errors during mounting/compacting/dismounting
#Requires -RunAsAdministrator function Compact-VHDX { param ( [Parameter(Mandatory = $false, HelpMessage = "Enter the name of the machine(s) from which space of the VHDX(s) should be recovered")][string[]]$VMName ) #Validate if hyper-v module is available, install when needed if (-not (Get-Module -listAvailable -Name Hyper-V)) { Write-Warning "Hyper-V module is not installed, installing now..." Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-Management-PowerShell -NoRestart:$true } #Check if specified VMName is valid if specified if ($VMName) { if (-not (Hyper-V\Get-VM -Name $VMName -ErrorAction SilentlyContinue)) { Write-Warning ("Specified VM {0} was not found, aborting..." -f $VMName) return } } #Validate if the Virtual machine specified is running, abort if yes if ($VMName) { foreach ($vm in $VMName) { if ((Hyper-V\Get-VM -Name $VM).State -eq 'Running') { Write-Warning ("One or more of the specified VM(s) {0} were found but are running, please shutdown VM(s) first. Aborting..." -f $VM) return } } } #Gather all VHDXs from the VMs (or VM is $VMName was specified which don't have a parent/snapshot if (-not ($VMName)) { $vhds = Hyper-V\Get-VM | Hyper-V\Get-VMHardDiskDrive | Where-Object Path -Like '*.vhdx' | Sort-Object VMName, Path } else { $vhds = Hyper-V\Get-VM $VMName | Hyper-V\Get-VMHardDiskDrive | Where-Object Path -Like '*.vhdx' | Sort-Object Path } if ($null -eq $vhds) { Write-Warning ("No disk(s) found without parent/snapshot configuration, aborting....") } #Gather current size of VHDX files $oldsize = foreach ($vhd in $vhds) { if ((Hyper-V\Get-VHD $vhd.path).VhdType -eq 'Dynamic') { [PSCustomObject]@{ VHD = $vhd.Path OldSize = [math]::round((Get-Item $vhd.Path).Length / 1GB, 3) } } } #Compress all files foreach ($vhd in $vhds) { if (-not (Hyper-V\Get-VM $vhd.VMName | Where-Object State -eq Running)) { Write-Host ("`nProcessing {0} from VM {1}..." -f $vhd.Path, $vhd.VMName) -ForegroundColor Gray try { Hyper-V\Mount-VHD -Path $vhd.Path -ReadOnly -ErrorAction Stop Write-Host "Mounting VHDX" -ForegroundColor Green } catch { Write-Warning ("Error mounting {0}, please check access or if file is locked..." -f $vhd.Path ) continue } try { Hyper-V\Optimize-VHD -Path $vhd.Path -Mode Full Write-Host ("Compacting VHDX") -ForegroundColor Green } catch { Write-Warning ("Error compacting {0}, dismounting..." -f $vhd.Path) Hyper-V\Dismount-VHD $vhd.Path return } try { Hyper-V\Dismount-VHD $vhd.Path -ErrorAction Stop Write-Host ("Dismounting VHDX`n") -ForegroundColor Green } catch { Write-Warning ("Error dismounting {0}, please check Disk Management and manually dismount..." -f $vhd.Path) return } } else { Write-Warning ("VM {0} is Running, skipping..." -f $vhd.VMName) } } #Report on new VHDX sizes $report = foreach ($vhd in $vhds) { if ((Hyper-V\Get-VHD $vhd.path).VhdType -eq 'Dynamic') { [PSCustomObject]@{ 'Old Size (Gb))' = ($oldsize | Where-Object VHD -eq $vhd.Path).OldSize 'New Size (Gb)' = [math]::round((Get-Item $vhd.Path).Length / 1GB, 3) 'Space recovered (Gb)' = ($oldsize | Where-Object VHD -eq $vhd.Path).OldSize - [math]::round((Get-Item $vhd.Path).Length / 1GB, 3) VM = $vhd.VMName VHD = $vhd.Path } } } #Show overview if ($null -ne $report) { return $report | Format-Table -AutoSize } else { Write-Warning ("No dynamic disk(s) found to recover hard-disk space from, aborting....") } }
Download the script(s) from GitHub here
Pingback: PowerShell is fun :)PowerShell Profile