a1a17e81a1
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
516 lines
25 KiB
Bash
516 lines
25 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
#####################################################################################
|
|
#### backup-smoke-tests.sh — Verify backups are actually working ####
|
|
#### Checks existence, recency, size, integrity, snapshot count, locks, restore. ####
|
|
#### Supports: restic, borg, directory, rsnapshot ####
|
|
#### ####
|
|
#### Author: Phil Connor ####
|
|
#### Contact: contact@mylinux.work ####
|
|
#### License: MIT ####
|
|
#### Version: 1.01 ####
|
|
#### ####
|
|
#### Usage: ####
|
|
#### export BACKUP_TYPE="restic" ####
|
|
#### export BACKUP_REPO="s3:s3.example.com/bucket" ####
|
|
#### ./backup-smoke-tests.sh ####
|
|
#### ####
|
|
#### See --help for all options. ####
|
|
#####################################################################################
|
|
|
|
set -euo pipefail
|
|
|
|
# ── Defaults ──────────────────────────────────────────────────────────
|
|
BACKUP_TYPE="${BACKUP_TYPE:-}"
|
|
BACKUP_REPO="${BACKUP_REPO:-}"
|
|
BACKUP_DIR="${BACKUP_DIR:-}"
|
|
MAX_AGE_HOURS="${MAX_AGE_HOURS:-26}"
|
|
MIN_SNAPSHOTS="${MIN_SNAPSHOTS:-1}"
|
|
MIN_SIZE_MB="${MIN_SIZE_MB:-1}"
|
|
RESTORE_TEST_FILE="${RESTORE_TEST_FILE:-}"
|
|
SKIP_RESTORE="${SKIP_RESTORE:-false}"
|
|
SKIP_INTEGRITY="${SKIP_INTEGRITY:-false}"
|
|
MOUNT_CHECK="${MOUNT_CHECK:-}"
|
|
OUTPUT_FORMAT="${OUTPUT_FORMAT:-text}" # text, tap, junit
|
|
JUNIT_FILE="${JUNIT_FILE:-backup-results.xml}"
|
|
VERBOSE="${VERBOSE:-false}"
|
|
COLOR="${COLOR:-auto}"
|
|
|
|
# ── State ─────────────────────────────────────────────────────────────
|
|
PASS=0
|
|
FAIL=0
|
|
SKIP=0
|
|
TOTAL=0
|
|
RESULTS=()
|
|
RESTORE_TMP=""
|
|
START_TIME=""
|
|
|
|
# ── Colors ────────────────────────────────────────────────────────────
|
|
setup_colors() {
|
|
if [[ "$COLOR" == "never" ]]; then
|
|
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
|
|
return
|
|
fi
|
|
if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[0;33m'
|
|
BLUE='\033[0;34m'
|
|
BOLD='\033[1m'
|
|
RESET='\033[0m'
|
|
else
|
|
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
|
|
fi
|
|
}
|
|
|
|
# ── Logging ───────────────────────────────────────────────────────────
|
|
log() { echo -e "${BLUE}[INFO]${RESET} $*"; }
|
|
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; }
|
|
err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
|
|
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${BLUE}[DEBUG]${RESET} $*"; fi; }
|
|
|
|
# ── Test Result Recording ─────────────────────────────────────────────
|
|
record_pass() {
|
|
local name="$1" detail="${2:-}"
|
|
((PASS++)) || true; ((TOTAL++)) || true
|
|
RESULTS+=("PASS|${name}|${detail}")
|
|
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then echo "ok ${TOTAL} - ${name}"
|
|
else echo -e " ${GREEN}✓${RESET} ${name}${detail:+ — ${detail}}"; fi
|
|
}
|
|
|
|
record_fail() {
|
|
local name="$1" detail="${2:-}"
|
|
((FAIL++)) || true; ((TOTAL++)) || true
|
|
RESULTS+=("FAIL|${name}|${detail}")
|
|
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
|
|
echo "not ok ${TOTAL} - ${name}"
|
|
[[ -n "$detail" ]] && echo " # ${detail}"
|
|
else echo -e " ${RED}✗${RESET} ${name}${detail:+ — ${detail}}"; fi
|
|
}
|
|
|
|
record_skip() {
|
|
local name="$1" reason="${2:-}"
|
|
((SKIP++)) || true; ((TOTAL++)) || true
|
|
RESULTS+=("SKIP|${name}|${reason}")
|
|
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then echo "ok ${TOTAL} - ${name} # SKIP ${reason}"
|
|
else echo -e " ${YELLOW}⊘${RESET} ${name}${reason:+ — ${reason}}"; fi
|
|
}
|
|
|
|
# ── Helpers ───────────────────────────────────────────────────────────
|
|
has_cmd() { command -v "$1" >/dev/null 2>&1; }
|
|
restore_ok() { find "${RESTORE_TMP}" -type f | grep -q .; }
|
|
require_tool() { if ! has_cmd "$1"; then record_skip "$2" "$1 not installed"; return 1; fi; }
|
|
|
|
# ── Cleanup ───────────────────────────────────────────────────────────
|
|
# shellcheck disable=SC2317
|
|
cleanup() { [[ -n "${RESTORE_TMP}" && -d "${RESTORE_TMP}" ]] && rm -rf "${RESTORE_TMP}"; }
|
|
trap cleanup EXIT
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# TEST SUITES
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
# ── 1. Repository Health ─────────────────────────────────────────────
|
|
test_repo_health() {
|
|
echo ""
|
|
echo -e "${BOLD}Repository Health${RESET}"
|
|
# 1a. Mount check (if configured)
|
|
if [[ -n "${MOUNT_CHECK}" ]]; then
|
|
if mountpoint -q "${MOUNT_CHECK}" 2>/dev/null; then
|
|
record_pass "Mount check" "${MOUNT_CHECK} is mounted"
|
|
else
|
|
record_fail "Mount check" "${MOUNT_CHECK} is not mounted"
|
|
warn "Skipping remaining tests — mount not available"
|
|
return
|
|
fi
|
|
fi
|
|
# 1b. Backup exists
|
|
case "${BACKUP_TYPE}" in
|
|
restic)
|
|
require_tool restic "Repository exists" || return
|
|
if restic cat config >/dev/null 2>&1; then record_pass "Repository exists"
|
|
else record_fail "Repository exists" "not accessible"; fi ;;
|
|
borg)
|
|
require_tool borg "Repository exists" || return
|
|
if borg info 2>/dev/null | grep -q "Repository ID"; then record_pass "Repository exists"
|
|
else record_fail "Repository exists" "not accessible"; fi ;;
|
|
directory|rsnapshot)
|
|
if [[ -d "${BACKUP_DIR}" ]]; then record_pass "Backup directory exists"
|
|
else record_fail "Backup directory exists" "${BACKUP_DIR} not found"; fi ;;
|
|
esac
|
|
# 1c. Repository reachable
|
|
case "${BACKUP_TYPE}" in
|
|
restic)
|
|
if [[ "${BACKUP_REPO}" =~ ^(s3|sftp|rest): ]]; then
|
|
require_tool restic "Repository reachable" || return
|
|
if restic cat config >/dev/null 2>&1; then record_pass "Repository reachable"
|
|
else record_fail "Repository reachable" "remote repository unreachable"; fi
|
|
else record_pass "Repository reachable" "local"; fi ;;
|
|
borg)
|
|
if [[ "${BACKUP_REPO}" =~ ^ssh:// || "${BACKUP_REPO}" =~ .*@.*:.* ]]; then
|
|
require_tool borg "Repository reachable" || return
|
|
if borg info >/dev/null 2>&1; then record_pass "Repository reachable"
|
|
else record_fail "Repository reachable" "remote repository unreachable"; fi
|
|
else record_pass "Repository reachable" "local"; fi ;;
|
|
directory|rsnapshot)
|
|
if [[ -r "${BACKUP_DIR}" ]]; then record_pass "Backup directory reachable"
|
|
else record_fail "Backup directory reachable" "${BACKUP_DIR} not readable"; fi ;;
|
|
esac
|
|
}
|
|
|
|
# ── 2. Backup Status ─────────────────────────────────────────────────
|
|
test_backup_status() {
|
|
echo ""
|
|
echo -e "${BOLD}Backup Status${RESET}"
|
|
# 2a. Recent backup
|
|
local last_ts="" max_age_s=$((MAX_AGE_HOURS * 3600))
|
|
case "${BACKUP_TYPE}" in
|
|
restic)
|
|
require_tool restic "Recent backup" || { test_size; test_snapshot_count; return; }
|
|
local latest
|
|
latest=$(restic snapshots --json --latest 1 2>/dev/null) || true
|
|
if [[ -z "${latest}" || "${latest}" == "[]" || "${latest}" == "null" ]]; then
|
|
record_fail "Recent backup" "no snapshots found"
|
|
else
|
|
local time_str
|
|
time_str=$(echo "${latest}" | grep -oP '"time"\s*:\s*"\K[^"]+' | head -1)
|
|
if [[ -z "${time_str}" ]]; then record_fail "Recent backup" "could not parse snapshot time"
|
|
else last_ts=$(date -d "${time_str}" +%s 2>/dev/null) || true; fi
|
|
fi ;;
|
|
borg)
|
|
require_tool borg "Recent backup" || { test_size; test_snapshot_count; return; }
|
|
local borg_time
|
|
borg_time=$(borg list --format '{time}{NL}' 2>/dev/null | tail -1) || true
|
|
if [[ -z "${borg_time}" ]]; then record_fail "Recent backup" "no archives found"
|
|
else last_ts=$(date -d "${borg_time}" +%s 2>/dev/null) || true; fi ;;
|
|
directory|rsnapshot)
|
|
local newest
|
|
newest=$(find "${BACKUP_DIR}" -maxdepth 1 -mindepth 1 -type d -printf '%T@\n' 2>/dev/null | sort -rn | head -1)
|
|
[[ -z "${newest}" ]] && newest=$(find "${BACKUP_DIR}" -maxdepth 1 -mindepth 1 -printf '%T@\n' 2>/dev/null | sort -rn | head -1)
|
|
if [[ -z "${newest}" ]]; then record_fail "Recent backup" "no backups found in ${BACKUP_DIR}"
|
|
else last_ts="${newest%%.*}"; fi ;;
|
|
esac
|
|
if [[ -n "${last_ts}" ]]; then
|
|
local now_ts age_s age_h
|
|
now_ts=$(date +%s); age_s=$((now_ts - last_ts)); age_h=$((age_s / 3600))
|
|
if [[ ${age_s} -le ${max_age_s} ]]; then record_pass "Recent backup" "${age_h}h ago (max ${MAX_AGE_HOURS}h)"
|
|
else record_fail "Recent backup" "${age_h}h ago (max ${MAX_AGE_HOURS}h)"; fi
|
|
fi
|
|
test_size
|
|
test_snapshot_count
|
|
}
|
|
|
|
test_size() {
|
|
local size_mb=0
|
|
case "${BACKUP_TYPE}" in
|
|
restic)
|
|
require_tool restic "Backup size" || return
|
|
local stats total_bytes
|
|
stats=$(restic stats --json --mode raw-data 2>/dev/null) || true
|
|
total_bytes=$(echo "${stats}" | grep -oP '"total_size"\s*:\s*\K[0-9]+' | head -1) || true
|
|
[[ -n "${total_bytes}" ]] && size_mb=$((total_bytes / 1048576)) ;;
|
|
borg)
|
|
require_tool borg "Backup size" || return
|
|
local size_str num unit
|
|
size_str=$(borg info 2>/dev/null | grep -i "all archives" | grep -oP '[0-9.]+\s*(TB|GB|MB|kB)' | head -1) || true
|
|
if [[ -n "${size_str}" ]]; then
|
|
num=$(echo "${size_str}" | grep -oP '[0-9.]+'); unit=$(echo "${size_str}" | grep -oP '[A-Za-z]+')
|
|
case "${unit}" in
|
|
TB) size_mb=$(echo "${num} * 1048576" | bc 2>/dev/null | cut -d. -f1) || size_mb=999999 ;;
|
|
GB) size_mb=$(echo "${num} * 1024" | bc 2>/dev/null | cut -d. -f1) || size_mb=999999 ;;
|
|
MB) size_mb=$(echo "${num}" | cut -d. -f1) ;;
|
|
kB) size_mb=0 ;;
|
|
esac
|
|
fi ;;
|
|
directory|rsnapshot)
|
|
size_mb=$(du -sm "${BACKUP_DIR}" 2>/dev/null | awk '{print $1}') || size_mb=0 ;;
|
|
esac
|
|
if [[ ${size_mb} -ge ${MIN_SIZE_MB} ]]; then record_pass "Backup size" "${size_mb} MB (min ${MIN_SIZE_MB} MB)"
|
|
else record_fail "Backup size" "${size_mb} MB < ${MIN_SIZE_MB} MB"; fi
|
|
}
|
|
|
|
test_snapshot_count() {
|
|
local count=0
|
|
case "${BACKUP_TYPE}" in
|
|
restic)
|
|
require_tool restic "Snapshot count" || return
|
|
count=$(restic snapshots --json 2>/dev/null | grep -c '"time"') || count=0 ;;
|
|
borg)
|
|
require_tool borg "Snapshot count" || return
|
|
count=$(borg list 2>/dev/null | wc -l) || count=0 ;;
|
|
directory)
|
|
count=$(find "${BACKUP_DIR}" -maxdepth 1 -mindepth 1 | wc -l) || count=0 ;;
|
|
rsnapshot)
|
|
count=$(find "${BACKUP_DIR}" -maxdepth 1 -mindepth 1 -type d | wc -l) || count=0 ;;
|
|
esac
|
|
if [[ ${count} -ge ${MIN_SNAPSHOTS} ]]; then record_pass "Snapshot count" "${count} (min ${MIN_SNAPSHOTS})"
|
|
else record_fail "Snapshot count" "${count} < ${MIN_SNAPSHOTS}"; fi
|
|
}
|
|
|
|
# ── 3. Integrity ─────────────────────────────────────────────────────
|
|
test_integrity_suite() {
|
|
echo ""
|
|
echo -e "${BOLD}Integrity${RESET}"
|
|
# 3a. Integrity check
|
|
if [[ "${SKIP_INTEGRITY}" == "true" ]]; then
|
|
record_skip "Integrity check" "SKIP_INTEGRITY=true"
|
|
else
|
|
case "${BACKUP_TYPE}" in
|
|
restic)
|
|
require_tool restic "Integrity check" || return
|
|
if restic check 2>/dev/null; then record_pass "Integrity check"
|
|
else record_fail "Integrity check" "restic check failed"; fi ;;
|
|
borg)
|
|
require_tool borg "Integrity check" || return
|
|
if borg check 2>/dev/null; then record_pass "Integrity check"
|
|
else record_fail "Integrity check" "borg check failed"; fi ;;
|
|
directory|rsnapshot)
|
|
record_skip "Integrity check" "not applicable for ${BACKUP_TYPE}" ;;
|
|
esac
|
|
fi
|
|
# 3b. Lock check
|
|
case "${BACKUP_TYPE}" in
|
|
restic)
|
|
require_tool restic "Lock check" || return
|
|
local lock_output
|
|
lock_output=$(restic list locks 2>/dev/null) || true
|
|
if [[ -z "${lock_output}" ]]; then record_pass "Lock check" "no stale locks"
|
|
else record_fail "Lock check" "$(echo "${lock_output}" | wc -l) lock(s) found"; fi ;;
|
|
borg)
|
|
require_tool borg "Lock check" || return
|
|
if borg info 2>&1 | grep -qi "lock"; then record_fail "Lock check" "repository appears locked"
|
|
else record_pass "Lock check" "no stale locks"; fi ;;
|
|
directory|rsnapshot)
|
|
local lc
|
|
lc=$(find "${BACKUP_DIR}" -maxdepth 1 \( -name "*.lock" -o -name ".lock" \) 2>/dev/null | wc -l) || lc=0
|
|
if [[ ${lc} -eq 0 ]]; then record_pass "Lock check" "no stale locks"
|
|
else record_fail "Lock check" "${lc} lock file(s) in ${BACKUP_DIR}"; fi ;;
|
|
esac
|
|
}
|
|
|
|
# ── 4. Recovery ──────────────────────────────────────────────────────
|
|
test_recovery() {
|
|
echo ""
|
|
echo -e "${BOLD}Recovery${RESET}"
|
|
if [[ "${SKIP_RESTORE}" == "true" ]]; then record_skip "Test restore" "SKIP_RESTORE=true"; return; fi
|
|
RESTORE_TMP=$(mktemp -d /tmp/backup-smoke-test-XXXXXX)
|
|
case "${BACKUP_TYPE}" in
|
|
restic)
|
|
require_tool restic "Test restore" || return
|
|
restic_restore "${RESTORE_TEST_FILE}" ;;
|
|
borg)
|
|
require_tool borg "Test restore" || return
|
|
borg_restore "${RESTORE_TEST_FILE}" ;;
|
|
directory)
|
|
dir_restore "${RESTORE_TEST_FILE:+${BACKUP_DIR}/${RESTORE_TEST_FILE}}" ;;
|
|
rsnapshot)
|
|
dir_restore "${RESTORE_TEST_FILE:+${BACKUP_DIR}/${RESTORE_TEST_FILE}}" ;;
|
|
esac
|
|
}
|
|
|
|
restic_restore() {
|
|
local target="${1:-}"
|
|
if [[ -z "${target}" ]]; then
|
|
target=$(restic ls latest 2>/dev/null | head -1) || true
|
|
[[ -z "${target}" ]] && { record_skip "Test restore" "no files in latest snapshot"; return; }
|
|
fi
|
|
if restic restore latest --target "${RESTORE_TMP}" --include "${target}" 2>/dev/null && restore_ok; then
|
|
record_pass "Test restore" "file restored successfully"
|
|
else record_fail "Test restore" "restic restore failed"; fi
|
|
}
|
|
|
|
borg_restore() {
|
|
local archive target
|
|
archive=$(borg list --format '{archive}{NL}' 2>/dev/null | tail -1) || true
|
|
[[ -z "${archive}" ]] && { record_skip "Test restore" "no archives found"; return; }
|
|
target="${1:-}"
|
|
if [[ -z "${target}" ]]; then
|
|
target=$(borg list "::${archive}" --format '{path}{NL}' 2>/dev/null | grep -v '/$' | head -1) || true
|
|
[[ -z "${target}" ]] && { record_skip "Test restore" "no files in latest archive"; return; }
|
|
fi
|
|
if (cd "${RESTORE_TMP}" && borg extract "::${archive}" "${target}" 2>/dev/null) && restore_ok; then
|
|
record_pass "Test restore" "file restored successfully"
|
|
else record_fail "Test restore" "borg extract failed"; fi
|
|
}
|
|
|
|
dir_restore() {
|
|
local src_file="${1:-}"
|
|
[[ -z "${src_file}" ]] && src_file=$(find "${BACKUP_DIR}" -type f 2>/dev/null | head -1)
|
|
[[ -z "${src_file}" || ! -f "${src_file}" ]] && { record_skip "Test restore" "no files in backup directory"; return; }
|
|
local dest_file
|
|
dest_file="${RESTORE_TMP}/$(basename "${src_file}")"
|
|
if cp "${src_file}" "${dest_file}" 2>/dev/null && [[ -f "${dest_file}" ]]; then
|
|
record_pass "Test restore" "file copied successfully"
|
|
else record_fail "Test restore" "copy failed"; fi
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# OUTPUT
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
print_summary() {
|
|
local end_time; end_time=$(date +%s)
|
|
local duration=$(( end_time - START_TIME ))
|
|
echo ""
|
|
echo -e "${BOLD}────────────────────────────────────────${RESET}"
|
|
echo -e "${BOLD}Summary${RESET} ${BACKUP_TYPE} ${BACKUP_REPO:-${BACKUP_DIR}}"
|
|
echo -e " ${GREEN}${PASS} passed${RESET} ${RED}${FAIL} failed${RESET} ${YELLOW}${SKIP} skipped${RESET} (${duration}s)"
|
|
echo -e "${BOLD}────────────────────────────────────────${RESET}"
|
|
if [[ $FAIL -eq 0 ]]; then echo -e "${GREEN}${BOLD}All tests passed.${RESET}"
|
|
else echo -e "${RED}${BOLD}${FAIL} test(s) failed.${RESET}"; fi
|
|
}
|
|
|
|
print_tap_header() {
|
|
echo "TAP version 13"
|
|
}
|
|
|
|
print_tap_footer() {
|
|
echo "1..${TOTAL}"
|
|
echo "# pass ${PASS}"
|
|
echo "# fail ${FAIL}"
|
|
echo "# skip ${SKIP}"
|
|
}
|
|
|
|
write_junit() {
|
|
local end_time; end_time=$(date +%s)
|
|
local duration=$(( end_time - START_TIME ))
|
|
cat > "$JUNIT_FILE" <<JUNIT_EOF
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<testsuites tests="${TOTAL}" failures="${FAIL}" skipped="${SKIP}" time="${duration}">
|
|
<testsuite name="backup-smoke-tests" tests="${TOTAL}" failures="${FAIL}" skipped="${SKIP}" time="${duration}">
|
|
JUNIT_EOF
|
|
|
|
for result in "${RESULTS[@]}"; do
|
|
local status name detail
|
|
IFS='|' read -r status name detail <<< "$result"
|
|
name=$(echo "$name" | sed 's/&/\&/g; s/</\</g; s/>/\>/g; s/"/\"/g')
|
|
detail=$(echo "$detail" | sed 's/&/\&/g; s/</\</g; s/>/\>/g; s/"/\"/g')
|
|
echo " <testcase name=\"${name}\" classname=\"smoke\">" >> "$JUNIT_FILE"
|
|
case "$status" in
|
|
PASS) [[ -n "$detail" ]] && echo " <system-out>${detail}</system-out>" >> "$JUNIT_FILE" ;;
|
|
FAIL) echo " <failure message=\"${detail}\">FAILED: ${name} — ${detail}</failure>" >> "$JUNIT_FILE" ;;
|
|
SKIP) echo " <skipped message=\"${detail}\"/>" >> "$JUNIT_FILE" ;;
|
|
esac
|
|
echo " </testcase>" >> "$JUNIT_FILE"
|
|
done
|
|
echo " </testsuite>" >> "$JUNIT_FILE"
|
|
echo "</testsuites>" >> "$JUNIT_FILE"
|
|
log "JUnit report written to ${JUNIT_FILE}"
|
|
}
|
|
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
# MAIN
|
|
# ══════════════════════════════════════════════════════════════════════
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $(basename "$0") [OPTIONS]
|
|
|
|
Smoke-test backup repositories. Supports restic, borg, directory, and rsnapshot.
|
|
|
|
Required environment variables:
|
|
BACKUP_TYPE Backend type: restic, borg, directory, rsnapshot
|
|
BACKUP_REPO Repository path/URL (restic, borg)
|
|
BACKUP_DIR Directory path (directory, rsnapshot)
|
|
|
|
Optional environment variables:
|
|
MAX_AGE_HOURS Max age of latest backup in hours (default: 26)
|
|
MIN_SNAPSHOTS Minimum snapshot count (default: 1)
|
|
MIN_SIZE_MB Minimum backup size in MB (default: 1)
|
|
RESTORE_TEST_FILE Specific file to restore (default: auto-detect)
|
|
MOUNT_CHECK Mountpoint to verify before testing (e.g. /mnt/backup)
|
|
RESTIC_PASSWORD Restic repository password
|
|
BORG_PASSPHRASE Borg repository passphrase
|
|
|
|
Options:
|
|
--skip-restore Skip the test-restore check
|
|
--skip-integrity Skip the integrity check (can be slow)
|
|
--format FORMAT Output: text (default), tap, junit
|
|
--junit-file FILE JUnit output path (default: backup-results.xml)
|
|
--verbose Show debug output
|
|
--no-color Disable colored output
|
|
--help Show this help
|
|
|
|
Examples:
|
|
BACKUP_TYPE=restic BACKUP_REPO=s3:s3.example.com/bucket ./$(basename "$0")
|
|
BACKUP_TYPE=borg BACKUP_REPO=/mnt/backup ./$(basename "$0") --format junit
|
|
BACKUP_TYPE=directory BACKUP_DIR=/backup/daily ./$(basename "$0") --skip-restore
|
|
BACKUP_TYPE=directory BACKUP_DIR=/mnt/s3/backups MOUNT_CHECK=/mnt/s3 ./$(basename "$0")
|
|
BACKUP_TYPE=restic BACKUP_REPO=/srv/restic ./$(basename "$0") --format tap
|
|
EOF
|
|
}
|
|
|
|
main() {
|
|
# Parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--skip-restore) SKIP_RESTORE=true ;;
|
|
--skip-integrity) SKIP_INTEGRITY=true ;;
|
|
--format) OUTPUT_FORMAT="$2"; shift ;;
|
|
--junit-file) JUNIT_FILE="$2"; shift ;;
|
|
--verbose) VERBOSE=true ;;
|
|
--no-color) COLOR=never ;;
|
|
--help|-h) usage; exit 0 ;;
|
|
*) err "Unknown option: $1"; usage; exit 1 ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
setup_colors
|
|
|
|
# Validate required vars
|
|
if [[ -z "${BACKUP_TYPE}" ]]; then
|
|
err "BACKUP_TYPE is required (restic, borg, directory, rsnapshot)"; echo ""; usage; exit 1
|
|
fi
|
|
case "${BACKUP_TYPE}" in
|
|
restic|borg)
|
|
[[ -z "${BACKUP_REPO}" ]] && { err "BACKUP_REPO is required for ${BACKUP_TYPE}"; echo ""; usage; exit 1; } ;;
|
|
directory|rsnapshot)
|
|
[[ -z "${BACKUP_DIR}" ]] && { err "BACKUP_DIR is required for ${BACKUP_TYPE}"; echo ""; usage; exit 1; } ;;
|
|
*) err "Unsupported BACKUP_TYPE '${BACKUP_TYPE}'"; echo ""; usage; exit 1 ;;
|
|
esac
|
|
|
|
# Export backend variables
|
|
if [[ "${BACKUP_TYPE}" == "restic" ]]; then
|
|
export RESTIC_REPOSITORY="${RESTIC_REPOSITORY:-${BACKUP_REPO}}"
|
|
[[ -n "${RESTIC_PASSWORD:-}" ]] && export RESTIC_PASSWORD
|
|
[[ -n "${RESTIC_PASSWORD_FILE:-}" ]] && export RESTIC_PASSWORD_FILE
|
|
[[ -n "${AWS_ACCESS_KEY_ID:-}" ]] && export AWS_ACCESS_KEY_ID
|
|
[[ -n "${AWS_SECRET_ACCESS_KEY:-}" ]] && export AWS_SECRET_ACCESS_KEY
|
|
fi
|
|
if [[ "${BACKUP_TYPE}" == "borg" ]]; then
|
|
export BORG_REPO="${BACKUP_REPO}"
|
|
[[ -n "${BORG_PASSPHRASE:-}" ]] && export BORG_PASSPHRASE
|
|
[[ -n "${BORG_PASSCOMMAND:-}" ]] && export BORG_PASSCOMMAND
|
|
export BORG_RELOCATED_REPO_ACCESS_IS_OK=yes
|
|
fi
|
|
|
|
START_TIME=$(date +%s)
|
|
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
|
|
print_tap_header
|
|
else
|
|
echo ""
|
|
echo -e "${BOLD}Backup Smoke Tests${RESET}"
|
|
echo -e "Backend: ${BACKUP_TYPE} Target: ${BACKUP_REPO:-${BACKUP_DIR}}"
|
|
echo -e "Time: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
echo ""
|
|
fi
|
|
|
|
test_repo_health
|
|
test_backup_status
|
|
test_integrity_suite
|
|
test_recovery
|
|
|
|
if [[ "$OUTPUT_FORMAT" == "tap" ]]; then
|
|
print_tap_footer
|
|
elif [[ "$OUTPUT_FORMAT" == "junit" ]]; then
|
|
print_summary
|
|
write_junit
|
|
else
|
|
print_summary
|
|
fi
|
|
[[ $FAIL -eq 0 ]] && exit 0 || exit 1
|
|
}
|
|
|
|
main "$@"
|