From cd0b088cdc9c39b47a40fe246c2133b688f07e80 Mon Sep 17 00:00:00 2001 From: zap Date: Sun, 8 Mar 2026 21:57:43 +0000 Subject: [PATCH] feat(backup): add MinIO recovery smoke test script --- scripts/recovery-smoke-minio.sh | 87 +++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100755 scripts/recovery-smoke-minio.sh diff --git a/scripts/recovery-smoke-minio.sh b/scripts/recovery-smoke-minio.sh new file mode 100755 index 0000000..3183745 --- /dev/null +++ b/scripts/recovery-smoke-minio.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +set -euo pipefail + +CREDS_FILE="${CREDS_FILE:-$HOME/.openclaw/credentials/minio-zap.env}" +MC_BIN="${MC_BIN:-$HOME/.openclaw/workspace/bin/mc}" +PREFIX_ROOT="${PREFIX_ROOT:-workspace-backups}" +MAX_AGE_HOURS="${MAX_AGE_HOURS:-12}" + +if [[ ! -x "$MC_BIN" ]]; then + MC_BIN="$(command -v mc || true)" +fi + +fail() { + echo "STATE=FAIL" + echo "ERROR=$1" + exit 1 +} + +[[ -f "$CREDS_FILE" ]] || fail "Missing creds file: $CREDS_FILE" +[[ -x "$MC_BIN" ]] || fail "MinIO client not found (set MC_BIN)" + +# shellcheck disable=SC1090 +source "$CREDS_FILE" + +[[ -n "${MINIO_ENDPOINT:-}" ]] || fail "MINIO_ENDPOINT missing" +[[ -n "${MINIO_ACCESS_KEY:-}" ]] || fail "MINIO_ACCESS_KEY missing" +[[ -n "${MINIO_SECRET_KEY:-}" ]] || fail "MINIO_SECRET_KEY missing" +[[ -n "${MINIO_BUCKET:-}" ]] || fail "MINIO_BUCKET missing" + +"$MC_BIN" alias set minio "$MINIO_ENDPOINT" "$MINIO_ACCESS_KEY" "$MINIO_SECRET_KEY" >/dev/null + +latest_prefix="$($MC_BIN ls "minio/$MINIO_BUCKET/$PREFIX_ROOT" 2>/dev/null | awk '{print $NF}' | tr -d '/' | grep -E '^[0-9]{8}T[0-9]{6}Z$' | sort | tail -n1 || true)" +[[ -n "$latest_prefix" ]] || fail "No backup prefixes found under $PREFIX_ROOT" + +# freshness check +backup_iso="${latest_prefix:0:4}-${latest_prefix:4:2}-${latest_prefix:6:2} ${latest_prefix:9:2}:${latest_prefix:11:2}:${latest_prefix:13:2} UTC" +backup_epoch="$(date -u -d "$backup_iso" +%s 2>/dev/null || echo 0)" +now_epoch="$(date -u +%s)" +[[ "$backup_epoch" -gt 0 ]] || fail "Could not parse timestamp from prefix: $latest_prefix" +age_h=$(( (now_epoch - backup_epoch) / 3600 )) +if (( age_h > MAX_AGE_HOURS )); then + fail "Latest backup too old (${age_h}h > ${MAX_AGE_HOURS}h): $latest_prefix" +fi + +prefix_path="minio/$MINIO_BUCKET/$PREFIX_ROOT/$latest_prefix" +archive="openclaw-${latest_prefix}.tar.gz" +sha_file="${archive}.sha256" +manifest="manifest.txt" + +# Ensure required objects exist +"$MC_BIN" stat "$prefix_path/$archive" >/dev/null 2>&1 || fail "Missing archive: $archive" +"$MC_BIN" stat "$prefix_path/$sha_file" >/dev/null 2>&1 || fail "Missing checksum: $sha_file" +"$MC_BIN" stat "$prefix_path/$manifest" >/dev/null 2>&1 || fail "Missing manifest: $manifest" + +TMPDIR="$(mktemp -d)" +trap 'rm -rf "$TMPDIR"' EXIT + +"$MC_BIN" cp "$prefix_path/$archive" "$TMPDIR/" >/dev/null +"$MC_BIN" cp "$prefix_path/$sha_file" "$TMPDIR/" >/dev/null +"$MC_BIN" cp "$prefix_path/$manifest" "$TMPDIR/" >/dev/null + +cd "$TMPDIR" +awk '{print $1" '$archive'"}' "$sha_file" > check.sha256 +sha256sum -c check.sha256 >/dev/null || fail "SHA256 verification failed" + +mkdir -p restore +# Extract without touching live ~/.openclaw +tar -xzf "$archive" -C restore || fail "Archive extraction failed" + +# Basic structure checks +for req in \ + "restore/.openclaw/openclaw.json" \ + "restore/.openclaw/agents" \ + "restore/.openclaw/credentials" \ + "restore/.openclaw/workspace"; do + [[ -e "$req" ]] || fail "Missing required path in restore: $req" +done + +size_bytes="$(stat -c '%s' "$archive")" + +echo "STATE=PASS" +echo "LATEST_PREFIX=$latest_prefix" +echo "AGE_HOURS=$age_h" +echo "ARCHIVE_BYTES=$size_bytes" +echo "CHECKSUM=OK" +echo "EXTRACT=OK" +echo "STRUCTURE=OK"