
close
close
Chance to win $250 in Petri 2023 Audience Survey
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)
get-vhd -id $vm.id -ComputerName $vm.computername
Using Get-VHD with Windows PowerShell. (Image Credit: Jeff Hicks)
$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)
$vm | remove-vm
Using Remove-VM with Windows PowerShell. (Image Credit: Jeff Hicks)
Example virtual machine with snapshots. (Image Credit: Jeff Hicks)
$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)
#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
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)
More in PowerShell
Most popular on petri