Private/Invoke-PVEGuestOperation.ps1

function Invoke-PVEGuestOperation {
  <#
  .SYNOPSIS
  Shared engine for lifecycle cmdlets that send one request per existing guest (Remove-*, Set-*,
  ConvertTo-*Template).
  .DESCRIPTION
  For each guest (already resolved by Resolve-PVEGuestTarget):
  1. With -SkipTemplate, a guest that is already a template is skipped with a warning and written
     unchanged.
  2. With -RequireStopped, reads the live status; a guest that is not stopped is an error (PVEGuestRunning)
     naming the cmdlet that stops it.
  3. Asks ShouldProcess on the calling cmdlet, then sends -Method to /nodes/{node}/{type}/{vmid} plus
     -Suffix, with -Parameters.
  4. Complete-PVEOperation waits for the tasks (endpoints that return no task count as finished) and
     writes the guests with their live status, or the tasks with -RunAsync, or nothing with -NoOutput.
  #>

  [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'Calls ShouldProcess on the calling public cmdlet, passed as -Cmdlet, so its -WhatIf and -Confirm apply.')]
  [CmdletBinding()]
  [OutputType([psobject])]
  param(
    [Parameter(Mandatory)]
    [System.Management.Automation.PSCmdlet]$Cmdlet,

    [Parameter(Mandatory)]
    [AllowEmptyCollection()]
    [psobject[]]$Guest,

    # Text for ShouldProcess and messages, for example 'Remove'.
    [Parameter(Mandatory)]
    [string]$Description,

    [Parameter(Mandatory)]
    [ValidateSet('POST', 'PUT', 'DELETE')]
    [string]$Method,

    # Appended to /nodes/{node}/{type}/{vmid}, for example '/template'. Empty for the guest itself.
    [AllowEmptyString()]
    [string]$Suffix = '',

    [hashtable]$Parameters,

    [switch]$RequireStopped,

    [switch]$SkipTemplate,

    [switch]$RunAsync,

    [switch]$NoOutput
  )

  $operations = [System.Collections.Generic.List[psobject]]::new()

  foreach ($item in $Guest) {
    $label = Get-PVEGuestLabel -Guest $item

    if ($SkipTemplate -and $item.IsTemplate) {
      Write-Warning "$label is already a template; '$Description' skipped."
      $item
      continue
    }

    if ($RequireStopped) {
      try {
        $null = Update-PVEGuestStatus -Guest $item
      }
      catch {
        $Cmdlet.WriteError($_)
        continue
      }
      if ($item.Status -ne 'stopped') {
        $stopCommand = if ($item.Type -eq 'lxc') { 'Stop-PVEContainerGuest or Stop-PVEContainer' } else { 'Stop-PVEVMGuest or Stop-PVEVM' }
        $Cmdlet.WriteError([System.Management.Automation.ErrorRecord]::new(
            [System.InvalidOperationException]::new("$label is $($item.Status); stop it first with $stopCommand."),
            'PVEGuestRunning',
            [System.Management.Automation.ErrorCategory]::InvalidOperation,
            $item))
        continue
      }
    }

    if (-not $Cmdlet.ShouldProcess($label, $Description)) {
      continue
    }

    $path = (Format-PVEPath '/nodes/{node}/{type}/{vmid}' @{ node = $item.Node; type = $item.Type; vmid = $item.VMId }) + $Suffix
    try {
      $result = Invoke-PVERequest -Connection $item.Connection -Method $Method -Path $path -Parameters $Parameters
    }
    catch {
      $Cmdlet.WriteError($_)
      continue
    }
    Write-Verbose "$Description $label`: $(if ($result) { "task $result" } else { 'done' })"
    $operations.Add([pscustomobject]@{ Upid = [string]$result; Connection = $item.Connection; Guest = $item })
  }

  Complete-PVEOperation -Cmdlet $Cmdlet -Operation $operations -RunAsync:$RunAsync -NoOutput:$NoOutput
}