Archived
Check NixOS configurations / eval-hosts (pull_request) Successful in 10m21s
Backs up a VM/CT on pve1 (snapshot mode by default, so the source stays online), relays the archive to pve-test, restores it there with fresh MAC addresses (--unique), and deletes both the source and relayed backup copies afterward -- no ad hoc backup files left behind on either node. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
222 lines
9.0 KiB
Bash
Executable File
222 lines
9.0 KiB
Bash
Executable File
#!/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 <<EOF
|
|
Usage: $0 --vmid <n> [options]
|
|
|
|
--vmid <n> Required: VMID on the source node to clone.
|
|
Kind (qemu VM vs LXC CT) is auto-detected.
|
|
--new-vmid <n> 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 <host> (default: \$PVE1_HOST, ${PVE1_HOST})
|
|
--target-node <host> (default: \$PVE_TEST_HOST, ${PVE_TEST_HOST})
|
|
--source-storage <pool> Where vzdump writes the backup on the
|
|
source node (default: local).
|
|
--target-storage <pool> 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}."
|