Completely Remove a Hyper-V Virtual Machine with PowerShell

In the PowerShell Hyper-V module there is a cmdlet called Remove-VM that does pretty much what the name says. You give it the name of a virtual machine (VM) on a Hyper-V server and PowerShell will gladly remove it. You can remove 1, 10, or 100 VMs with the same, simple one-line command: Remove-VM MyVM –computername HV01. The cmdlet will also delete the VM from the Hyper-V host just as if you had used the graphical Hyper-V Manager to delete it. Note that the VM must not be running.

However, not everything is completely removed. When you delete a VM, the only thing that is truly deleted is the configuation file and its registration with the Hyper-V host. Any VHD or VHDX files remain, untouched. I suppose the thinking is that you might want to re-use the virtual disk file with another VM.
A VM is an easy thing to create, but a virtual disk is a bit more valuable. Still, what if you truly want to remove a VM including disk files? Here’s one way I have come up with using PowerShell and it can all be accomplished from your desktop, assuming you have the Hyper-V module available on your desktop. Let’s walk through the process.
First, we need a VM.

$vmname = "test1"
$Computername = "chi-hvr2"
$vm = Get-VM -Name $VMName -ComputerName $Computername	

Using PowerShell, we can see the path for the VM’s hard drive.

Getting the path to the virtual machine's hard drive with Windows PowerShell. (Image Credit: Jeff Hicks)
Getting the path to the virtual machine’s hard drive with Windows PowerShell. (Image Credit: Jeff Hicks)

We can also use Get-VHD.

get-vhd -id $vm.id -ComputerName $vm.computername

Using Get-VHD with Windows PowerShell. (Image Credit: Jeff Hicks)
Using Get-VHD with Windows PowerShell. (Image Credit: Jeff Hicks)

Unfortunately, Get-VHD won’t let us pipe an object with the computer name. But here to you can see a path. This path is relative to the Hyper-V host. In my network, that is CHI-HVR2. So to delete this file, I will need to rely on PowerShell remoting.

$disks = Get-VHD -VMId $vm.Id -ComputerName $computername
Invoke-Command {
 Remove-Item $using:disks.path -WhatIf
} -computername $vm.ComputerName			

Using PowerShell remoting to delete file. (Image Credit: Jeff Hicks)
Using PowerShell remoting to delete file. (Image Credit: Jeff Hicks)

I can re-run this without –Whatif and all disk files will be removed. Once the disks are gone I can  go ahead and run Remove-VM.

$vm | remove-vm

Using Remove-VM with Windows PowerShell. (Image Credit: Jeff Hicks)
Using Remove-VM with Windows PowerShell. (Image Credit: Jeff Hicks)

However, there is a potential wrinkle if the VM has any snapshots. Let’s look at another VM as an example. My Test2 VM has a single VHDX file D:\Disks\test2.vhdx. But it also has a snapshot so the current file name reflects that as you can see in the following image:
Example virtual machine with snapshots. (Image Credit: Jeff Hicks)
Example virtual machine with snapshots. (Image Credit: Jeff Hicks)


When you run Remove-VM it will also remove any snapshots automatically. This means that the disk configuration will revert back to Test2.vhdx, and it won’t be deleted. I suppose I could try to parse the file name. But because the snapshots are going to go anyway, I might as well delete them myself first.

$VM | Remove-VMSnapshot –IncludeAllChildSnapshots

Now, the VM reflects the correct file path:

Removing the virtual machine's snapshot with Windows PowerShell. (Image Credit: Jeff Hicks)
Removing the virtual machine’s snapshot with Windows PowerShell. (Image Credit: Jeff Hicks)

With this done, I can remove the file and VM as I did before. But now for the fun part.
I’m assuming this is a task you may want to perform more than once, in which case having a PowerShell tool will make life much easier. So here is my function called Remove-MyVM. This will require PowerShell v4 and the Hyper-V PowerShell module. But you should be able to run it on a Windows 8.1 desktop.

#requires -version 4.0
Function Remove-MyVM {
<#
 .Synopsis
 Remove a Hyper-V Virtual machine including disk files.
 .Description
 This is a wrapper and proxy command that uses Get-VM and Remove-VM to remove a Hyper-V virtual machine as well as its associated virtual disk files. If the virtual machine has snapshots, those will be removed as well.
 Note: There appears to be an issue with the Remove-VM cmdlet. It does not appear to respect the -Confirm parameter. You will most likely always be prompted to remove the virtual machine.
 .Parameter Computername
 The name of they Hyper-V host. You should always specify the name of a remote server, even if piping an expression to this command. This parameter has an alias of CN.
 .Parameter ClusterObject
 Specifies the cluster resource or cluster group of the virtual machine to be retrieved.
 .Parameter Id
 Specifies the identifier of the virtual machine to be retrieved.
 .Parameter Name
 Specifies the name of the virtual machine to be retrieved. This parameter has an alias of VMName.
 .Notes
 Version      : 1.0
 Last Modified:	October 21, 2014
 Learn more about PowerShell:
 
Essential PowerShell Learning Resources
**************************************************************** * DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED * * THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK. IF * * YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, * * DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING. * **************************************************************** .Example PS C:\> Remove-MyVM Test1 -computername chi-hvr2 .Example PS C:\> get-vm test* -computername chi-hvr2 | remove-myvm -computername chi-hvr2 -whatif What if: Remove-VMSnapshot will remove snapshot "Test3 - (10/21/2014 - 8:33:29 PM)". What if: Performing the operation "Remove File" on target "D:\disks\test3.vhdx". What if: Remove-VM will remove virtual machine "Test3". What if: Performing the operation "Remove File" on target "D:\Disks\Test2.vhdx". What if: Remove-VM will remove virtual machine "Test2". What if: Performing the operation "Remove File" on target "D:\Disks\Test1.vhdx". What if: Remove-VM will remove virtual machine "Test1". Run this command without -Whatif to remove these files and virtual machines. .Link Get-VM Remove-VM #> [CmdletBinding(DefaultParameterSetName='Name',SupportsShouldProcess=$True)] param( [Parameter(ParameterSetName='Id')] [Parameter(ParameterSetName='Name')] [ValidateNotNullOrEmpty()] [Alias('CN')] [string]$ComputerName = $env:COMPUTERNAME, [Parameter(ParameterSetName='Id', Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)] [ValidateNotNull()] [System.Nullable[guid]] $Id, [Parameter(ParameterSetName='Name', Position=0, ValueFromPipeline=$true)] [Alias('VMName')] [ValidateNotNullOrEmpty()] [string[]]$Name, [Parameter(ParameterSetName='ClusterObject', Mandatory=$true, Position=0, ValueFromPipeline=$true)] [PSTypeName('Microsoft.FailoverClusters.PowerShell.ClusterObject')] [ValidateNotNullOrEmpty()] [psobject]$ClusterObject, [Switch]$Passthru ) begin { Write-Verbose -Message "Starting $($MyInvocation.Mycommand)" Write-Verbose -Message "Using parameter set $($PSCmdlet.ParameterSetName)" Try { $outBuffer = $null if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer)) { $PSBoundParameters['OutBuffer'] = 1 } #remove Whatif from Boundparameters since Get-VM doesn't recognize it $PSBoundParameters.Remove("WhatIf") | Out-Null $PSBoundParameters.Remove("Passthru") | Out-Null $PSBoundParameters.Remove("Confirm") | Out-Null $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Get-VM', [System.Management.Automation.CommandTypes]::Cmdlet) #$scriptCmd = {& $wrappedCmd @PSBoundParameters } Write-Verbose "Using parameters:" Write-verbose ($PSBoundParameters | Out-String) $ScriptCmd = { & $wrappedCmd @PSBoundParameters -pv vm | foreach-object -begin { #create a PSSession to the computer Try { Write-Verbose "Creating PSSession to $computername" $mysession = New-PSSession -ComputerName $computername #turn off Hyper-V object caching Write-Verbose "Disabling VMEventing on $computername" Invoke-Command { Disable-VMEventing -Force -confirm:$False } -session $mySession } catch { Throw } } -process { Write-Debug ($vm | Out-String) #write the VM to the pipeline if -Passthru was called if ($Passthru) { $vm } Invoke-Command -scriptblock { [CmdletBinding(SupportsShouldProcess=$True,ConfirmImpact='medium')] Param($VM,[string]$VerbosePreference) $WhatIfPreference = $using:WhatifPreference $confirmPreference = $using:ConfirmPreference Write-Verbose "Whatif = $WhatifPreference" Write-Verbose "ConfirmPreference = $ConfirmPreference" #set confirm value switch ($confirmPreference) { "high" { $cv = $false } "medium" { $cv = $False } "low" { $cv = $True } Default { $cv = $False } } #remove snapshots first #$VM is the pipelinevariable from the wrapped command Write-Verbose "Testing for snapshots" if (Get-VMSnapshot -VMName $VM.name -ErrorAction SilentlyContinue) { Write-Verbose "Removing existing snapshots" Remove-VMSnapshot -VMName $VM.name -IncludeAllChildSnapshots -Confirm:$cv } $disks = $vm.id | Get-VHD #remove disks foreach ($disk in $disks) { #the disk path might still reflect a snapshot so clean them up first #This code shouldn't be necessary since we are removing snapshots, #but just in case... [regex]$rx = "\.avhd(.?)" if ($disk.path -match $rx) { Write-Verbose "Cleaning up snapshot leftovers" #get a clean version of the file extension $extension = $rx.Matches($disk.path).value.replace("a","") #a regex to find a GUID and file extension [regex]$rx = "_(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}.avhd(.?)" $thePath = $rx.Replace($disk.path,$extension) } else { $thePath = $disk.path } Try { Write-Verbose "Removing $thePath" Remove-Item -Path $thePath -ErrorAction Stop -Confirm:$cv $DiskRemove = $True } Catch { Write-Warning "Failed to remove $thePath" Write-warning $_.exception.message #don't continue removing anything if there was a problem removing the disk file $DiskRemove = $False } } if ($diskRemove) { #remove the VM $VM | foreach { Write-Verbose "Removing virtual machine $($_.name)" Remove-VM -Name $_.Name -Confirm:$cv } #foreach } #if disk remove was successful else { Write-Verbose "Aborting virtual machine removal" } } -session $mySession -ArgumentList ($vm,$VerbosePreference) -hidecomputername } -end { #remove PSSession. Ignore -Whatif and always remove it. Write-Verbose "Removing PSSession to $computername" Remove-PSSession -Session $mySession -WhatIf:$false -Confirm:$False } } #scriptCMD $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin) $steppablePipeline.Begin($PSCmdlet) } catch { throw } } process { try { $steppablePipeline.Process($_) } catch { throw } } end { try { $steppablePipeline.End() } catch { throw } Write-Verbose -Message "Ending $($MyInvocation.Mycommand)" } } #end function Remove-MyVM

This script is mix of proxy function and wrapper. I probably could have written something completely from scratch, but I wanted to take advantage existing cmdlets. The proxy function is actually for Get-VM and during pipeline processing that happens first. But then I have code to automatically handle all of the tasks I just demonstrated.
In a proxy function, the script command must be a single, pipelined expression, so I had to get creative. I’m taking advantage of a new PowerShell v4 feature called pipelinevariable, which has an alias of pv.

&$wrappedCmd @PSBoundParameters -pv vm

The results of the wrapped command, i.e., Get-VM, are saved to the variable $pv so that I can use them later in the pipelined expression. The Get-VM command is going to return a collection of VMs, and I decided that the best way to handle the processing is to run all the commands remotely, so I pipe to a Foreach-Object and before anything is processed, create a PSSession to the Hyper-V host.

Foreach-object -begin {
           #create a PSSession to the computer
            Try {
                Write-Verbose "Creating PSSession to $computername"
                $mysession = New-PSSession -ComputerName $computername
                #turn off Hyper-V object caching
                Write-Verbose "Disabling VMEventing on $computername"
                Invoke-Command { Disable-VMEventing -Force -confirm:$False } -session $mySession
            }			

I process each VM with Invoke-Command, running a scriptblock to remove snapshots, disk files and VMs. Because I wanted to preserve features like –Whatif and –Confirm, even in the remote scriptblock I had to resort to a little trickery. While a script block can use parameters, they are positional and you really can’t pass something like –Verbose or –Whatif. So the beginning of the script block sets preferences remotely, using local values.

Invoke-Command -scriptblock {
                [CmdletBinding(SupportsShouldProcess=$True,ConfirmImpact='medium')]
                Param($VM,[string]$VerbosePreference)
                $WhatIfPreference = $using:WhatifPreference
                $confirmPreference = $using:ConfirmPreference
                Write-Verbose "Whatif = $WhatifPreference"
                Write-Verbose "ConfirmPreference = $ConfirmPreference"
                #set confirm value
                switch ($confirmPreference) {
                "high" { $cv = $false  }
                "medium" { $cv = $False  }
                "low" { $cv = $True }
                Default { $cv = $False }
                }			

But now I can run the command like this:

remove-myvm test* -ComputerName chi-hvr2 -whatif

102214 0148 CompletelyR8
Or I can pipe an expression to Remove-MyVM.

get-vm test1,test2 -ComputerName chi-hvr2 | Remove-MyVM -ComputerName chi-hvr2

Piping an expression to Remove-MyVM with PowerShell. (Image Credit: Jeff Hicks)
Piping an expression to Remove-MyVM with PowerShell. (Image Credit: Jeff Hicks)

When you are piping something to Remove-MyVM, be sure to include the computer name again for the Hyper-V host. Otherwise the command will try to find the VMs locally.

There is one quirk though with Remove-VM. As you can see in my screen shot, I am being prompted for confirmation. It appears the Remove-VM will always prompt you regardless of what you do with –Confirm. In this case, you might as well say yes because the disk files have already been removed. Please, test this in a safe, non-production environment so that you fully understand how it works. Standard disclaimers apply.
I hope you find this useful, and if so please let me know.