Archived
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e601ce85c | ||
|
|
cbdb288bc3 |
@@ -1,221 +0,0 @@
|
|||||||
#!/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}."
|
|
||||||
Reference in new issue
Block a user