Free Hyper-V Script: Completely Delete a Virtual Machine

From my own library, my favorite Hyper-V script is the orphaned file locator. It would be nice, though, if such a script weren’t necessary. I believe that you should be able to delete a virtual machine and be done with it. Digging through the file system to collect scraps tossed to the roadside by the official tools is tedious busy work that should be done by the computer. So, I built a tool that will do that. It is not intended to replace the orphaned file locator. This script is intended to run against a virtual machine before deletion; the orphaned file locator is intended to be run at some time after. You will not be able to run this script against already-deleted systems.

A note on the script name: I debated on shadowing the built-in Remove-VM cmdlet to enhance it with features that I believed that it should have always had. In the end, I decided to choose a new name because this script deviates so far from the original cmdlet. The hard part was in choosing a verb. Microsoft has some pre-defined “approved” verbs that they’d like everyone to use. As much as I hate being boxed in, I have to admit that it’s a good idea. You can use Get-Verb to see what the approved list is. Unfortunately, that list doesn’t have anything fun like “obliterate”, “demolish”, “eradicate”, “annihilate”, etc. (but Start-GlobalThermonuclearWar is perfectly acceptable). So, following patterns that I’ve seen in other names, I chose “Clear” as the verb.

Script Features

These are the features of the script:

  • Deletes a virtual machine
  • Removes a virtual machine from its cluster
  • Removes all of a virtual machine’s files, including virtual hard disks
  • If snapshots are present, applies the oldest before doing anything — this avoids the necessity to wait on virtual hard disks to merge
  • Deletes any folders that held the virtual machine’s files IF they are empty after the deletion and IF they are not marked as important. Criteria for importance:
    • Not used as a default VM or VHD storage location by the VM’s host
    • Not used as a configuration, snapshot, or smart paging location by any other virtual machine or snapshot on the host
  • Detailed -WhatIf  support
  • Not dependent on any other PowerShell module
  • Operates on local and remote hosts

How It Works

This script is very procedural in nature. This is the process:

  1. Gather a list of everything that makes up the virtual machine: files, folders, whether or not it is clustered, and whether or not it is participating in Hyper-V Replica. If it is being replicated, that’s a hard stop. You’ll need to break the replica configuration yourself.
  2. Gather an inventory of its host: host default folders and the storage folders for its other virtual machines. This helps to ensure that we do not inadvertently delete an important folder.
  3. Stop the virtual machine if it is running. Since it’s being deleted anyway, this is a hard stop — no waiting for an orderly guest shut down. If the virtual machine is currently in a saved state, that is automatically cleared.
  4. The virtual machine is removed from the cluster, if it is highly available.
  5. The oldest snapshot, if any, is applied. That means that the current state of the virtual machine will be permanently lost — which seems like a non-issue to me, since you asked to delete it. The benefit is that you won’t have to wait for any virtual hard disks to be merged like you do when using the built-in delete operations.
  6. All snapshots, if any, are deleted.
  7. The virtual machine is deleted.
  8. Any surviving files gathered during the inventory are deleted.
  9. Any surviving folders gathered during the inventory that are not shared by other virtual machines and do not have any files or folders as children are deleted.

Note on Windows 10/Windows Server 2016 Support

I wrote this script against Windows Server 2012 R2 test systems so I have full confidence it will operate on that generation of Hyper-V. Given the timing of this article’s publication, a natural question is, “will it work on Windows 10/Windows Server 2016?” The answer is: probably. The script relies on the native functionality of Hyper-V as much as possible, meaning that most of the heavy lifting is done by the Virtual Machine Management service. Because VMMS should be handling all of the new file types, none should be left over for this script to worry about. So, as long as your system is healthy and performing like it should, a general assumption would be that this script should have the same outcome on versions 2012-2016 as well as their matching Client Hyper-V versions. If something doesn’t work, it will not recognize those new file types and will leave them and their folders behind. All that said, this script must make some assumptions about the file layout of virtual machines. I believe that these conditions carry forth well enough to 2016, but I don’t yet know that for certain. Until I know, proceed at your own risk.

Once I have a solid Windows Server 2016 environment to test with, I will update the script to check for the new file types and handle them as well.

Disclaimer

This script deletes files. That means that data loss is part of what it is designed to cause. I have done everything in my power to scope its deletions as tightly as possible to the input, but I cannot control your input nor can I ever be totally certain that a script with this level of complexity will always perform precisely as intended in every possible situation. As always, you should have good backups of everything and be extremely careful with wildcards and pipeline input.

Neither I nor Altaro Software, Ltd. accept any responsibility for anything that is done with this script. If you don’t trust it, don’t use it.

Script Source

Save this script as Clear-VM.ps1. If pasted verbatim, you call the file when you need it (ex: C:\Scripts\Clear-VM.ps1). There are instructions in the file for removing three comment markers to enable the script to be dot-sourced so that you can include it in your profile, if you like.

The script fully supports Get-Help.

<#
.SYNOPSIS
	 Completely removes a virtual machine and all of its related files.
.DESCRIPTION
	 Completely removes a virtual machine and all of its related files.
	 Removes any related cluster resources, virtual hard disk files, and folders created specially for this virtual machine.
.PARAMETER VM
	The virtual machine to be deleted.
	Accepts objects of type:
	* String: A name of a virtual machine.
	* VirtualMachine: An object from Get-VM.
	* System.GUID: A virtual machine ID. MUST be of type System.GUID to match.
	* ManagementObject: A WMI object of type Msvm_ComputerSystem
	* ManagementObject: A WMI object of type MSCluster_Resource
.PARAMETER ComputerName
	The name of the computer that currently hosts the virtual machine to remove. If not specified, the local computer is assumed.
	Ignored if VM is of type VirtualMachine or ManagementObject.
.PARAMETER Force
	Bypasses prompting. Also, if the oldest snapshot cannot be applied, attempts to delete all snapshots as they are. This could cause a long merging operation.
.NOTES
	 Author: Eric Siron
	 Copyright: (C) 2016 Altaro Software
	 Version 1.1
	 Authored Date: October 1, 2016

	 Revision History
	 ----------------
	 1.2
	 ---
	 More graceful error messaging when a virtual machine's component folders cannot be searched. Functionality is unchanged.

	 1.1
	 ---
	 * Changed testing process for items to be delete to accomodate more PS versions and hosts.
	 * Changed testing for existence of clustered resources to accommodate "empty but not null" conditions.
.EXAMPLE
	Clear-VM oldvm
	--------------
	Deletes the virtual machine "oldvm" from the local computer.

.EXAMPLE
	Clear-VM oldvm othercomputer
	----------------------------
	Deletes the virtual machine "oldvm" from the host named "othercomputer"

.EXAMPLE
	Get-VM | Clear-VM
	-----------------
	Deletes every virtual machine on the local computer.
#>
#requires -Version 4

#function Clear-VM		# Uncomment this line to use as dot-sourced function or in a profile. Also the next line and the very last line.
#{							# Uncomment this line to use as dot-sourced function or in a profile. Also the previous line and the very last line.
	[CmdletBinding(ConfirmImpact='High')]
	param(
		[Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=1)]
		[Alias('VMName', 'Name')]
		[Object]
		$VM,

		[Parameter(ParameterSetName='ByName', Position=2)]
		[String]
		$ComputerName = $env:COMPUTERNAME,

		[Parameter()]
		[Switch]
		$IgnoreFolders,

		[Parameter()]
		[Switch]
		$Force,

		[Parameter()]
		[Switch]
		$WhatIf
	)
BEGIN {
	Set-StrictMode -Version Latest
	$FilterRemoteDisk = '^[A-Z]:\\'
	$FileSearchScriptBlock = {
		param(
			[Parameter(Mandatory=$true, Position=1)]
			[String[]]
			$FoldersToScan
		)

		foreach($FolderToScan in $FoldersToScan)
		{
			try
			{
				Get-ChildItem -Path $FolderToScan -File
			}
			catch
			{
				Write-Warning -Message ('Unable to enumerate files in {0}. Manual cleanup of this location may be necessary. Error: {0}' -f $_.Exception.Message)
			}
		}
	}

	$FileOrFolderDeleteScriptBlock = {
		param(
			[Parameter(Mandatory=$true, Position=1)]
			[String[]]
			$ItemsToDelete
		)

		foreach($ItemToDelete in $ItemsToDelete)
		{
			if ($ItemToDelete -and (Test-Path $ItemToDelete))
			{
				$FolderTest = Get-Item -Path $ItemToDelete
				if((Test-Path -Path $FolderTest -PathType Container) -and (Get-ChildItem -Path $FolderTest))
				{
					Write-Warning -Message ('Cannot remove folder {0} as it has children' -f $ItemToDelete)
					continue
				}
				try
				{
					Remove-Item -Path $ItemToDelete -Force -ErrorAction Stop
				}
				catch
				{
					Write-Warning -Message $_.Message
				}
			}
		}
	}

	$ProcessWMIJobScriptBlock = {
		param
		(
			[Parameter(ValueFromPipeline=$true)][System.Management.ManagementBaseObject]$WmiResponse,
			[Parameter()][String]$WmiClassPath = $null,
			[Parameter()][String]$MethodName = $null
		)

		$ErrorCode = 0

		if($WmiResponse.ReturnValue -eq 4096)
		{
			$Job = [WMI]$WmiResponse.Job

			while ($Job.JobState -eq 4)
			{
				Start-Sleep -Milliseconds 100
				$Job.PSBase.Get()
			}

			if($Job.JobState -ne 7)
			{
				if ($Job.ErrorDescription -ne '')
				{
					throw $Job.ErrorDescription
				}
				else
				{
					$ErrorCode = $Job.ErrorCode
				}
			}
		}
		elseif ($WmiResponse.ReturnValue -ne 0)
		{
			$ErrorCode = $WmiResponse.ReturnValue
		}

		if($ErrorCode -ne 0)
		{
			if($WmiClassPath -and $MethodName)
			{
				$PSWmiClass = [WmiClass]$WmiClassPath
				$PSWmiClass.PSBase.Options.UseAmendedQualifiers = $true
				$MethodQualifiers = $PSWmiClass.PSBase.Methods[$MethodName].Qualifiers
				$IndexOfError = [System.Array]::IndexOf($MethodQualifiers['ValueMap'].Value, [String]$ErrorCode)
				if($IndexOfError -ne -1)
				{
					'Error Code: {0}, Method: {1}, Error: {2}' -f $ErrorCode, $MethodName, $MethodQualifiers['Values'].Value[$IndexOfError]
				}
				else
				{
					'Error Code: {0}, Method: {1}, Error: Message Not Found' -f $ErrorCode, $MethodName
				}
			}
		}
	}
}

PROCESS {
	$VMObject = $null
	$ClusteredResources = $null
	$VMClusterGroup = $null
	Write-Progress -Activity 'Gathering information' -Status 'Loading the specified virtual machine' -PercentComplete 0
	switch($VM.GetType().FullName)
	{
		'Microsoft.HyperV.PowerShell.VirtualMachine' {
			$VMObject = Get-WmiObject -ComputerName $VM.ComputerName -Namespace 'root\virtualization\v2' -Class 'Msvm_ComputerSystem' -Filter ('Name="{0}"' -f $VM.Id) -ErrorAction Stop
		}

		'System.Guid' {
			$VMObject = Get-WmiObject -ComputerName $Computername -Namespace 'root\virtualization\v2' -Class 'Msvm_ComputerSystem' -Filter ('Name="{0}"' -f $VM) -ErrorAction Stop
		}

		'System.Management.ManagementObject' {
			switch ($VM.ClassPath.ClassName)
			{
				'Msvm_ComputerSystem' {
					$VMObject = $VM
				}
				'MSCluster_Resource' {
					$VMObject = Get-WmiObject -ComputerName $VM.ClassPath.Server -Namespace 'root\virtualization\v2' -Class 'Msvm_ComputerSystem' -Filter ('Name="{0}"' -f $VM.PrivateProprties.VmID) -ErrorAction Stop
				}
				default {
					$ArgEx = New-Object System.ArgumentException(('Cannot accept objects of type {0}' -f $VM.ClassPath.ClassName), 'VM')
					Write-Error -Exception $ArgEx
					return
				}
			}
		}

		'System.String' {
			if($VM -ne $ComputerName -and $VM -ne $env:COMPUTERNAME)
			{
				$VMObject = Get-WmiObject -ComputerName $ComputerName -Namespace 'root\virtualization\v2' -Class 'Msvm_ComputerSystem' -Filter ('ElementName="{0}"' -f $VM) -ErrorAction Stop | select -First 1
			}
		}

		default {
			$ArgEx = New-Object System.ArgumentException(('Unable to process objects of type {0}' -f $VM.GetType().FullName), 'VM')
			Write-Error -Exception $ArgEx
			return
		}
	}

	if($VMObject -eq $null)
	{
		$ArgEx = New-Object System.ArgumentException(('The specified virtual machine "{0}" could not be found' -f $VM), 'VM')
		Write-Error -Exception $ArgEx
		return
	}

	Write-Progress -Activity 'Gathering information' -Status ('Checking if {0} is replicating.' -f $VMObject.ElementName) -PercentComplete 10
	if($VMObject.ReplicationState -gt 0)
	{
		Write-Error -Message ('Replication is enabled for {0}. Please disable replication before deletion.' -f $VMObject.ElementName)
		return
	}

	$ComputerName = $VMObject.__SERVER
	Write-Progress -Activity 'Gathering information' -Status 'Loading virtual machine settings' -PercentComplete 20
	$RelatedVMSettings = $VMObject.GetRelated('Msvm_VirtualSystemSettingData') | select -Unique
	$VMSettings = $RelatedVMSettings | where -Property VirtualSystemType -eq 'Microsoft:Hyper-V:System:Realized'
	$Snapshots = $RelatedVMSettings | where -Property VirtualSystemType -ne 'Microsoft:Hyper-V:System:Realized'
	$HostIsRemote = $false
	$RemoteSession = $null
	if($env:COMPUTERNAME -ine ($ComputerName -replace '\..*', ''))
	{
		Write-Progress -Activity 'Gathering information' -Status ('Creating a PowerShell session on {0}' -f $ComputerName) -PercentComplete 30
		$HostIsRemote = $true
		$RemoteSession = New-PSSession -ComputerName $ComputerName -ErrorAction Stop
	}
	else
	{
		$ComputerName = '.'
	}

	Write-Progress -Activity 'Gathering information' -Status ('Determining if {0} is clustered.' -f $VMObject.ElementName) -PercentComplete 40
	# There is a KVP item that contains this information but parsing it out is obnoxious. Since we'll need the cluster resource info anyway, we'll just assume it's clustered and quietly proceed if it's not.
	if(Get-WmiObject -Computer $ComputerName -Namespace 'root' -Class '__NAMESPACE' -Filter 'Name="MSCluster"')
	{
		$ClusteredResources = Get-WmiObject -ComputerName $ComputerName -Namespace 'root\MSCluster' -Class 'MSCluster_Resource' -Filter ('PrivateProperties.VmID="{0}"' -f $VMObject.Name | select -First 1)
		if($ClusteredResources)
		{
			$VMClusterGroup = Get-WmiObject -ComputerName $ComputerName -Namespace 'root\MSCluster' -Class 'MSCluster_ResourceGroup' -Filter ('Name="{0}"' -f $ClusteredResources.OwnerGroup)
		}
	}
	$IsClustered = $VMClusterGroup -ne $null

	Write-Progress -Activity 'Gathering information' -Status 'Determining VM file locations' -PercentComplete 50
	$AllFoldersToScan = @()
	$FileList = @()
	$LocalFoldersToScan = @()
	$LocalFilesToRemove = @()
	$RemoteFoldersToScan = @()
	$RemoteFilesToRemove = @()

	foreach ($RelatedObject in $RelatedVMSettings)
	{
		$AllFoldersToScan += $RelatedObject.ConfigurationDataRoot
		$AllFoldersToScan += Join-Path -Path $RelatedObject.ConfigurationDataRoot -ChildPath 'Virtual Machines'
		if(-not ([String]::IsNullOrEmpty($RelatedObject.SnapshotDataRoot)))
		{
			$AllFoldersToScan += $RelatedObject.SnapshotDataRoot
			$AllFoldersToScan += Join-Path -Path $RelatedObject.SnapshotDataRoot -ChildPath 'Snapshots'
		}
		$AllFoldersToScan += $RelatedObject.SwapFileDataRoot
		$AllFoldersToScan += Join-Path -Path $RelatedObject.ConfigurationDataRoot -ChildPath $RelatedObject.SuspendDataRoot	# this is a delicious yet dirty hack that always yields the folder that contains the BIN and VSV files
	}
	$AllFoldersToScan = $AllFoldersToScan | select -Unique
	if($HostIsRemote)
	{
		foreach($FolderToScan in $AllFoldersToScan)
		{
			if($HostIsRemote -and $FolderToScan -imatch $FilterRemoteDisk)
			{
				$RemoteFoldersToScan += $FolderToScan
			}
			else
			{
				$LocalFoldersToScan += $FolderToScan
			}
		}
	}
	else
	{
		$LocalFoldersToScan = $AllFoldersToScan
	}

	if($LocalFoldersToScan)
	{
		Write-Progress -Activity 'Gathering information' -Status 'Gathering a list of files to delete' -PercentComplete 60
		$FileList = Invoke-Command -ScriptBlock $FileSearchScriptBlock -ArgumentList @(, $LocalFoldersToScan)
	}
	if($HostIsRemote -and $RemoteFoldersToScan)
	{
		Write-Progress -Activity 'Gathering information' -Status ('Connecting to {0} to gather a list of files to delete' -f $ComputerName) -PercentComplete 70
		$FileList += Invoke-Command -Session $RemoteSession -ScriptBlock $FileSearchScriptBlock -ArgumentList @(, $RemoteFoldersToScan)
	}

	foreach ($FoundFile in $FileList)
	{
		foreach ($InstanceIDField in $RelatedVMSettings.InstanceId)
		{
			if($InstanceIDField -match 'Microsoft:(.*)')
			{
				$InstanceID = $Matches[1]
				if($FoundFile.BaseName -imatch ('^{0}(\.|\z)' -f $InstanceID) -and $FoundFile.Extension -imatch 'xml|bin|vsv|slp')
				{
					if($HostIsRemote -and $FoundFile -imatch $FilterRemoteDisk)
					{
						$RemoteFilesToRemove += $FoundFile
					}
					else
					{
						$LocalFilesToRemove += $FoundFile
					}
				}
			}
		}
	}

	Write-Progress -Activity 'Gathering information' -Status 'Building a list of virtual hard disks that must be deleted' -PercentComplete 80
	foreach ($VHDResource in ($RelatedVMSettings.GetRelated() | where -Property ResourceSubType -eq 'Microsoft:Hyper-V:Virtual Hard Disk' -ErrorAction SilentlyContinue))
	{
		foreach ($VirtualHardDiskPath in $VHDResource.HostResource)
		{
			if($HostIsRemote -and $VirtualHardDiskPath -imatch $FilterRemoteDisk)
			{
				$RemoteFilesToRemove += $VirtualHardDiskPath
			}
			else
			{
				$LocalFilesToRemove += $VirtualHardDiskPath
			}
			$VirtualHardDiskParentPath = Split-Path -Path $VirtualHardDiskPath -Parent
			if($HostIsRemote -and $VirtualHardDiskParentPath -imatch $FilterRemoteDisk)
			{
				$RemoteFoldersToScan += $VirtualHardDiskParentPath
			}
			else
			{
				$LocalFoldersToScan += $VirtualHardDiskParentPath
			}
		}
	}

	Write-Progress -Activity 'Gathering information' -Status 'Building a list of virtual floppy disks and ISOs that must be deleted' -PercentComplete 90
	foreach ($RemovableResource in ($RelatedVMSettings.GetRelated() | where -Property ResourceSubType -match 'Microsoft:Hyper-V:Virtual (CD/DVD|Floppy) Disk' -ErrorAction SilentlyContinue))
	{
		foreach ($RemovableResourcePath in $RemovableResource.HostResource)
		{
			$RemovableResourceParentPath = Split-Path -Path $RemovableResourcePath -Parent
			foreach ($Folder in $AllFoldersToScan)
			{
				if($Folder -imatch ($RemovableResourceParentPath -replace '\\', '\\')) # RemovableResourceParentPath must be a sub of an already-found folder to be eligible for removal
				{
					if($HostIsRemote -and $RemovableResourcePath -imatch $FilterRemoteDisk)
					{
						$RemoteFilesToRemove += $RemovableResourcePath
					}
					else
					{
						$LocalFilesToRemove += $RemovableResourcePath
					}
				}
			}
		}
	}

	Write-Progress -Activity 'Gathering information' -Status 'Determining important folders' -PercentComplete 95
	$ImportantFolders = @()

	$VSMSSD = Get-WmiObject -ComputerName $ComputerName -Namespace 'root\virtualization\v2' -Class 'Msvm_VirtualSystemManagementServiceSettingData'
	$ImportantFolders += $VSMSSD.DefaultExternalDataRoot
	$ImportantFolders += $VSMSSD.DefaultVirtualHardDiskPath

	$OtherVMSettingsData = Get-WmiObject -ComputerName $ComputerName -Namespace 'root\virtualization\v2' -Class 'Msvm_VirtualSystemSettingData' -Filter ('VirtualSystemIdentifier != "{0}"' -f $VMObject.Name)
	foreach($VMSettingData in $OtherVMSettingsData)
	{
		$ImportantFolders += $VMSettingData.ConfigurationDataRoot
		$ImportantFolders += Join-Path -Path $VMSettingData.ConfigurationDataRoot -ChildPath 'Virtual Machines'
		if(-not [String]::IsNullOrEmpty($VMSettingData.SnapshotDataRoot))
		{
			$ImportantFolders += $VMSettingData.SnapshotDataRoot
			$ImportantFolders += Join-Path -Path $VMSettingData.SnapshotDataRoot -ChildPath 'Snapshots'
		}
		$ImportantFolders += $VMSettingData.SwapFileDataRoot
	}

	$AllVHDs = Get-WmiObject -ComputerName $ComputerName -Namespace 'root\virtualization\v2' -Class 'Msvm_StorageAllocationSettingData'
	foreach ($VHD in $AllVHDs)
	{
		foreach($VHDPath in $VHD.HostResource)
		{
			if($VHDPath -notin $LocalFilesToRemove -and $VHDPath -notin $RemoteFilesToRemove)
			{
				$ImportantFolders += Split-Path -Path $VHDPath -Parent
			}
		}
	}
	$ImportantFolders = $ImportantFolders | select -Unique
	$RemoteFoldersToScan = $RemoteFoldersToScan | where { $_ -notin $ImportantFolders }
	$LocalFoldersToScan = $LocalFoldersToScan | where { $_ -notin $ImportantFolders }

	Write-Progress -Activity 'Gathinering information' -Status 'Finalizing folder delete list' -PercentComplete 99
	$RemoteFoldersToScan = $RemoteFoldersToScan | select -Unique
	$LocalFoldersToScan = $LocalFoldersToScan | select -Unique
	$AllFoldersToScan = $RemoteFoldersToScan + $LocalFoldersToScan
	try	# because we might have an open PowerShell session and because PS has no goto, we must encap the rest in a t/c/f
	{
		Write-Progress -Activity 'Gathering information' -Status ('Verifying that {0} can accept operations' -f $VMObject.ElementName) -PercentComplete 100
		$VMObject.Get()	# would save a bit of processing time to do this earlier, but safer to wait until the last second in case something changes
		if($VMObject.EnabledState -notin @(2, 3, 6) -or $VMObject.OperationalStatus[0] -ne 2)
		{
			throw('The current state of virtual machine {0} does not allow operations.' -f $VMObject.ElementName)
		}
		Write-Progress -Activity 'Gathering information' -Completed

		if($WhatIf)
		{
			Write-Host -ForegroundColor ([System.ConsoleColor]::Yellow) -Object ('WhatIf: Deleting virtual machine {0} from {1}' -f $VMObject.ElementName, $VMObject.__SERVER)
		}
		if($WhatIf -or -not $Force)
		{
			if($VMObject.EnabledState -eq 2)
			{
				Write-Host -ForegroundColor ([System.ConsoleColor]::Yellow) -Object 'Stop virtual machine'
			}
			elseif($VMObject.EnabledState -eq 6)
			{
				Write-Host -ForegroundColor ([System.ConsoleColor]::Yellow) -Object 'Discard saved state'
			}
			if($ClusteredResources)
			{
				Write-Host -ForegroundColor ([System.ConsoleColor]::Yellow) -Object 'Remove virtual machine from the cluster'
			}
			if($RelatedVMSettings.GetType().BaseType.Name -eq 'Array')
			{
				Write-Host -ForegroundColor ([System.ConsoleColor]::Yellow) -Object 'Apply oldest snapshot (to avoid merges)'
				Write-Host -ForegroundColor ([System.ConsoleColor]::Yellow) -Object 'Destroy snapshots'
			}
			Write-Host -ForegroundColor ([System.ConsoleColor]::Yellow) -Object 'Destroy virtual machine'
			foreach ($FileName in $RemoteFilesToRemove)
			{
				Write-Host -ForegroundColor ([System.ConsoleColor]::Yellow) -Object ('If it is still present, delete file {0} from {1}' -f $FileName, $ComputerName)
			}
			foreach ($FileName in $LocalFilesToRemove)
			{
				Write-Host -ForegroundColor ([System.ConsoleColor]::Yellow) -Object ('If it is still present, delete file {0}' -f $FileName)
			}
			foreach($FolderName in $RemoteFoldersToScan)
			{
				Write-Host -ForegroundColor ([System.ConsoleColor]::Yellow) -Object ('If it is still present AND empty, delete folder {0} from {1}' -f $FolderName, $ComputerName)
			}
			foreach($FolderName in $LocalFoldersToScan)
			{
				Write-Host -ForegroundColor ([System.ConsoleColor]::Yellow) -Object ('If it is still present AND empty, delete folder {0}' -f $FolderName)
			}
		}
		if(-not $WhatIf -and ($Force -or $PSCmdlet.ShouldProcess($VMObject.ElementName, 'Purge')))
		{
			Write-Progress -Activity ('Deleting virtual machine {0}' -f $VMObject.ElementName) -Status 'Loading the virtual machine management service' -PercentComplete 10
			$VMMS = Get-WmiObject -ComputerName $ComputerName -Namespace 'root\virtualization\v2' -Class Msvm_VirtualSystemManagementService
			Write-Progress -Activity ('Deleting virtual machine {0}' -f $VMObject.ElementName) -Status 'Loading the virtual machine snapshot service' -PercentComplete 20
			$VMSS = Get-WmiObject -ComputerName $ComputerName -Namespace 'root\virtualization\v2' -Class Msvm_VirtualSystemSnapshotService
			if($VMMS -eq $null -or $VMSS -eq $null)
			{
				throw ('Could not access virtual machine management service on {0}' -f $ComputerName)
			}

			if($VMObject.EnabledState -in @(2, 6))
			{
				Write-Progress -Activity ('Deleting virtual machine {0}' -f $VMObject.ElementName) -Status 'Stopping the virtual machine' -PercentComplete 30
				$Result = $VMObject.RequestStateChange(3)
				$Outcome = Invoke-Command -ScriptBlock $ProcessWMIJobScriptBlock -ArgumentList @($Result, $VMObject.ClassPath, 'RequestStateChange')
				if ($Outcome)
				{
					throw ('Could not turn off/discard saved state for {0}. {1}' -f $VMObject.ElementName, $Outcome)
				}
			}

			if($IsClustered)
			{
				try
				{
					Write-Progress -Activity ('Deleting virtual machine {0}' -f $VMObject.ElementName) -Status 'Removing from the cluster' -PercentComplete 40
					$VMClusterGroup.DestroyGroup(2)
				}
				catch
				{
					throw ('Cannot remove {0} from cluster. {1}' -f $VMObject.ElementName, $_.Message)
				}
			}

			$OldestSnapshot = $null
			foreach($VMSettingData in $RelatedVMSettings)
			{
				if($VMSettingData.Parent -eq $null -and $VMSettingData.VirtualSystemType -eq 'Microsoft:Hyper-V:Snapshot:Realized')
				{
					$OldestSnapshot = $VMSettingData
				}
			}
			if ($OldestSnapshot -ne $null)
			{
				Write-Progress -Activity ('Deleting virtual machine {0}' -f $VMObject.ElementName) -Status 'Applying the oldest snapshot' -PercentComplete 50
				$Result = $VMSS.ApplySnapshot($OldestSnapshot)
				$Outcome = Invoke-Command -ScriptBlock $ProcessWMIJobScriptBlock -ArgumentList @($Result, $VMSS.ClassPath, 'ApplySnapshot')
				if($Outcome)
				{
					Write-Error -Message ('Unable to apply oldest snapshot to {0}. {1}' -f $VMObject.ElementName, $Outcome)
					if(-not $Force -and -not ($PSCmdlet.ShouldProcess($VMObject.ElementName, 'Merge all snapshots before deleting')))
					{
						throw('Unable to apply oldest snapshot. Merging all snapshots disallowed by user input.')
					}
				}

				Write-Progress -Activity ('Deleting virtual machine {0}' -f $VMObject.ElementName) -Status 'Destroying all snapshots' -PercentComplete 60
				$Result = $VMSS.DestroySnapshotTree($OldestSnapshot)
				$Outcome = Invoke-Command -ScriptBlock $ProcessWMIJobScriptBlock -ArgumentList @($Result, $VMSS.ClassPath, 'DestroySnapshotTree')
				if($Outcome)
				{
					throw ('Unable to remove snapshot tree for {0}. {1}' -f $VMObject.ElementName, $Outcome)
				}
			}

			Write-Progress -Activity ('Deleting virtual machine {0}' -f $VMObject.ElementName) -Status 'Deleting the virtual machine' -PercentComplete 70
			$Result = $VMMS.DestroySystem($VMObject)
			$Outcome = Invoke-Command -ScriptBlock $ProcessWMIJobScriptBlock -ArgumentList @($Result, $VMMS.ClassPath, 'DestroySystem')
			if($Outcome)
			{
				throw ('Unable to delete {0}. {1}' -f $VMObject.ElementName, $Outcome)
			}

			Write-Progress -Activity ('Deleting virtual machine {0}' -f $VMObject.ElementName) -Status 'Deleting any surviving files' -PercentComplete 80
			if($LocalFilesToRemove)
			{
				Invoke-Command -ScriptBlock $FileOrFolderDeleteScriptBlock -ArgumentList @(, $LocalFilesToRemove)
			}
			if($HostIsRemote -and $RemoteFilesToRemove)
			{
				Invoke-Command -ScriptBlock $FileOrFolderDeleteScriptBlock -ArgumentList @(, $RemoteFilesToRemove) -ComputerName $ComputerName
			}

			Write-Progress -Activity ('Deleting virtual machine {0}' -f $VMObject.ElementName) -Status 'Deleting any surviving folders' -PercentComplete 90
			if($LocalFoldersToScan)
			{
				Invoke-Command -ScriptBlock $FileOrFolderDeleteScriptBlock -ArgumentList @(, ($LocalFoldersToScan | sort -Descending))
			}
			if($HostIsRemote -and $RemoteFoldersToScan)
			{
				Invoke-Command -ScriptBlock $FileOrFolderDeleteScriptBlock -ArgumentList @(, ($RemoteFoldersToScan | sort -Descending)) -ComputerName $ComputerName
			}
		} # delete process
	} # encapsulating try
	catch
	{
		Write-Error $_
	}
	finally
	{
		Write-Progress -Activity ('Deleting virtual machine {0}' -f $VMObject.ElementName) -Completed
		if($RemoteSession -ne $null)
		{
			Remove-PSSession -Session $RemoteSession
		}
	}
}
#}						# Uncomment this line to use as dot-sourced function or in a profile. Also the beginning two lines.

 

Altaro Hyper-V Backup
Share this post

Not a DOJO Member yet?

Join thousands of other IT pros and receive a weekly roundup email with the latest content & updates!

10 thoughts on "Free Hyper-V Script: Completely Delete a Virtual Machine"

  • Mark says:

    Thanks for this but I’m getting 2 errors when I try to use it that cause it to fail. This is on Hyper-V 2012 R2:

    PS C:Powershell> ./clear-vm.ps1 vm-to-be-deleted
    Get-ChildItem : Cannot find path ‘E:hypervvirtual machinesvm-to-be-deletedSnapshots’ because it does not exist.
    At C:PowershellClear-VM.ps1:77 char:3
    Get-ChildItem -Path $FoldersToScan -File
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    CategoryInfo : ObjectNotFound: (E:hypervvirtu…nhoggSnapshots:String) [Get-ChildItem], ItemNotFound
    Exception
    FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand

    C:PowershellClear-VM.ps1 : The variable ‘$ClusteredResources’ cannot be retrieved because it has not been set.
    At line:1 char:1
    ./clear-vm.ps1 vm-to-be-deleted
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
    FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Clear-VM.ps1

    Any idea what is causing these?
    Thanks, Mark.

    • Eric Siron says:

      I will check the script and update you. I’m guessing that this particular VM is not clustered? I’ll need to know that when I check the logic.

      • Mark says:

        Thanks. You are correct, it is not clustered.

        • Eric Siron says:

          Hi Mark,
          I did a code review of the offending sections and did not find anything overtly wrong.
          I modified the way that it performs the checks in question to be somewhat more accommodating. If you haven’t already manually deleted the VM, please try again and let me know what you discover.

          • Mark says:

            Thanks Eric. I still got the first error about the snapshots directory but not the second one. I ran it with whatif first, then normally and it deleted my VM ok. Might I suggest, given the destructive nature of the script, that you change the default action to the Confirm prompt to be L (no to all) rather than Y so that a double-return key press does not result in an accidental deletion. Thanks for this helpful script, saves having to track down all the less obvious files involved in a VM.
            Mark.

          • Eric Siron says:

            Hmm, there’s only one other place that the error might be thrown, which I didn’t consider because that would mean that constituent VM folders created by the system are wrong. I can see where that would actually happen. If that’s what happened to you, then v1.2 would report it better, but nothing else would be different.

            I’m not opposed to the idea of changing the default selection, but I don’t know that it’s possible change the way that PowerShell presents the confirmations.

  • Mark says:

    Thanks for this but I’m getting 2 errors when I try to use it that cause it to fail. This is on Hyper-V 2012 R2:

    PS C:Powershell> ./clear-vm.ps1 vm-to-be-deleted
    Get-ChildItem : Cannot find path ‘E:hypervvirtual machinesvm-to-be-deletedSnapshots’ because it does not exist.
    At C:PowershellClear-VM.ps1:77 char:3
    Get-ChildItem -Path $FoldersToScan -File
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    CategoryInfo : ObjectNotFound: (E:hypervvirtu…nhoggSnapshots:String) [Get-ChildItem], ItemNotFound
    Exception
    FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand

    C:PowershellClear-VM.ps1 : The variable ‘$ClusteredResources’ cannot be retrieved because it has not been set.
    At line:1 char:1
    ./clear-vm.ps1 vm-to-be-deleted
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
    FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Clear-VM.ps1

    Any idea what is causing these?
    Thanks, Mark.

  • Aurelien says:

    Super, bon j’ai juste perdu ma VM mais désormais j’ai de la place ! Merci

Leave a comment or ask a question

Your email address will not be published. Required fields are marked *

Your email address will not be published. Required fields are marked *

Notify me of follow-up replies via email

Yes, I would like to receive new blog posts by email

What is the color of grass?

Please note: If you’re not already a member on the Dojo Forums you will create a new account and receive an activation email.