public/Remove-LablyVM.ps1

Function Remove-LablyVM {

    <#
     
    .SYNOPSIS
 
    Remove a VM from Lably and from Hyper-V.
 
    .DESCRIPTION
 
    This function is used to remove a VM from Lably and from Hyper-V.
 
    .PARAMETER Path
     
    Optional parameter to define where the lably that this VM is a member of is stored. If this parameter is not defined, it will default to the path from which the function was called.
 
    .PARAMETER DisplayName
 
    Display Name of the VM to be removed. Either this or the VMID parameter is required. This parameter supports auto-complete, you can tab through options or use CTRL+SPACE to view all options.
 
    .PARAMETER VMID
 
    Lably ID of the VM to be removed. Either this or the DisplayName parameter is required.
 
    .PARAMETER Confirm
 
    Optional Switch to bypass confirming that you want to delete the Virtual Machine.
 
    .INPUTS
 
    None. You cannot pipe objects to Remove-LablyVM.
 
    .OUTPUTS
 
    None. The function will either complete successfully or throw an error.
     
    .EXAMPLE
 
    Remove-LablyVM -DisplayName "[Chris' Lab] LABDC01"
 
    .EXAMPLE
 
    Remove-LablyVM -VMID 717b54e6-a50a-480e-8a3f-9f21ab2e08e9
 
    #>


    [CmdLetBinding(DefaultParameterSetName='DisplayName')]
    Param(
        [Parameter(Mandatory=$False)]    
        [String]$Path = $PWD,

        [Parameter(Mandatory=$True,ParameterSetName="DisplayName",Position=0)]
        [String]$DisplayName,

        [Parameter(Mandatory=$True,ParameterSetName='VMID',Position=0)]
        [String]$VMId,

        [Parameter(Mandatory=$False)]
        [Switch]$Confirm
    )

    ValidateModuleRun -RequiresAdministrator

    $LablyScaffold = Join-Path $Path -ChildPath "scaffold.lably.json"
    Write-Verbose "Reading Lably Scaffolding File at $LablyScaffold"

    If(-Not(Test-Path $LablyScaffold -ErrorAction SilentlyContinue)){
        Throw "There is no Lably at $Path."
    }

    Try {
        $Scaffold = Get-Content $LablyScaffold | ConvertFrom-Json
    } Catch {
        Throw "Unable to import Lably scaffold. $($_.Exception.Message)"
    }

    $Asset = $Scaffold.Assets | Where-Object { $_.DisplayName -eq $DisplayName -or $_.VMid -eq $VMID } | Select-Object -First 1
       
    If(-Not($Asset)) {
        Throw "Cannot find defined VM in this Lably."
    }

    Try {
        $VM = Get-VM -Id $Asset.VMId -ErrorAction Stop
    } Catch {
        Write-Warning "Warning: Cannot Find VM, Skipping. $($_.Exception.Message)"
    }

    If($VM) {
        
        If(-Not($Confirm)) {
            Write-Host "You are about to delete $($VM.Name)"
            Write-Host "This operation cannot be undone." -ForegroundColor Red
            Write-Host ""
            If($(Read-Host "If you're certain you'd like to continue, type DELETE and press enter") -ne "DELETE") {
                Write-Host "Code 'DELETE' was not entered. Aborting." -ForegroundColor Yellow
                Return
            }
        }

        Write-Host "Stopping VM"
        Try {
            $VM | Stop-VM -Force -TurnOff -ErrorAction Stop -WarningAction SilentlyContinue
        } Catch {
            Throw "Could not stop VM. $($_.Exception.Message)"
        }

        Write-Verbose "State of $($VM.Name) is $($VM.State). Status is $($VM.Status)."

        While($VM.State -ne [Microsoft.HyperV.PowerShell.VMState]::Off -and $VM.Status -ne "Operating Normally") {
            Write-Verbose "Waiting for VM to Stop (State=$($VM.State) Status=$($VM.Status))..."
            Start-Sleep -Seconds 1
        }

        $ActivityStart = Get-Date
        $RunTime = New-TimeSpan -Start $ActivityStart -End (Get-Date)
        While(@($VM | Get-VMHardDiskDrive | Select-Object -ExpandProperty Path) -like "*.avhdx" -or $RunTime.TotalMinutes -ge 1) {
            Write-Verbose "Waiting for Disk Merging Activities to Complete ..."
            Start-Sleep -Seconds 1
            $RunTime = New-TimeSpan -Start $ActivityStart -End (Get-Date)
        }

        $VHDPaths = @()

        $VM | Get-VMHardDiskDrive | ForEach-Object { 
            Write-Host "Deleting $($_.Path) ..."
        
            $VHDPaths += Split-Path $_.Path

            $AttemptNumber = 0
            $MaxAttempts = 5

            Do {
                Try {
                    $FileDeleteSuccess = $False
                    $AttemptNumber++
                    Remove-Item $($_.Path) -Force -ErrorAction Stop
                    $FileDeleteSuccess = $True
                } Catch {
                    # In edge cases, VHDx is still merging even though status doesn't seem to show that's the case. The file
                    # appears to be held in use for an extra second or so.
                    Write-Warning "Could not delete $($_.Path) (Attempt $AttemptNumber of $MaxAttempts)."
                    If($Attempt -eq $MaxAttempt) { Write-Warning $($_.Exception.Message) }
                    Start-Sleep -Seconds 5
                }    
            } Until ($FileDeleteSuccess -or $AttemptNumber -eq $MaxAttempts)
        }

        Write-Host "Removing VM $($VM.Name)"

        Try {
            $VM | Remove-VM -Force
        } Catch {
            Write-Warning "Could not delete $($VM.Name). $($_.Exception.Message)"            
        }      
    }
    
    ForEach($VHDPath in $VHDPaths) {
        Write-Verbose "Clearing $VHDPath (If Empty)"
        If(-Not (Get-ChildItem $VHDPath | Select-Object -First 1)) { Remove-Item $VHDPath -ErrorAction SilentlyContinue }
    }

    Try {
        $Scaffold = Get-Content $LablyScaffold | ConvertFrom-Json
        $Scaffold.Assets = $Scaffold.Assets | Where-Object { $_.VMId -ne $Asset.VMid }
        $Scaffold | ConvertTo-Json -Depth 10 | Out-File $LablyScaffold -Force
    } Catch {
        Write-Warning "VM is removed but we were unable to remove it from your Lably Scaffold."
        Write-Warning $_.Exception.Message
    }

   Write-Host "Done."

}