File: //bigscoots/lxd/migrate.sh
#!/bin/bash
# LXD Container/VM Migration Script
# Usage: bash migrate.sh <container> <source_node> <source_ip> <target_node> <target_ip>
# Example: bash migrate.sh wpo-39566 vps216 154.18.240.50 wpo694 154.18.240.26
set -e
# ============================================================
# VARIABLES
# ============================================================
CONTAINER=$1
SOURCE_NODE=$2
SOURCE_IP=$3
TARGET_NODE=$4
TARGET_IP=$5
MIGRATION_ID=$(uuidgen)
MIGRATION_START=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
ABANDONED_NAME="${CONTAINER}-migrated-${MIGRATION_ID:0:8}"
CLEANUP_AFTER=$(date -u -d "+30 days" +"%Y-%m-%dT%H:%M:%SZ")
echo "============================================================"
echo " LXD Container Migration"
echo "============================================================"
echo " Migration ID : $MIGRATION_ID"
echo " Container : $CONTAINER"
echo " Source : $SOURCE_NODE ($SOURCE_IP)"
echo " Destination : $TARGET_NODE ($TARGET_IP)"
echo "============================================================"
# ============================================================
# PHASE 1: SETUP - Add destination as remote
# ============================================================
echo ""
echo "[PHASE 1] Setting up remote connection to $TARGET_NODE..."
# Remove stale remote if exists from previous failed migration
if lxc remote list | grep -q "^| $TARGET_NODE "; then
echo "[WARN] Stale remote $TARGET_NODE found, removing..."
lxc remote remove $TARGET_NODE
fi
TOKEN=$(ssh -p 2222 root@$TARGET_IP "lxc config trust add --name migration-${MIGRATION_ID:0:8} --quiet")
lxc remote add $TARGET_NODE "$TOKEN"
echo "[OK] Remote $TARGET_NODE added successfully"
# ============================================================
# PHASE 2: INITIAL SNAPSHOT + BULK COPY
# ============================================================
echo ""
echo "[PHASE 2] Taking initial snapshot and starting bulk copy..."
echo "[INFO] Instance will remain online during this phase"
SYNC1_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
lxc snapshot $CONTAINER sync1
echo "[OK] Snapshot sync1 taken at $SYNC1_TIME"
echo "[INFO] Starting bulk copy to $TARGET_NODE (this may take a while)..."
lxc copy "$CONTAINER" "$TARGET_NODE:${CONTAINER}-mig" --stateless --target="$TARGET_NODE"
echo "[OK] Bulk copy complete"
# ============================================================
# PHASE 3: INCREMENTAL SYNC 1 (instance still online)
# ============================================================
echo ""
echo "[PHASE 3] Running incremental sync 1 of 2..."
SYNC2_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
lxc snapshot $CONTAINER sync2
echo "[OK] Snapshot sync2 taken at $SYNC2_TIME"
lxc copy "$CONTAINER" "$TARGET_NODE:${CONTAINER}-mig" --stateless --refresh --target="$TARGET_NODE"
echo "[OK] Incremental sync 1 complete"
lxc delete "$CONTAINER/sync1"
lxc delete "${CONTAINER}-mig/sync1"
SYNC1_DELETED=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
echo "[OK] Snapshot sync1 deleted from both sides"
# ============================================================
# PHASE 4: INCREMENTAL SYNC 2 (instance still online)
# ============================================================
echo ""
echo "[PHASE 4] Running incremental sync 2 of 2..."
SYNC3_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
lxc snapshot $CONTAINER sync3
echo "[OK] Snapshot sync3 taken at $SYNC3_TIME"
lxc copy "$CONTAINER" "$TARGET_NODE:${CONTAINER}-mig" --stateless --refresh --target="$TARGET_NODE"
echo "[OK] Incremental sync 2 complete"
lxc delete "$CONTAINER/sync2"
lxc delete "${CONTAINER}-mig/sync2"
SYNC2_DELETED=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
echo "[OK] Snapshot sync2 deleted from both sides"
# ============================================================
# PHASE 5: CUTOVER
# ============================================================
echo ""
echo "[PHASE 5] Starting cutover..."
DOWNTIME_START=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
lxc stop $CONTAINER
echo "[OK] Instance stopped - DOWNTIME STARTED"
SYNCFINAL_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
lxc snapshot $CONTAINER syncfinal
lxc copy "$CONTAINER" "$TARGET_NODE:${CONTAINER}-mig" --stateless --refresh --target="$TARGET_NODE"
echo "[OK] Final incremental stream sent"
# ============================================================
# PHASE 6: RENAME
# ============================================================
echo ""
echo "[PHASE 6] Renaming instances..."
lxc rename $CONTAINER $ABANDONED_NAME
echo "[OK] Source renamed to $ABANDONED_NAME"
lxc rename "${CONTAINER}-mig" $CONTAINER
echo "[OK] Destination renamed to $CONTAINER"
# ============================================================
# PHASE 7: START + HEALTH CHECK
# ============================================================
echo ""
echo "[PHASE 7] Starting instance and running health check..."
lxc start $CONTAINER
echo "[OK] Instance started on $TARGET_NODE"
sleep 5
HEALTH=$(lxc exec $CONTAINER -- systemctl is-system-running 2>/dev/null || echo "agent-unavailable")
echo "[OK] Health check: $HEALTH"
DOWNTIME_END=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
DOWNTIME_SECONDS=$(( $(date -d "$DOWNTIME_END" +%s) - $(date -d "$DOWNTIME_START" +%s) ))
echo "[OK] DOWNTIME ENDED - Total downtime: ${DOWNTIME_SECONDS} seconds"
# ============================================================
# PHASE 8: CLEANUP SNAPSHOTS
# ============================================================
echo ""
echo "[PHASE 8] Cleaning up snapshots..."
lxc delete "$ABANDONED_NAME/sync3"
lxc delete "$ABANDONED_NAME/syncfinal"
lxc delete "$CONTAINER/sync3"
lxc delete "$CONTAINER/syncfinal"
SNAPSHOTS_DELETED=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
echo "[OK] All snapshots deleted"
# ============================================================
# PHASE 9: REMOVE REMOTE
# ============================================================
echo ""
echo "[PHASE 9] Removing LXD remote..."
lxc remote remove $TARGET_NODE
echo "[OK] Remote $TARGET_NODE removed"
MIGRATION_END=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# ============================================================
# MIGRATION SUMMARY JSON
# ============================================================
echo ""
echo "============================================================"
echo " MIGRATION COMPLETE"
echo "============================================================"
cat <<EOF
{
"migration_id": "$MIGRATION_ID",
"status": "completed_pending_cleanup",
"created_at": "$MIGRATION_START",
"completed_at": "$MIGRATION_END",
"cleanup_after": "$CLEANUP_AFTER",
"source": {
"node": "$SOURCE_NODE",
"ip": "$SOURCE_IP",
"container_original_name": "$CONTAINER",
"container_abandoned_name": "$ABANDONED_NAME",
"status": "abandoned_pending_cleanup"
},
"destination": {
"node": "$TARGET_NODE",
"ip": "$TARGET_IP",
"container_name": "$CONTAINER",
"status": "live"
},
"snapshots": [
{
"name": "sync1",
"taken_at": "$SYNC1_TIME",
"type": "initial",
"deleted_at": "$SYNC1_DELETED"
},
{
"name": "sync2",
"taken_at": "$SYNC2_TIME",
"type": "incremental",
"deleted_at": "$SYNC2_DELETED"
},
{
"name": "sync3",
"taken_at": "$SYNC3_TIME",
"type": "incremental",
"deleted_at": "$SNAPSHOTS_DELETED"
},
{
"name": "syncfinal",
"taken_at": "$SYNCFINAL_TIME",
"type": "final",
"deleted_at": "$SNAPSHOTS_DELETED"
}
],
"downtime": {
"started_at": "$DOWNTIME_START",
"ended_at": "$DOWNTIME_END",
"duration_seconds": $DOWNTIME_SECONDS
},
"cleanup": {
"status": "pending",
"triggered_at": null,
"completed_at": null,
"steps": [
{
"action": "delete_abandoned_container",
"target": "$ABANDONED_NAME",
"node": "$SOURCE_NODE",
"status": "pending"
}
]
}
}
EOF