#!/bin/bash # Sync OpenClaw VM config to ~/lab/swarm and back up to MinIO # # Local: rsync ~/.openclaw/ → ~/lab/swarm/openclaw/ (live mirror, no archive) # MinIO: timestamped archive → s3://zap/backups/ (point-in-time recovery) set -euo pipefail INSTANCE="${1:-zap}" REGISTRY="${HOME}/.claude/state/openclaw-instances.json" SYNC_DIR="${HOME}/lab/swarm/openclaw" MINIO_BUCKET="zap" MINIO_PREFIX="backups" KEEP=7 # Resolve instance from registry GUEST_HOST=$(python3 -c " import json, sys data = json.load(open('${REGISTRY}')) inst = next((i for i in data['instances'] if i['name'] == '${INSTANCE}'), None) if not inst: print('ERROR: unknown instance', file=sys.stderr); sys.exit(1) if not inst.get('host'): print('ERROR: instance has no host', file=sys.stderr); sys.exit(1) print(inst['host']) ") GUEST_USER=$(python3 -c " import json data = json.load(open('${REGISTRY}')) inst = next(i for i in data['instances'] if i['name'] == '${INSTANCE}') print(inst['user']) ") TIMESTAMP=$(date +%Y%m%d_%H%M%S) echo "[$(date '+%Y-%m-%d %H:%M:%S')] Syncing ${INSTANCE} (${GUEST_HOST})..." # Check VM is reachable if ! ssh -o ConnectTimeout=5 -o BatchMode=yes "root@${GUEST_HOST}" "echo ok" &>/dev/null; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: Cannot reach ${GUEST_HOST}" >&2 exit 1 fi mkdir -p "${SYNC_DIR}" # Rsync ~/.openclaw/ → ~/lab/swarm/openclaw/ # Excludes large/ephemeral data not needed for redeployment rsync -az --delete \ --exclude='workspace/' \ --exclude='extensions-quarantine/' \ --exclude='logs/' \ --exclude='*.bak' \ --exclude='*.bak.*' \ --exclude='*.bak-*' \ --exclude='*.backup-*' \ --exclude='*.pre-*' \ --exclude='*.failed' \ -e "ssh -o BatchMode=yes" \ "root@${GUEST_HOST}:/home/${GUEST_USER}/.openclaw/" \ "${SYNC_DIR}/" echo "[$(date '+%Y-%m-%d %H:%M:%S')] Synced to ${SYNC_DIR}" # Create archive from synced files and upload to MinIO ARCHIVE=$(mktemp --suffix=".tar.gz") trap 'rm -f "${ARCHIVE}"' EXIT tar czf "${ARCHIVE}" -C "${HOME}/lab/swarm" openclaw MINIO_KEY="${MINIO_PREFIX}/${INSTANCE}-${TIMESTAMP}.tar.gz" aws s3 cp "${ARCHIVE}" "s3://${MINIO_BUCKET}/${MINIO_KEY}" --quiet SIZE=$(du -sh "${ARCHIVE}" | cut -f1) echo "[$(date '+%Y-%m-%d %H:%M:%S')] MinIO: s3://${MINIO_BUCKET}/${MINIO_KEY} (${SIZE})" # Prune MinIO backups, keep last N mapfile -t OLD_KEYS < <(aws s3 ls "s3://${MINIO_BUCKET}/${MINIO_PREFIX}/${INSTANCE}-" \ | awk '{print $4}' | sort | head -n -${KEEP}) for key in "${OLD_KEYS[@]:-}"; do [[ -z "${key}" ]] && continue aws s3 rm "s3://${MINIO_BUCKET}/${MINIO_PREFIX}/${key}" --quiet echo "[$(date '+%Y-%m-%d %H:%M:%S')] Pruned MinIO: ${key}" done echo "[$(date '+%Y-%m-%d %H:%M:%S')] Done."