#!/bin/bash # # restore-pve-lxc.sh # # Restores a Proxmox vzdump LXC backup into a plain LXC container on a # non-Proxmox Debian host (e.g. a Dell Wyse 3040 standby box). # # This does NOT try to auto-translate the Proxmox .conf file. Proxmox's # container config format doesn't map 1:1 onto upstream LXC's config syntax, # and auto-generating network/memory settings without review is a bad idea # on a box with 2GB RAM and 8GB eMMC. Instead, this script: # 1. Creates a fresh skeleton container via lxc-create (gets you a valid # base LXC config + rootfs directory structure) # 2. Wipes the skeleton rootfs and unpacks your Proxmox backup over it # 3. Prints out anything useful it can find in the backup (hostname, # original network config) so you can fill in the LXC config yourself # 4. Leaves the container STOPPED so you review before first boot # # Usage: # ./restore-pve-lxc.sh -n -b [options] # # Required: # -n NAME Container name (e.g. adguard01) # -b BACKUP Path to vzdump backup file (.tar.zst / .tar.gz / .tar.lzo) # # Optional: # -d DIST Skeleton distro for lxc-create (default: debian) # -r RELEASE Skeleton release (default: bookworm) # -a ARCH Skeleton arch (default: amd64) # -f Force: destroy and recreate if container already exists # -h Show this help # # Example: # ./restore-pve-lxc.sh -n adguard01 -b /mnt/backups/vzdump-adguard01.tar.zst # set -euo pipefail DIST="debian" RELEASE="bookworm" ARCH="amd64" FORCE=0 NAME="" BACKUP="" usage() { sed -n '2,33p' "$0" | sed 's/^# \{0,1\}//' exit 1 } while getopts "n:b:d:r:a:fh" opt; do case "$opt" in n) NAME="$OPTARG" ;; b) BACKUP="$OPTARG" ;; d) DIST="$OPTARG" ;; r) RELEASE="$OPTARG" ;; a) ARCH="$OPTARG" ;; f) FORCE=1 ;; h) usage ;; *) usage ;; esac done # --- Validation --- if [[ -z "$NAME" || -z "$BACKUP" ]]; then echo "ERROR: -n (name) and -b (backup file) are required." >&2 usage fi if [[ $EUID -ne 0 ]]; then echo "ERROR: must be run as root (LXC container management needs root)." >&2 exit 1 fi if [[ ! -f "$BACKUP" ]]; then echo "ERROR: backup file not found: $BACKUP" >&2 exit 1 fi if ! command -v lxc-create >/dev/null 2>&1; then echo "ERROR: lxc-create not found. Install with: apt install lxc" >&2 exit 1 fi CONTAINER_DIR="/var/lib/lxc/${NAME}" ROOTFS_DIR="${CONTAINER_DIR}/rootfs" if [[ -d "$CONTAINER_DIR" ]]; then if [[ "$FORCE" -eq 1 ]]; then echo "==> -f given: destroying existing container '${NAME}'..." lxc-stop -n "$NAME" --kill >/dev/null 2>&1 || true lxc-destroy -n "$NAME" else echo "ERROR: container '${NAME}' already exists at ${CONTAINER_DIR}." >&2 echo " Re-run with -f to destroy and recreate it." >&2 exit 1 fi fi # --- Disk space sanity check --- # On an 8GB eMMC box, blindly unpacking a backup without checking free space # first is asking for a corrupted half-written rootfs. BACKUP_SIZE_KB=$(du -k "$BACKUP" | cut -f1) # Use /var/lib rather than /var/lib/lxc, since /var/lib/lxc may not exist # yet on a freshly-installed host before the first container is created. AVAIL_KB=$(df -k --output=avail /var/lib | tail -1 | tr -d ' ') # Rough heuristic: assume up to ~3x expansion for compressed backups, plus # headroom for the skeleton container lxc-create downloads first. NEEDED_KB=$(( BACKUP_SIZE_KB * 4 )) echo "==> Backup size: $(( BACKUP_SIZE_KB / 1024 )) MB (compressed)" echo "==> Available on disk: $(( AVAIL_KB / 1024 )) MB" if [[ "$AVAIL_KB" -lt "$NEEDED_KB" ]]; then echo "WARNING: available space may not be enough for a ${BACKUP_SIZE_KB}KB" >&2 echo " compressed backup once extracted. Proceed with caution," >&2 echo " or free up space first." >&2 read -rp "Continue anyway? [y/N] " confirm [[ "$confirm" =~ ^[Yy]$ ]] || exit 1 fi # --- Step 1: create skeleton container --- echo "==> Creating skeleton container '${NAME}' (${DIST} ${RELEASE} ${ARCH})..." lxc-create -n "$NAME" -t download -- -d "$DIST" -r "$RELEASE" -a "$ARCH" # --- Step 2: wipe skeleton rootfs, unpack backup over it --- echo "==> Wiping skeleton rootfs..." rm -rf "${ROOTFS_DIR:?}"/* echo "==> Extracting backup into rootfs..." case "$BACKUP" in *.tar.zst) tar --zstd -xpf "$BACKUP" -C "$ROOTFS_DIR" --numeric-owner ;; *.tar.gz|*.tgz) tar -xzpf "$BACKUP" -C "$ROOTFS_DIR" --numeric-owner ;; *.tar.lzo) command -v lzop >/dev/null 2>&1 || { echo "ERROR: lzop not installed (apt install lzop)" >&2; exit 1; } tar --use-compress-program=lzop -xpf "$BACKUP" -C "$ROOTFS_DIR" --numeric-owner ;; *.tar) tar -xpf "$BACKUP" -C "$ROOTFS_DIR" --numeric-owner ;; *) echo "ERROR: unrecognised backup extension on: $BACKUP" >&2 echo " Expected .tar.zst, .tar.gz, .tar.lzo, or .tar" >&2 exit 1 ;; esac # Proxmox vzdump LXC archives sometimes nest content under ./ or a top-level # dir depending on dump version. Detect and flatten if needed. if [[ ! -d "${ROOTFS_DIR}/etc" ]] && [[ -d "${ROOTFS_DIR}" ]]; then NESTED=$(find "$ROOTFS_DIR" -maxdepth 2 -type d -name etc | head -1) if [[ -n "$NESTED" ]]; then NESTED_ROOT=$(dirname "$NESTED") echo "==> Detected nested rootfs at ${NESTED_ROOT}, flattening..." shopt -s dotglob mv "${NESTED_ROOT}"/* "$ROOTFS_DIR"/ shopt -u dotglob rmdir "$NESTED_ROOT" 2>/dev/null || true fi fi if [[ ! -d "${ROOTFS_DIR}/etc" ]]; then echo "WARNING: ${ROOTFS_DIR}/etc not found after extraction." >&2 echo " The backup archive structure may be unexpected — inspect manually:" >&2 echo " ls ${ROOTFS_DIR}" >&2 fi # --- Step 3: surface useful info for manual config --- echo echo "================================================================" echo " Extraction complete for: ${NAME}" echo "================================================================" if [[ -f "${ROOTFS_DIR}/etc/hostname" ]]; then echo "Original hostname: $(cat "${ROOTFS_DIR}/etc/hostname")" fi if [[ -f "${ROOTFS_DIR}/etc/network/interfaces" ]]; then echo echo "Original /etc/network/interfaces:" echo "----------------------------------------------------------------" cat "${ROOTFS_DIR}/etc/network/interfaces" echo "----------------------------------------------------------------" fi if [[ -f "${ROOTFS_DIR}/etc/resolv.conf" ]]; then echo echo "Original /etc/resolv.conf:" cat "${ROOTFS_DIR}/etc/resolv.conf" fi echo echo "NEXT STEPS:" echo " 1. Review/edit the LXC config: ${CONTAINER_DIR}/config" echo " Set lxc.net.0.link, lxc.net.0.ipv4.address, lxc.net.0.ipv4.gateway" echo " to match the IP this container should use on the standby box." echo " 2. Check ${ROOTFS_DIR}/etc/hosts and hostname match what you want here." echo " 3. Start it with: lxc-start -n ${NAME}" echo " 4. Check it with: lxc-ls --fancy / lxc-attach -n ${NAME}" echo echo "Container left STOPPED for review."