#!/usr/bin/env bash # Ad hoc clone of a single VM/CT from pve1 (production) to pve-test # (sandbox), via vzdump + qmrestore/pct restore -- not a general-purpose # backup tool, just a quick "give me a disposable copy of this thing on # pve-test" for testing against real-ish data without touching prod. # # Flow: # 1. vzdump the resource on pve1 into its "local" storage (--mode # snapshot by default, so the source keeps running throughout -- # see --mode below for when that's not possible). # 2. Stream the resulting archive straight from pve1 to pve-test # (ssh pve1 cat ... | ssh pve-test cat > ...) -- this machine is # just the relay, no separate on-disk staging copy here. # 3. qmrestore / pct restore it on pve-test under --new-vmid (default: # same VMID as the source -- pve-test is a separate node/cluster, so # no collision unless that VMID is already in use there too). # Always restored with --unique 1 (fresh MAC addresses) since the # source is typically still running on the same LAN -- restoring # with the *same* MAC would put two live guests on the wire with # identical hardware addresses. # 4. Delete the vzdump archive from pve1's local storage and the # relayed copy on pve-test, so neither node accumulates ad hoc # backup files from this script. Only the pve1 original is # preserved on any failure after step 1, so a failed # transfer/restore can be retried without re-running the backup. # # This script's own defaults are pve1 -> pve-test, unlike # create-proxmox-resource.sh's --node (which defaults to production) -- # see CLAUDE.md's "Two Proxmox nodes" section. pve1 is only ever touched # here after typing the source VMID back to confirm; pve-test is treated # as disposable, matching this repo's usual policy for that node. # # See --help for the full option list. set -euo pipefail repo_root="$(cd "$(dirname "$0")/../.." && pwd)" # shellcheck source=../env.sh source "${repo_root}/scripts/env.sh" # shellcheck source=../lib/confirm.sh source "${repo_root}/scripts/lib/confirm.sh" usage() { cat < [options] --vmid Required: VMID on the source node to clone. Kind (qemu VM vs LXC CT) is auto-detected. --new-vmid VMID to restore as on the target node (default: same as --vmid). --mode snapshot|suspend|stop vzdump backup mode (default: snapshot -- the source resource keeps running throughout; requires snapshot-capable storage, e.g. ZFS/LVM-thin/Ceph/qcow2). Fall back to "suspend" (brief pause) or "stop" (source goes down for the duration) if the source's storage doesn't support live snapshots -- vzdump's own error will say so. --source-node (default: \$PVE1_HOST, ${PVE1_HOST}) --target-node (default: \$PVE_TEST_HOST, ${PVE_TEST_HOST}) --source-storage Where vzdump writes the backup on the source node (default: local). --target-storage Where the restored disk/rootfs lands on the target node (default: \$PROXMOX_STORAGE, ${PROXMOX_STORAGE}). --keep-backup Don't delete the vzdump archive from either node afterward (debugging aid). --yes Skip the typed VMID confirmation before touching the source node. --dry-run Print the full plan and skip every mutating step (vzdump, transfer, restore, delete) and the confirm prompt. Still makes read-only SSH calls to look up the source kind and check the target VMID is free -- harmless on either node. -h, --help EOF } vmid="" new_vmid="" mode="snapshot" source_node="$PVE1_HOST" target_node="$PVE_TEST_HOST" source_storage="local" target_storage="$PROXMOX_STORAGE" keep_backup=0 skip_confirm=0 dry_run=0 while [[ $# -gt 0 ]]; do case "$1" in --vmid) vmid="$2"; shift 2 ;; --new-vmid) new_vmid="$2"; shift 2 ;; --mode) mode="$2"; shift 2 ;; --source-node) source_node="$2"; shift 2 ;; --target-node) target_node="$2"; shift 2 ;; --source-storage) source_storage="$2"; shift 2 ;; --target-storage) target_storage="$2"; shift 2 ;; --keep-backup) keep_backup=1; shift ;; --yes) skip_confirm=1; shift ;; --dry-run) dry_run=1; shift ;; -h | --help) usage; exit 0 ;; *) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;; esac done if [[ -z "$vmid" ]]; then echo "ERROR: --vmid is required." >&2 usage >&2 exit 1 fi if [[ "$mode" != "snapshot" && "$mode" != "suspend" && "$mode" != "stop" ]]; then echo "ERROR: --mode must be snapshot, suspend, or stop." >&2 exit 1 fi [[ -z "$new_vmid" ]] && new_vmid="$vmid" source_target="${PROXMOX_SSH_USER}@${source_node}" target_target="${PROXMOX_SSH_USER}@${target_node}" # No dry-run wrapper needed for the calls below: every mutating step # (vzdump, transfer, restore, delete) is reached only after the --dry-run # early-exit further down, so a plain `ssh` call is never in the dry-run # path. # --- identify the resource kind on the source node ----------------------- echo "==> Looking up VMID ${vmid} on ${source_node}..." kind="" if ssh "$source_target" "qm status ${vmid}" >/dev/null 2>&1; then kind="vm" elif ssh "$source_target" "pct status ${vmid}" >/dev/null 2>&1; then kind="lxc" else echo "ERROR: VMID ${vmid} doesn't exist on ${source_node} as either a VM or CT." >&2 exit 1 fi echo "VMID ${vmid} on ${source_node} is a ${kind}." # --- refuse to clobber an existing resource on the target node ----------- if ssh "$target_target" "qm status ${new_vmid}" >/dev/null 2>&1 \ || ssh "$target_target" "pct status ${new_vmid}" >/dev/null 2>&1; then echo "ERROR: VMID ${new_vmid} already exists on ${target_node}. Pass --new-vmid" >&2 echo "with a free ID, or remove the existing resource there first." >&2 exit 1 fi echo echo "Plan:" echo " source: ${kind} VMID ${vmid} on ${source_node} (storage: ${source_storage}, mode: ${mode})" echo " target: VMID ${new_vmid} on ${target_node} (storage: ${target_storage}, fresh MAC via --unique)" [[ "$keep_backup" -eq 1 ]] && echo " backup archives are kept on both nodes afterward (--keep-backup)" if [[ "$dry_run" -eq 1 ]]; then echo echo "[dry-run] No backup, transfer, restore, or delete was performed." exit 0 fi if [[ "$skip_confirm" -ne 1 ]]; then echo if ! confirm_typed "$vmid" "Type the source VMID (${vmid}) to confirm backing it up from ${source_node}: "; then echo "Cancelled -- input didn't match ${vmid}." >&2 exit 1 fi fi # --- vzdump on the source node -------------------------------------------- echo echo "==> Backing up VMID ${vmid} on ${source_node} (mode=${mode}, storage=${source_storage})..." vzdump_log="$(ssh "$source_target" \ "vzdump ${vmid} --mode ${mode} --storage ${source_storage} --compress zstd" 2>&1)" \ || { echo "$vzdump_log" >&2 echo "ERROR: vzdump failed on ${source_node}." >&2 exit 1 } echo "$vzdump_log" archive="$(echo "$vzdump_log" | grep -oP "creating vzdump archive '\K[^']+" | tail -n1)" if [[ -z "$archive" ]]; then echo "ERROR: couldn't find the archive path in vzdump's output above." >&2 exit 1 fi archive_basename="$(basename "$archive")" target_tmp_archive="/var/tmp/${archive_basename}" echo "Archive: ${archive}" # Always clean up the relayed copy on the target node, success or failure # -- it's only ever a working copy, restored or not. cleanup_target_tmp() { if [[ "$keep_backup" -ne 1 ]]; then ssh "$target_target" "rm -f '${target_tmp_archive}'" >/dev/null 2>&1 || true fi } trap cleanup_target_tmp EXIT # --- relay the archive from source to target ------------------------------ echo echo "==> Transferring archive to ${target_node}..." ssh "$source_target" "cat '${archive}'" | ssh "$target_target" "cat > '${target_tmp_archive}'" # --- restore on the target node -------------------------------------------- echo echo "==> Restoring as VMID ${new_vmid} on ${target_node} (storage=${target_storage})..." if [[ "$kind" == "vm" ]]; then ssh "$target_target" "qmrestore '${target_tmp_archive}' ${new_vmid} --storage ${target_storage} --unique 1" else ssh "$target_target" "pct restore ${new_vmid} '${target_tmp_archive}' --storage ${target_storage} --unique 1" fi # --- clean up the source backup now that the restore succeeded ----------- if [[ "$keep_backup" -ne 1 ]]; then echo echo "==> Deleting backup archive from ${source_node}'s ${source_storage} storage..." ssh "$source_target" "rm -f '${archive}' '${archive}.notes' '${archive}.log'" >/dev/null 2>&1 || true fi echo echo "Done. VMID ${new_vmid} (${kind}) is now on ${target_node}, cloned from" \ "VMID ${vmid} on ${source_node}."