Last Update: Sep 04, 2024 | Published: Oct 24, 2014
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.
We can also use Get-VHD.
get-vhd -id $vm.id -ComputerName $vm.computername
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
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
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:Diskstest2.vhdx. But it also has a snapshot so the current file name reflects that as you can see in the following image:
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:
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:diskstest3.vhdx". What if: Remove-VM will remove virtual machine "Test3". What if: Performing the operation "Remove File" on target "D:DisksTest2.vhdx". What if: Remove-VM will remove virtual machine "Test2". What if: Performing the operation "Remove File" on target "D:DisksTest1.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
Or I can pipe an expression to Remove-MyVM.
get-vm test1,test2 -ComputerName chi-hvr2 | Remove-MyVM -ComputerName chi-hvr2
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.