Public/Restore-PVEBackup.ps1

function Restore-PVEBackup {
  <#
  .SYNOPSIS
  Restores a VM or container from a backup.
  .DESCRIPTION
  Restores with POST /nodes/{node}/qemu (archive=<backup>) for a VM backup, or POST /nodes/{node}/lxc
  (ostemplate=<backup>, restore=1) for a container backup, waits for the task, and writes the restored
  guest with its live status. Whether the backup is of a VM or a container comes from -GuestType, which
  binds from piped PVEPS.StorageContent objects; otherwise it is looked up in the storage's backup list.

  The guest is restored as -VMId. Restoring over an existing guest needs -Force and replaces that guest,
  disks included; that cannot be undone. Prompts for confirmation by default (ConfirmImpact High).
  .PARAMETER Volume
  Backup volume ID, for example local:backup/vzdump-qemu-100-2026_10_17-02_00_00.vma.zst. Binds from
  piped PVEPS.StorageContent objects.
  .PARAMETER VMId
  ID to restore as. Binds from piped objects, so a piped backup restores to its original ID unless
  -VMId is given.
  .PARAMETER GuestType
  qemu or lxc. Binds from piped objects; looked up if omitted.
  .PARAMETER Storage
  Storage for the restored disks. PVE restores to the original storages if omitted.
  .PARAMETER Pool
  Resource pool to add the restored guest to. The API documentation lets VM.Allocate on the pool stand
  in for VM.Allocate on the guest, so a user whose rights are on a pool needs this to restore a new
  guest.
  .PARAMETER Force
  Replace an existing guest with this ID.
  .PARAMETER Unique
  Give the restored guest new random MAC addresses, for example when the original still runs.
  .PARAMETER PowerOn
  Start the guest once it has been restored.
  .PARAMETER Node
  Node to restore on. Binds from piped objects; can be omitted on a single-node server.
  .PARAMETER RunAsync
  Return the task at once instead of waiting.
  .PARAMETER Server
  Connection to use. Defaults to the default connection; binds from piped objects.
  .EXAMPLE
  Get-PVEBackup -VMId 100 -Latest | Restore-PVEBackup -VMId 150 -Unique
  .EXAMPLE
  Get-PVEBackup -VMId 100 -Latest | Restore-PVEBackup -Force -Confirm:$false
  #>

  [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
  [OutputType('PVEPS.VM', 'PVEPS.Container', 'PVEPS.Task')]
  param(
    [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)]
    [ValidatePattern('^[^:]+:.+')]
    [string]$Volume,

    [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
    [ValidateRange(100, 999999999)]
    [int]$VMId,

    [Parameter(ValueFromPipelineByPropertyName)]
    [ValidateSet('qemu', 'lxc')]
    [string]$GuestType,

    [ValidateNotNullOrEmpty()]
    [string]$Storage,

    [ValidateNotNullOrEmpty()]
    [string]$Pool,

    [switch]$Force,

    [switch]$Unique,

    [switch]$PowerOn,

    [Parameter(ValueFromPipelineByPropertyName)]
    [ValidateNotNullOrEmpty()]
    [string]$Node,

    [switch]$RunAsync,

    [Parameter(ValueFromPipelineByPropertyName)]
    [Alias('Connection')]
    [object]$Server
  )

  process {
    try {
      $connection = Resolve-PVESingleConnection -Server $Server -Reason 'a backup is restored on exactly one'
      $nodeName = Resolve-PVENodeName -Connection $connection -Node $Node
      $type = $GuestType
      $entry = $null
      if (-not $type) {
        $backupStorage = $Volume.Substring(0, $Volume.IndexOf(':'))
        $entry = Get-PVEStorageItem -Connection $connection -Node $nodeName -Storage $backupStorage -Content backup | Where-Object { $_.Volume -eq $Volume }
      }
    }
    catch {
      $PSCmdlet.WriteError($_)
      return
    }
    if (-not $type) {
      if (-not $entry) {
        $PSCmdlet.WriteError([System.Management.Automation.ErrorRecord]::new(
            [System.Management.Automation.ItemNotFoundException]::new("Backup '$Volume' was not found on $backupStorage on $nodeName."),
            'PVEObjectNotFound',
            [System.Management.Automation.ErrorCategory]::ObjectNotFound,
            $Volume))
        return
      }
      $type = $entry.GuestType
      if (-not $type) {
        $PSCmdlet.WriteError((New-PVEArgumentError -Message "Cannot tell whether '$Volume' is a VM or a container backup; give -GuestType." -Target $Volume))
        return
      }
    }

    $kind = if ($type -eq 'lxc') { 'container' } else { 'VM' }
    $action = '{0}Restore {1} {2} from {3}' -f $(if ($Force) { 'Replace and ' } else { '' }), $kind, $VMId, $Volume
    if (-not $PSCmdlet.ShouldProcess("$kind $VMId on $nodeName", $action)) {
      return
    }

    $body = @{ vmid = $VMId }
    if ($type -eq 'lxc') {
      $body.ostemplate = $Volume
      $body.restore = 1
    }
    else {
      $body.archive = $Volume
    }
    if ($Storage) { $body.storage = $Storage }
    if ($Pool) { $body.pool = $Pool }
    if ($Force) { $body.force = 1 }
    if ($Unique) { $body.unique = 1 }
    if ($PowerOn) { $body.start = 1 }

    $path = Format-PVEPath '/nodes/{node}/{type}' @{ node = $nodeName; type = $type }
    try {
      $upid = [string](Invoke-PVERequest -Connection $connection -Method POST -Path $path -Parameters $body)
    }
    catch {
      $PSCmdlet.WriteError($_)
      return
    }
    $operations = [System.Collections.Generic.List[psobject]]::new()
    $operations.Add([pscustomobject]@{ Upid = $upid; Connection = $connection; Guest = $null; Type = $type; Node = $nodeName; VMId = $VMId; Pool = $Pool })
    Complete-PVEOperation -Cmdlet $PSCmdlet -Operation $operations -RunAsync:$RunAsync
  }
}