Private/Invoke-PVEBackup.ps1

function Invoke-PVEBackup {
  <#
  .SYNOPSIS
  Shared engine for Backup-PVEVM and Backup-PVEContainer.
  .DESCRIPTION
  Asks ShouldProcess on the calling cmdlet for each guest, then starts one backup task per node with
  POST /nodes/{node}/vzdump, vmid being the approved guests' IDs as a comma-separated list (the schema's
  pve-vmid-list format). Waits for the tasks, then writes the backups each one created as
  PVEPS.StorageContent objects.

  The new backups are found by listing the backup content of the target storage (or, without -Storage,
  of every active storage on the node that holds backups) before and after: whatever is new for the
  guest's ID is its backup. This needs no comparison of clocks between this computer and the node.
  #>

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

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

    [Parameter(Mandatory)]
    [hashtable]$Body,

    [string]$Storage,

    [Parameter(Mandatory)]
    [string]$Description,

    [switch]$RunAsync
  )

  $operations = [System.Collections.Generic.List[psobject]]::new()
  foreach ($group in ($Guest | Group-Object -Property { '{0}/{1}' -f $_.Connection.Id, $_.Node })) {
    $connection = $group.Group[0].Connection
    $node = $group.Group[0].Node
    $approved = @($group.Group | Where-Object { $Cmdlet.ShouldProcess((Get-PVEGuestLabel -Guest $_), $Description) })
    if ($approved.Count -eq 0) {
      continue
    }

    try {
      $storages = if ($Storage) {
        @($Storage)
      }
      else {
        @(Get-PVEStorage -Node $node -Content backup -Server $connection -ErrorAction Stop | Where-Object Active | ForEach-Object { $_.Name })
      }
      $before = [System.Collections.Generic.HashSet[string]]::new()
      foreach ($name in $storages) {
        foreach ($item in @(Get-PVEStorageItem -Connection $connection -Node $node -Storage $name -Content backup)) {
          $null = $before.Add($item.Volume)
        }
      }
    }
    catch {
      $Cmdlet.WriteError($_)
      continue
    }

    $parameters = $Body.Clone()
    $parameters.vmid = @($approved | ForEach-Object { $_.VMId }) -join ','
    $path = Format-PVEPath '/nodes/{node}/vzdump' @{ node = $node }
    try {
      $upid = [string](Invoke-PVERequest -Connection $connection -Method POST -Path $path -Parameters $parameters)
    }
    catch {
      $Cmdlet.WriteError($_)
      continue
    }
    Write-Verbose "Backup of $($parameters.vmid) on $node`: task $upid"
    $operations.Add([pscustomobject]@{ Upid = $upid; Connection = $connection; Node = $node; Guests = $approved; Storages = $storages; Before = $before })
  }
  if ($operations.Count -eq 0) {
    return
  }

  if ($RunAsync) {
    foreach ($op in $operations) {
      if ($op.Upid -match '^UPID:') {
        try {
          Get-PVETask -Upid $op.Upid -Server $op.Connection -ErrorAction Stop
        }
        catch {
          $Cmdlet.WriteError($_)
        }
      }
    }
    return
  }

  foreach ($op in @(Wait-PVEOperation -Operation $operations)) {
    try {
      $new = @(
        foreach ($name in $op.Storages) {
          Get-PVEStorageItem -Connection $op.Connection -Node $op.Node -Storage $name -Content backup |
            Where-Object { -not $op.Before.Contains($_.Volume) }
        }
      )
    }
    catch {
      $Cmdlet.WriteError($_)
      continue
    }
    foreach ($member in $op.Guests) {
      $mine = @($new | Where-Object { $_.VMId -eq $member.VMId })
      if ($mine.Count -eq 0) {
        Write-Warning "$(Get-PVEGuestLabel -Guest $member): the backup task succeeded, but no new backup was found on $($op.Storages -join ', ')."
      }
      $mine
    }
  }
}