Hyper-V VHD Summary

When I made the move to Windows 8, one of my tasks was to migrate my test environment from VirtualBox to Hyper-V. Windows 8 includes a client Hyper-V feature that is easy to use and includes PowerShell support. Plus I needed to expand my Hyper-V skills anyway, especially from the console. After setting up a number of VMs, I realized I needed to get a handle on disks: what files were in use for which VM? The Get-VM cmdlet writes a VirtualMachine object to the pipeline. One of its properties is HardDrives.


PS S:\> $vm = get-vm "XP Lab"
PS S:\> $vm.GetType().Name
VirtualMachine
PS S:\> $vm | select *

VMName : XP Lab
VMId : da416538-7a72-4b57-b08d-7780d81745b6
Id : da416538-7a72-4b57-b08d-7780d81745b6
Name : XP Lab
State : Off
OperationalStatus : {Ok}
PrimaryOperationalStatus : Ok
SecondaryOperationalStatus :
StatusDescriptions : {Operating normally}
PrimaryStatusDescription : Operating normally
SecondaryStatusDescription :
Status : Operating normally
Heartbeat :
ReplicationState : Disabled
ReplicationHealth : NotApplicable
ReplicationMode : None
CPUUsage : 0
MemoryAssigned : 0
MemoryDemand : 0
MemoryStatus :
SmartPagingFileInUse : False
Uptime : 00:00:00
IntegrationServicesVersion :
ResourceMeteringEnabled : False
ConfigurationLocation : C:\ProgramData\Microsoft\Windows\Hyper-V
SnapshotFileLocation : C:\ProgramData\Microsoft\Windows\Hyper-V
AutomaticStartAction : StartIfRunning
AutomaticStopAction : Save
AutomaticStartDelay : 0
SmartPagingFilePath : C:\ProgramData\Microsoft\Windows\Hyper-V
NumaAligned :
NumaNodesCount : 1
NumaSocketCount : 1
IsDeleted : False
ComputerName : SERENITY
Notes :
Path : C:\ProgramData\Microsoft\Windows\Hyper-V
CreationTime : 8/20/2012 5:43:38 PM
IsClustered : False
SizeOfSystemFiles : 29366
ParentSnapshotId :
ParentSnapshotName :
MemoryStartup : 536870912
DynamicMemoryEnabled : False
MemoryMinimum : 536870912
MemoryMaximum : 1099511627776
ProcessorCount : 1
RemoteFxAdapter :
NetworkAdapters : {Network Adapter}
FibreChannelHostBusAdapters : {}
ComPort1 : Microsoft.HyperV.PowerShell.VMComPort
ComPort2 : Microsoft.HyperV.PowerShell.VMComPort
FloppyDrive : Microsoft.HyperV.PowerShell.VMFloppyDiskDrive
DVDDrives : {DVD Drive on IDE controller number 1 at location 0}
HardDrives : {Hard Drive on IDE controller number 0 at location 0}
VMIntegrationService : {Time Synchronization, Heartbeat, Key-Value Pair Exchange, Shutdown...}

The property is actually a nested object.


PS S:\> $vm.HardDrives

VMName ControllerType ControllerNumber ControllerLocation DiskNumber Path
------ -------------- ---------------- ------------------ ---------- ----
XP Lab IDE 0 0 D:\VHD\XPLab.vhd

That tells me where the VHD is, but not about it. For that I can use Get-VHD.


PS S:\> $vm.HardDrives | Get-VHD

ComputerName : SERENITY
Path : D:\VHD\XPLab.vhd
VhdFormat : VHD
VhdType : Dynamic
FileSize : 8709523456
Size : 10737418240
MinimumSize : 10725765120
LogicalSectorSize : 512
PhysicalSectorSize : 512
BlockSize : 2097152
ParentPath :
FragmentationPercentage : 0
Alignment : 0
Attached : False
DiskNumber :
IsDeleted : False
Number :

There is some great information here like sizes, type and format. All I need now is a way to combine all of this information so that for every VM I can get a summary of pertinent VHD information. This is a great scenario for using Select-Object and creating a custom property.


$vm.HardDrives | Select VMName,Path,@{Name="Size";Expression={ (Get-VHD $_.path).Size}}

VMName Path Size
------ ---- ----
XP Lab D:\VHD\XPLab.vhd 10737418240

In the hash table, the Expression script block is running Get-VHD and returning the Size property. Eventually I want this for all VMs which means I’ll need to expand the HardDrives property.


$vm | select -expandproperty HardDrives | Select VMName,Path,@{Name="Size";Expression={ (Get-VHD $_.path).Size}},
@{Name="FileSize";Expression={(Get-VHD $_.path).FileSize}}

VMName Path Size FileSize
------ ---- ---- --------
XP Lab D:\VHD\XPLab.vhd 10737418240 8709523456

That is very promising. In fact, let’s cut to the chase.


#requires -version 3.0

Function Get-VHDSummary {

<#
.Synopsis
Get Hyper-V VHD Information
.Description
Get a summary report of all associated VHDs with their respective virtual
machines. This requires the Hyper-V PowerShell module.
.Example
PS C:\> Get-VHDSummary

VMName : CHI-Client02
Path : D:\VHD\Win7_C.vhd
Type : Dynamic
Format : VHD
SizeGB : 25
FileSizeGB : 21

VMName : CHI-Client02
Path : C:\Users\Public\Documents\Hyper-V\Virtual Hard Disks\CHI-Client2-Swap.vhdx
Type : Dynamic
Format : VHDX
SizeGB : 4
FileSizeGB : 1

VMName : Win2012-01
Path : D:\VHD\Win2012-01_340B1F8F-FFFB-4DCD-9B3A-7819D8BCE3C2.avhdx
Type : Differencing
Format : VHDX
SizeGB : 40
FileSizeGB : 2

.Example
PS C:\> get-vhdsummary | sort Type | format-table -GroupBy Type -Property VMName,Path,*Size*
.Example
PS C:\> get-vhdsummary | Copy-item -destination Z:\VHDBackup
.Example
PS C:\> get-vhdsummary | measure-object -property FilesizeGB -sum
.Link
Get-VM
Get-VHD
#>

Param()

Get-VM | Select-Object -expandproperty harddrives |
Select-Object -property VMName,Path,
@{Name="Type";Expression={(Get-VHD -Path $_.Path).VhdType}},
@{Name="Format";Expression={(Get-VHD -Path $_.Path).VhdFormat}},
@{Name="SizeGB";Expression={((Get-VHD -Path $_.Path).Size)/1GB -as [int]}},
@{Name="FileSizeGB";Expression={((Get-VHD -Path $_.Path).FileSize)/1GB -as [int]}}

} #end function

This function writes a summary object for each VM which means I can further sort, filter or whatever I need to do. Because this can take a bit of time to run, I’ll save the results to a variable.


PS S:\> $summary = get-vhdsummary

One unexpected bonus was that it helped me identify a VM with a “bad” VHD reference.


PS S:\> $summary | where {-Not $_.type}

VMName : Windows Server 2012 VHD Boot
Path : C:\Win8BetaServer.vhd
Type :
Format :
SizeGB : 0
FileSizeGB : 0

This was a file I had renamed, which “broke” the virtual machine. Or I can see how much space my VHD files are taking.


PS S:\> $summary | measure FileSizeGB -sum

Count : 18
Average :
Sum : 102
Maximum :
Minimum :
Property : FileSizeGB

Or perhaps I need to back them all up.


PS S:\> $summary | copy -dest g:\vhd-backup -whatif
What if: Performing operation "Copy File" on Target "Item: D:\VHD\Win7_C.vhd Destination: G:\vhd-backup\Win7_C.vhd".
...

Awesome. But…..my function is based on code I developed to run from the command line which works great as a one line command. I’m not likely to have a great number of virtual machines so performance isn’t that big a deal. Plus I could always run it as a job. If I’m going to turn this into a function, perhaps it makes sense to break this up.

If you look at my function, I’m running Get-VHD multiple times for the same file. It probably makes more sense to only get it once. Here’s a revised block of code.


Function Get-VHDSummary2 {
Param()

#get all virtual machines
$vms=Get-VM

foreach ($vm in $vms) {
Write-Host "Getting drive info from $($vm.name)" -foregroundcolor Cyan
#get the hard drives foreach virtual machine
$vm.HardDrives | foreach-object {
#a VM might have multiple drives so for each one get the VHD
$vhd=Get-VHD -path $_.path

<#
$_ is the hard drive object so select a few properties and
include properties from the VHD
#>
$_ | Select-Object -property VMName,Path,
@{Name="Type";Expression={$vhd.VhdType}},
@{Name="Format";Expression={$vhd.VhdFormat}},
@{Name="SizeGB";Expression={($vhd.Size)/1GB -as [int]}},
@{Name="FileSizeGB";Expression={($vhd.FileSize)/1GB -as [int]}}
} #foreach
} #foreach vm

} #close function

I’ll get the same result, and actually faster. My first version took 7.8 seconds and this takes 2.3 seconds. Because this is a function, I only have to type it once so I can add comments and even a progress message with Write-Host. This isn’t too say I can’t do this all from the console; it is just a bit more tedious.

The purpose of my post is to not only demonstrate some of the Hyper-V cmdlets, but also that what you type at the console doesn’t always make the best PowerShell script. Sometimes you need to re-think things for performance and maintainability.

Download Get-VHDSummary.