#!/bin/bash
# Script Version: 04
# Description: Incus migration helper (DEV state)
# ======= USAGE =======
# Usage: ./incus_remote_rsync.sh <container-name> <target-host> [project] [--snapshot] [--no-compress]
# Examples:
# ./incus_remote_rsync.sh zammad 80.109.18.113 raid1 --snapshot
# ./incus_remote_rsync.sh zammad 80.109.18.113
# ./incus_remote_rsync.sh peertube 80.109.18.113 raid1 --no-compress
# ======= ARGS & DEFAULTS =======
if [ -z "$1" ] || [ -z "$2" ]; then
echo "Usage: $0 <container-name> <target-host> [project] [--snapshot] [--no-compress]"
exit 1
fi
CT_NAME="$1"
INCUS_DST_HOST="$2"
PROJECT="default"
SNAPSHOT=0
NO_COMPRESS=0
# parse remaining args (project and/or --snapshot and/or --no-compress)
for ARG in "${@:3}"; do
if [ "$ARG" = "--snapshot" ]; then
SNAPSHOT=1
elif [ "$ARG" = "--no-compress" ]; then
NO_COMPRESS=1
else
PROJECT="$ARG"
fi
done
# prepare export directory
EXPORT_DIR="/root/incus-export"
if [ ! -d "$EXPORT_DIR" ]; then
mkdir -p "$EXPORT_DIR" || { echo "Failed to create export directory $EXPORT_DIR"; exit 1; }
fi
BASE_PATH="$EXPORT_DIR/${CT_NAME}"
# choose compression (prefer zstd, then xz, then gzip) — will be ignored if NO_COMPRESS=1
if [ "$NO_COMPRESS" -eq 1 ]; then
COMPRESS_CMD=""
SUFFIX=".tar"
else
if command -v zstd >/dev/null 2>&1; then
COMPRESS_CMD="zstd -19 -T0"
SUFFIX=".tar.zst"
elif command -v xz >/dev/null 2>&1; then
COMPRESS_CMD="xz -9e"
SUFFIX=".tar.xz"
else
COMPRESS_CMD="gzip -9"
SUFFIX=".tar.gz"
fi
fi
EXPORT_FILE="$BASE_PATH$SUFFIX"
TEMP_TAR="$BASE_PATH.tar"
# ======= DEBUG =======
echo "[DEBUG] Target host: $INCUS_DST_HOST"
echo "[DEBUG] Container: $CT_NAME"
echo "[DEBUG] Project: $PROJECT"
echo "[DEBUG] Snapshot: $SNAPSHOT"
echo "[DEBUG] No compression flag: $NO_COMPRESS"
echo "[DEBUG] Preferred compression command: $COMPRESS_CMD"
# ======= MAIN =======
# Ensure container is stopped before export
CT_STATE=$(incus list "$CT_NAME" --format csv -c s 2>/dev/null | tr -d '
')
if [ "$CT_STATE" != "STOPPED" ]; then
echo "[DEBUG] Container is running — stopping it now"
incus stop "$CT_NAME" --force || { echo "Failed to stop container"; exit 1; }
else
echo "[DEBUG] Container already stopped"
fi
# Optional snapshot
if [ "$SNAPSHOT" -eq 1 ]; then
echo "[DEBUG] Creating snapshot pre-migrate"
incus snapshot create "$CT_NAME" pre-migrate || { echo "Snapshot failed"; exit 1; }
else
echo "[DEBUG] Snapshot skipped"
fi
# If export file already exists, skip export
if [ -f "$EXPORT_FILE" ]; then
echo "[DEBUG] Export file $EXPORT_FILE already exists — skipping export."
else
echo "[DEBUG] Exporting instance to temporary tar: $TEMP_TAR"
incus export "$CT_NAME" "$TEMP_TAR" || { echo "Export failed"; rm -f "$TEMP_TAR"; exit 1; }
# Decide whether to compress or keep tar based on size/flags
if [ "$NO_COMPRESS" -eq 1 ]; then
mv "$TEMP_TAR" "$BASE_PATH.tar"
EXPORT_FILE="$BASE_PATH.tar"
echo "[DEBUG] Skipped compression by user — using uncompressed tar: $EXPORT_FILE"
else
# threshold: 20 GiB
THRESHOLD_BYTES=$((20 * 1024 * 1024 * 1024))
ACT_SIZE=$(stat -c%s "$TEMP_TAR")
if [ "$ACT_SIZE" -ge "$THRESHOLD_BYTES" ]; then
echo "[DEBUG] Exported tar is large ($ACT_SIZE bytes) — skipping compression to save CPU and disk overhead"
mv "$TEMP_TAR" "$BASE_PATH.tar"
EXPORT_FILE="$BASE_PATH.tar"
else
echo "[DEBUG] Compressing archive with preferred compressor"
# compress and remove temp tar on success
if sh -c "$COMPRESS_CMD < \"$TEMP_TAR\" > \"$BASE_PATH$SUFFIX\""; then
rm -f "$TEMP_TAR"
EXPORT_FILE="$BASE_PATH$SUFFIX"
echo "[DEBUG] Backup exported and compressed successfully: $EXPORT_FILE"
else
echo "Compression failed"; rm -f "$TEMP_TAR"; exit 1
fi
fi
fi
fi
# Transfer (skip if already present on remote). Use --partial and --checksum to allow resuming and verify integrity
REMOTE_EXISTS=$(ssh root@"$INCUS_DST_HOST" "[ -f \"$EXPORT_FILE\" ] && echo 1 || echo 0")
if [ "$REMOTE_EXISTS" -eq 1 ]; then
echo "[DEBUG] Remote file $EXPORT_FILE already exists — skipping rsync."
else
echo "[DEBUG] Sending file to remote via rsync (partial + checksum enabled)"
rsync -avP --partial --checksum "$EXPORT_FILE" root@"$INCUS_DST_HOST":"$EXPORT_FILE" || { echo "Transfer failed"; exit 1; }
fi
echo "[DEBUG] Transfer completed (or skipped). Starting remote import..."
# Import on target (use project if provided)
ssh root@"$INCUS_DST_HOST" "incus import \"$EXPORT_FILE\" \"$CT_NAME\" --project \"$PROJECT\""
RC=$?
if [ $RC -ne 0 ]; then
echo "Error: remote import failed with code $RC"
echo "Check 'incus import --help' and remote logs on $INCUS_DST_HOST"
exit 1
fi
# Start on target
ssh root@"$INCUS_DST_HOST" "incus start \"$CT_NAME\" --project \"$PROJECT\""
RC=$?
if [ $RC -ne 0 ]; then
echo "Warning: failed to start instance on target (rc=$RC). Check remote 'incus start' output."
exit 1
fi
# Optional: verify instance exists on target
ssh root@"$INCUS_DST_HOST" "incus list --all-projects | grep -w \"$CT_NAME\" || true"
echo "[DEBUG] Migration completed."