- ansible/: VM provisioning playbooks and roles - provision-vm.yml: create KVM VM from Ubuntu cloud image - install.yml: install OpenClaw on guest (upstream) - customize.yml: swappiness, virtiofs fstab, linger - roles/vm/: libvirt domain XML, cloud-init templates - inventory.yml + host_vars/zap.yml: zap instance config - backup-openclaw-vm.sh: daily rsync + MinIO upload - restore-openclaw-vm.sh: full redeploy from scratch - README.md: full operational documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
85 lines
2.7 KiB
Bash
Executable File
85 lines
2.7 KiB
Bash
Executable File
#!/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."
|