- If a Windows service hangs, restart the service with PowerShell - Mon, Dec 12 2022
- Install, remove, list, and set default printer with PowerShell - Mon, Nov 7 2022
- Format time and date output of PowerShell New-TimeSpan - Wed, Nov 2 2022
Prerequisites
You'll need a supported version of Windows with the Hyper-V role installed, including the Client editions. Yes, you can also use it with a Windows 10/11 machine.
Compress-Vhdx uses Optimize-VHD, which comes with the Hyper-V management module. This means you need the Hyper-V management module installed on the computer where you plan to run the command. Normally, this should already be installed. If it isn't, you can quickly add it using the following command:
Install-WindowsFeature -Name RSAT-Hyper-V-Tools -IncludeManagementTools
Wait for a few seconds for the installation to complete …
… and you're done. You do not need to restart your computer when the installation has completed.
Usage examples
Now it's time to see Compress-Vhdx in action. I find it very easy to learn by example, so let's put our function to work.
Basic usage
The basic usage is just Compress-Vhdx with the path to the folder where your VHDX files are located.
Compress-Vhdx C:\VMs
If you know for sure that there are more VHDX files in your folder, you may wonder why they don't appear in your list. Well, the answer is that if a VHDX file is in use (the VM is running), that virtual disk cannot be compressed. You need to turn off your VM before you can do that. The example below shows you the additional information you get in Verbose mode.
Using the Verbose switch
Let's use the same command as above but switch to Verbose mode.
Compress-Vhdx -Path C:\VMs\ -Verbose
In Verbose mode, you see more information about what's happening under the hood:
- You see how many files will be processed.
- You see which VHDX files have been skipped.
- At the end, you may also see how much time the command took to complete. This may be useful if you're dealing with larger files.
Note: You may notice that I have explicitly included the "Path" switch before providing the path to my target folder. It is not mandatory, but it sure is nice to use it, particularly in a script that other admins might use.
Include subfolders
Another useful switch is IncludeSubfolders, which allows the command to work recursively through all the subfolders in the path you provide. If you have 20 VMs and each VM has its dedicated folder—including the VHDX file(s)—you only need to run the command once, at the level of the root folder.
Compress-Vhdx -Path C:\VMs\ -IncludeSubfolders -Verbose
In this last example, an extra VHDX file (highlighted in yellow) was processed. It was located in a subfolder, which was not scanned in the previous examples.
Note: For this last example, I shut down one of my VMs (named WSUS_Export) to show the difference from the previous example. Before shutdown, I deleted some files from it. You may see the space gains in the column Saved [GB] (highlighted in green).
The Compress-Vhdx function
The complete code for the function is shown below. You may include it in your PowerShell profile script or in a module for easy access.
Subscribe to 4sysops newsletter!
function Compress-Vhdx { <# .Synopsis Compresses all the VHDX files from a specified location .DESCRIPTION Compress-Vhdx retrieves each VHDX files from a specified location and compacts each using the native command Optimize-VHD .EXAMPLE Compress-Vhdx -Path "C:\MyVMs" -Recurse Compacts all the VHDX files from the specified path, including subfolders .NOTES Last updated on 2021.11.05 #> [CmdletBinding()] Param ( # Path to the VHDX files [Parameter(ValueFromPipelineByPropertyName,Position=0)]$Path = (Get-Location).Path, # Go through subfolders, if selected [Parameter(Position=1)][switch]$IncludeSubfolders ) Begin { $StartTime = Get-Date # Search for VHDX files in subfolders? if ($IncludeSubfolders) { $AllVhdx = Get-ChildItem *.vhdx -Path $Path -Recurse -ErrorAction SilentlyContinue } else { $AllVhdx = Get-ChildItem *.vhdx -Path $Path -ErrorAction SilentlyContinue } # Are there any VHDX files in the location? if ($AllVhdx.Count -lt 1) { Write-Warning "There is no VHDX file to compress in `"$Path`". Make sure that the path is correct and it contains VHDX files" break } #Clear-Host Write-Verbose "Compacting $($AllVhdx.Count) VHDX files, please wait" } #Begin Process { $Stats = foreach ($v in $AllVhdx) { $OldSize = $v.Length try { Optimize-VHD -Path $v.FullName -Mode Full -ErrorAction Stop Write-Verbose "Compressing $($v.Name)" $NewSize = (Get-ChildItem -Path $v.FullName).Length $Saved = $OldSize - $NewSize [PSCustomObject] @{ #Name = $v.Name Path = $v.FullName "Initial Size [GB]" = [math]::round($OldSize /1Gb, 2) "Current Size [GB]" = [math]::round($NewSize /1Gb, 2) "Saved [GB]" = [math]::round($Saved /1Gb, 2) } } catch { Write-Verbose "Skipping $($v.Name). File may be in use " } $TotalSaved += $Saved } #$Stats } #Process End { $Duration = New-TimeSpan -Start $StartTime -End (Get-Date) $DurationPretty = $($Duration.Hours).ToString() + "h:" + $($Duration.Minutes).ToString() + "m:" + $($Duration.Seconds).ToString() + "s" $Stats | FT -Wrap -AutoSize Write-Verbose "The operation completed in $DurationPretty" Write-Verbose "Disk space saved: $([math]::round($TotalSaved /1Gb, 2)) GB" } #End } #function