Private/Invoke-PVEVolumeMove.ps1

function Invoke-PVEVolumeMove {
  <#
  .SYNOPSIS
  Shared engine for Move-PVEVM and Move-PVEContainer: moves guest volumes to another storage.
  .DESCRIPTION
  For each guest, lists its storage-backed volumes (Get-PVEGuestVolume), keeps those named in -Key (all
  if -Key is omitted) that are not already on -Storage, asks ShouldProcess on the calling cmdlet once
  for the guest, then moves the volumes one at a time, waiting for each task before starting the next.
  A failed move stops the remaining moves for that guest. Each guest is written with its live status
  once its moves are done (or if it had nothing to move).

  QEMU: POST /nodes/{node}/qemu/{vmid}/move_disk. LXC: POST /nodes/{node}/lxc/{vmid}/move_volume.
  #>

  [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,

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

    [string[]]$Key,

    [switch]$DeleteSource,

    [string]$Format
  )

  foreach ($item in $Guest) {
    $label = Get-PVEGuestLabel -Guest $item
    try {
      $volumes = @(Get-PVEGuestVolume -Guest $item)
    }
    catch {
      $Cmdlet.WriteError($_)
      continue
    }

    if ($Key) {
      $missing = @($Key | Where-Object { $_ -notin @($volumes | ForEach-Object { $_.Key }) })
      foreach ($name in $missing) {
        $Cmdlet.WriteError([System.Management.Automation.ErrorRecord]::new(
            [System.Management.Automation.ItemNotFoundException]::new("$label has no storage volume '$name'. Volumes: $(@($volumes | ForEach-Object { $_.Key }) -join ', ')."),
            'PVEVolumeNotFound',
            [System.Management.Automation.ErrorCategory]::ObjectNotFound,
            $name))
      }
      if ($missing.Count -gt 0) {
        continue
      }
      $volumes = @($volumes | Where-Object { $_.Key -in $Key })
    }

    $toMove = @($volumes | Where-Object { $_.Storage -ne $Storage })
    if ($toMove.Count -eq 0) {
      Write-Warning "$label has no volumes to move; they are already on $Storage."
      try {
        Update-PVEGuestStatus -Guest $item
      }
      catch {
        $Cmdlet.WriteError($_)
      }
      continue
    }

    $keys = @($toMove | ForEach-Object { $_.Key }) -join ', '
    $action = 'Move {0} to storage {1}{2}' -f $keys, $Storage, $(if ($DeleteSource) { ', deleting the source volumes' } else { '' })
    if (-not $Cmdlet.ShouldProcess($label, $action)) {
      continue
    }

    $failed = $false
    foreach ($volume in $toMove) {
      if ($item.Type -eq 'lxc') {
        $path = Format-PVEPath '/nodes/{node}/lxc/{vmid}/move_volume' @{ node = $item.Node; vmid = $item.VMId }
        $body = @{ volume = $volume.Key; storage = $Storage }
      }
      else {
        $path = Format-PVEPath '/nodes/{node}/qemu/{vmid}/move_disk' @{ node = $item.Node; vmid = $item.VMId }
        $body = @{ disk = $volume.Key; storage = $Storage }
        if ($Format) {
          $body.format = $Format
        }
      }
      if ($DeleteSource) {
        $body.delete = 1
      }
      try {
        $upid = [string](Invoke-PVERequest -Connection $item.Connection -Method POST -Path $path -Parameters $body)
      }
      catch {
        $Cmdlet.WriteError($_)
        $failed = $true
        break
      }
      Write-Verbose "Move $($volume.Key) of $label to $Storage`: task $upid"
      if ($upid -match '^UPID:') {
        $finished = @([pscustomobject]@{ Upid = $upid; Connection = $item.Connection } | Wait-PVETask)
        if ($finished.Count -eq 0) {
          $failed = $true
          break
        }
      }
    }
    if ($failed) {
      continue
    }

    try {
      Update-PVEGuestStatus -Guest $item
    }
    catch {
      $Cmdlet.WriteError($_)
    }
  }
}