chore: initial commit of docker-tools
This commit is contained in:
366
bash/docker-auto-migrate.sh
Executable file
366
bash/docker-auto-migrate.sh
Executable file
@@ -0,0 +1,366 @@
|
||||
#!/bin/bash
|
||||
# Docker Auto-Migration Script
|
||||
# Migrates ALL stacks to a specified destination host
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Version
|
||||
VERSION="2.6.2"
|
||||
|
||||
# Resolve script directory and source shared library
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=lib/common.sh
|
||||
source "$SCRIPT_DIR/lib/common.sh"
|
||||
|
||||
# Enable debug trace after sourcing common.sh (so DEBUG variable is available)
|
||||
enable_debug_trace
|
||||
|
||||
# Default configuration
|
||||
LOG_FILE="${LOG_FILE:-/tmp/docwell/docker-auto-migrate.log}"
|
||||
DEST_HOST="${DEST_HOST:-}"
|
||||
DEST_USER="${DEST_USER:-$(whoami)}"
|
||||
DEST_PORT="${DEST_PORT:-22}"
|
||||
REMOTE_STACKS_DIR="${REMOTE_STACKS_DIR:-/opt/stacks}"
|
||||
TRANSFER_METHOD="rclone"
|
||||
COMPRESSION="${COMPRESSION:-zstd}"
|
||||
|
||||
# Load config file overrides
|
||||
load_config
|
||||
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--version)
|
||||
echo "docker-auto-migrate.sh v$VERSION"
|
||||
exit 0
|
||||
;;
|
||||
--quiet|-q)
|
||||
QUIET=true
|
||||
shift
|
||||
;;
|
||||
--yes|-y)
|
||||
YES=true
|
||||
shift
|
||||
;;
|
||||
--dest)
|
||||
DEST_HOST="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dest-user)
|
||||
DEST_USER="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dest-port)
|
||||
DEST_PORT="$2"
|
||||
shift 2
|
||||
;;
|
||||
--stacks-dir)
|
||||
STACKS_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--remote-stacks-dir)
|
||||
REMOTE_STACKS_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--method)
|
||||
TRANSFER_METHOD="$2"
|
||||
shift 2
|
||||
;;
|
||||
--install-deps)
|
||||
AUTO_INSTALL=true
|
||||
shift
|
||||
;;
|
||||
--log)
|
||||
LOG_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
--debug|-d)
|
||||
DEBUG=true
|
||||
shift
|
||||
;;
|
||||
--verbose|-v)
|
||||
VERBOSE=true
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}Unknown option: $1${NC}" >&2
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
show_help() {
|
||||
cat << EOF
|
||||
Docker Auto-Migration Script v$VERSION
|
||||
|
||||
Migrates ALL Docker stacks to a specified destination host.
|
||||
Requires root privileges and SSH key-based authentication.
|
||||
|
||||
Usage: $0 [OPTIONS]
|
||||
|
||||
Options:
|
||||
--dest HOST Destination host (required)
|
||||
--dest-user USER Destination user (default: current user)
|
||||
--dest-port PORT Destination SSH port (default: 22)
|
||||
--stacks-dir DIR Local stacks directory (default: /opt/stacks)
|
||||
--remote-stacks-dir Remote stacks directory (default: /opt/stacks)
|
||||
--method METHOD Transfer method: rclone, rsync (default: rclone)
|
||||
--quiet, -q Suppress non-error output
|
||||
--yes, -y Auto-confirm all prompts
|
||||
--dry-run Show what would be done without actually doing it
|
||||
--debug, -d Enable debug mode (verbose output + xtrace)
|
||||
--verbose, -v Enable verbose logging
|
||||
--install-deps Auto-install missing dependencies (requires root)
|
||||
--log FILE Log file path
|
||||
--version Show version information
|
||||
--help, -h Show this help message
|
||||
|
||||
Environment Variables:
|
||||
DEST_HOST Destination host
|
||||
DEST_USER Destination user
|
||||
DEST_PORT Destination SSH port
|
||||
STACKS_DIR Local stacks directory
|
||||
REMOTE_STACKS_DIR Remote stacks directory
|
||||
|
||||
Examples:
|
||||
$0 --dest 10.0.0.2 --dest-user admin
|
||||
$0 --dest 10.0.0.2 --method rsync --dry-run
|
||||
$0 --dest remote-host --yes
|
||||
EOF
|
||||
}
|
||||
|
||||
check_dependencies() {
|
||||
local required=("docker" "ssh")
|
||||
|
||||
case "$TRANSFER_METHOD" in
|
||||
rclone) required+=("rclone") ;;
|
||||
rsync) required+=("rsync") ;;
|
||||
esac
|
||||
|
||||
local missing=()
|
||||
for cmd in "${required[@]}"; do
|
||||
command -v "$cmd" &>/dev/null || missing+=("$cmd")
|
||||
done
|
||||
|
||||
if [[ ${#missing[@]} -gt 0 ]]; then
|
||||
log_error "Missing dependencies: ${missing[*]}"
|
||||
if [[ "$AUTO_INSTALL" == "true" ]]; then
|
||||
install_dependencies "${missing[@]}"
|
||||
return $?
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
stop_local_stack() {
|
||||
local stack="$1"
|
||||
local stack_path="$STACKS_DIR/$stack"
|
||||
local compose_file
|
||||
compose_file=$(get_compose_file "$stack_path") || return 1
|
||||
run_or_dry "stop $stack" docker compose -f "$stack_path/$compose_file" down >/dev/null 2>&1
|
||||
}
|
||||
|
||||
start_remote_stack() {
|
||||
local stack="$1"
|
||||
local remote_path="$REMOTE_STACKS_DIR/$stack"
|
||||
|
||||
for f in compose.yaml compose.yml docker-compose.yaml docker-compose.yml; do
|
||||
if ssh -p "$DEST_PORT" -o StrictHostKeyChecking=no "$DEST_USER@$DEST_HOST" \
|
||||
"test -f '$remote_path/$f'" 2>/dev/null; then
|
||||
run_or_dry "start $stack on remote" \
|
||||
ssh -p "$DEST_PORT" -o StrictHostKeyChecking=no "$DEST_USER@$DEST_HOST" \
|
||||
"cd '$remote_path' && docker compose -f '$f' up -d" >/dev/null 2>&1
|
||||
return $?
|
||||
fi
|
||||
done
|
||||
|
||||
log_error "No compose file found on destination for $stack"
|
||||
return 1
|
||||
}
|
||||
|
||||
transfer_files() {
|
||||
local src="$1" dst="$2"
|
||||
|
||||
case "$TRANSFER_METHOD" in
|
||||
rclone)
|
||||
run_or_dry "rclone sync $src to $dst" \
|
||||
rclone sync "$src" ":sftp,host=$DEST_HOST,user=$DEST_USER,port=$DEST_PORT:$dst" \
|
||||
--transfers 16 --progress --sftp-ask-password 2>&1
|
||||
;;
|
||||
rsync)
|
||||
run_or_dry "rsync $src to $dst" \
|
||||
rsync -avz --progress --delete \
|
||||
-e "ssh -p $DEST_PORT -o StrictHostKeyChecking=no" \
|
||||
"$src/" "$DEST_USER@$DEST_HOST:$dst/"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
migrate_volume() {
|
||||
local vol="$1"
|
||||
local vol_backup_dir="$2"
|
||||
|
||||
local comp_info
|
||||
comp_info=$(get_compressor "$COMPRESSION")
|
||||
local compressor="${comp_info%%:*}"
|
||||
local ext="${comp_info#*:}"
|
||||
|
||||
log_info "Backing up volume: $vol"
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
log_info "[DRY-RUN] Would migrate volume $vol"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Backup locally
|
||||
docker run --rm \
|
||||
-v "${vol}:/data:ro" \
|
||||
-v "$vol_backup_dir:/backup" \
|
||||
alpine \
|
||||
sh -c "tar -cf - -C /data . | $compressor > /backup/${vol}${ext}" 2>/dev/null || {
|
||||
log_error "Failed to backup volume: $vol"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Transfer to remote
|
||||
transfer_files "$vol_backup_dir/${vol}${ext}" "/tmp/docwell_vol_migrate/"
|
||||
|
||||
# Restore on remote
|
||||
local decompressor
|
||||
case "$compressor" in
|
||||
zstd) decompressor="zstd -d" ;;
|
||||
gzip) decompressor="gzip -d" ;;
|
||||
*) decompressor="cat" ;;
|
||||
esac
|
||||
|
||||
ssh -p "$DEST_PORT" -o StrictHostKeyChecking=no "$DEST_USER@$DEST_HOST" \
|
||||
"docker volume create '$vol' 2>/dev/null; \
|
||||
docker run --rm -v '${vol}:/data' -v '/tmp/docwell_vol_migrate:/backup:ro' alpine \
|
||||
sh -c 'cd /data && cat /backup/${vol}${ext} | ${decompressor} | tar -xf -'" 2>/dev/null || {
|
||||
log_warn "Failed to restore volume on remote: $vol"
|
||||
return 1
|
||||
}
|
||||
|
||||
log_success "Volume $vol migrated"
|
||||
}
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
|
||||
# Must be root
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
log_error "This script requires root privileges"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$DEST_HOST" ]]; then
|
||||
log_error "Destination host required (--dest HOST)"
|
||||
show_help
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! check_dependencies; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test SSH
|
||||
if ! test_ssh "$DEST_HOST" "$DEST_PORT" "$DEST_USER"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local stacks
|
||||
mapfile -t stacks < <(get_stacks)
|
||||
|
||||
if [[ ${#stacks[@]} -eq 0 ]]; then
|
||||
log_error "No stacks found in $STACKS_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_header "Auto-Migrate All Stacks"
|
||||
echo -e "${GRAY}DocWell Auto-Migration v$VERSION${NC}"
|
||||
echo
|
||||
echo -e " Source: local ($STACKS_DIR)"
|
||||
echo -e " Destination: $DEST_USER@$DEST_HOST:$DEST_PORT ($REMOTE_STACKS_DIR)"
|
||||
echo -e " Method: $TRANSFER_METHOD"
|
||||
echo -e " Stacks: ${#stacks[@]}"
|
||||
echo
|
||||
|
||||
if ! confirm "Proceed with migrating ALL stacks?"; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
local start_time=$SECONDS
|
||||
local total=${#stacks[@]}
|
||||
local failed=()
|
||||
local vol_backup_dir
|
||||
vol_backup_dir=$(mktemp -d /tmp/docwell_auto_migrate.XXXXXX)
|
||||
register_cleanup "rm -rf '$vol_backup_dir'"
|
||||
|
||||
local idx=0
|
||||
for stack in "${stacks[@]}"; do
|
||||
((idx++))
|
||||
echo
|
||||
log_separator
|
||||
echo -e "${CYAN}[$idx/$total]${NC} ${BOLD}$stack${NC}"
|
||||
|
||||
# Step 1: Stop local stack
|
||||
log_info "Stopping local stack..."
|
||||
if is_stack_running "$stack"; then
|
||||
stop_local_stack "$stack" || {
|
||||
log_warn "Failed to stop $stack, continuing..."
|
||||
}
|
||||
fi
|
||||
|
||||
# Step 2: Transfer files
|
||||
log_info "Transferring files..."
|
||||
if ! transfer_files "$STACKS_DIR/$stack" "$REMOTE_STACKS_DIR/$stack"; then
|
||||
log_error "Failed to transfer files for $stack"
|
||||
failed+=("$stack")
|
||||
continue
|
||||
fi
|
||||
|
||||
# Step 3: Migrate volumes
|
||||
local volumes
|
||||
mapfile -t volumes < <(get_service_volumes "$stack" 2>/dev/null || true)
|
||||
for vol in "${volumes[@]}"; do
|
||||
[[ -z "$vol" ]] && continue
|
||||
migrate_volume "$vol" "$vol_backup_dir" || {
|
||||
log_warn "Volume migration failed for $vol"
|
||||
}
|
||||
done
|
||||
|
||||
# Step 4: Start on remote
|
||||
log_info "Starting on destination..."
|
||||
if ! start_remote_stack "$stack"; then
|
||||
log_warn "Failed to start $stack on destination"
|
||||
failed+=("$stack")
|
||||
else
|
||||
log_success "$stack migrated"
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
log_separator
|
||||
local elapsed=$(( SECONDS - start_time ))
|
||||
|
||||
if [[ ${#failed[@]} -gt 0 ]]; then
|
||||
log_warn "Failed stacks (${#failed[@]}): ${failed[*]}"
|
||||
else
|
||||
log_success "All stacks migrated successfully"
|
||||
fi
|
||||
|
||||
log_info "Auto-migration completed in $(format_elapsed "$elapsed")"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user