Compact Hyper-V Disks

Code Properties

  • Language: PowerShell
  • Modules: Hyper-V
  • Requires: Administrator privileges

Overview

Sources:

WARNING

The script/function name was renamed to Compress-VHDX to align with PowerShell approved verbs.

This function compresses VHDX (Virtual Hard Disk) files of Hyper-V virtual machines to recover disk space. It mounts each disk in read-only mode, optimizes it, and reports the space recovered.

Code

#Requires -RunAsAdministrator
function Compress-VHDX {
    param (
        [Parameter(Mandatory = $false, HelpMessage = "Enter the name of the VM(s) from which space should be recovered")]
        [string[]]$VMName
    )
      
    # validate if hyper-v module is available
    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 ($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 VMs are running
    if ($VMName) {
        foreach ($vm in $VMName) {
            if ((Hyper-V\Get-VM -Name $VM).State -eq 'Running') {
                Write-Warning ("One or more specified VM(s) {0} are running, please shutdown first. Aborting..." -f $VM)
                return
            }
        }
    }
    
    # gather VHDXs
    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
    $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..." -f $vhd.Path)
                return
            }        
        }
        else {
            Write-Warning ("VM {0} is Running, skipping..." -f $vhd.VMName)
        }
    }
 
    # report on new 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
            }
        }
    }
    
    if ($null -ne $report) {
        return $report | Format-Table -AutoSize
    }
    else {
        Write-Warning ("No dynamic disk(s) found to recover space from, aborting....")
    }
}

Usage

# compact all VMs
Compress-VHDX
 
# compact specific VM
Compress-VHDX -VMName "MyVM"
 
# compact multiple VMs
Compress-VHDX -VMName "VM1", "VM2"

Appendix

Note created on 2024-05-08 and last modified on 2024-12-31.

See Also


(c) No Clocks, LLC | 2024