Files
homelab-infra/ops/restore-tests/common.sh
T

133 lines
4.1 KiB
Bash

#!/bin/bash
set -euo pipefail
RESTORE_TESTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BORG_CONTAINER="${BORG_CONTAINER:-borg-ui}"
BORG_RESTORE_HOST_ROOT="${BORG_RESTORE_HOST_ROOT:-/mnt/user/appdata/borg-ui/restore}"
BORG_PASSPHRASE_FILE_DEFAULT="${BORG_PASSPHRASE_FILE_DEFAULT:-/mnt/user/appdata/secrets/borg_repo_passphrase.txt}"
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "Missing command: $1" >&2
exit 1
}
}
require_path() {
[ -e "$1" ] || {
echo "Missing path: $1" >&2
exit 1
}
}
require_borg_container() {
docker inspect "$BORG_CONTAINER" >/dev/null 2>&1 || {
echo "Missing Borg container: $BORG_CONTAINER" >&2
exit 1
}
[ "$(docker inspect -f '{{.State.Running}}' "$BORG_CONTAINER" 2>/dev/null)" = "true" ] || {
echo "Borg container is not running: $BORG_CONTAINER" >&2
exit 1
}
docker exec "$BORG_CONTAINER" test -r /data/borg.db >/dev/null 2>&1 || {
echo "Missing borg-ui database in container: $BORG_CONTAINER:/data/borg.db" >&2
exit 1
}
docker exec "$BORG_CONTAINER" test -r /local/secrets/borg_repo_passphrase.txt >/dev/null 2>&1 || {
echo "Missing Borg passphrase in container: $BORG_CONTAINER:/local/secrets/borg_repo_passphrase.txt" >&2
echo "Host path exists, but borg-ui must mount it as /local/secrets/borg_repo_passphrase.txt." >&2
exit 1
}
}
latest_archive_name() {
require_borg_container
docker exec -i "$BORG_CONTAINER" python3 - <<'PY'
import sqlite3
conn = sqlite3.connect('/data/borg.db')
cur = conn.cursor()
cur.execute("select archive_name from backup_jobs where status in ('completed', 'completed_with_warnings') order by created_at desc limit 1")
row = cur.fetchone()
if not row:
raise SystemExit("No completed borg archive found")
print(row[0])
PY
}
borg_repo_url() {
require_borg_container
docker exec -i "$BORG_CONTAINER" python3 - <<'PY'
import sqlite3
conn = sqlite3.connect('/data/borg.db')
cur = conn.cursor()
cur.execute("select path from repositories where path is not null and path != '' order by id asc limit 1")
row = cur.fetchone()
if not row:
raise SystemExit("No borg repository configured")
print(row[0])
PY
}
borg_extract() {
local extract_dir="$1"
shift
local paths=("$@")
require_borg_container
docker exec -i "$BORG_CONTAINER" python3 - "$extract_dir" "${paths[@]}" <<'PY'
import os, sys, subprocess
extract_dir = sys.argv[1]
paths = sys.argv[2:]
import sqlite3
conn = sqlite3.connect('/data/borg.db')
cur = conn.cursor()
cur.execute("select path from repositories where path is not null and path != '' order by id asc limit 1")
repo = cur.fetchone()[0]
cur.execute("select archive_name from backup_jobs where status in ('completed', 'completed_with_warnings') order by created_at desc limit 1")
archive = cur.fetchone()[0]
with open('/local/secrets/borg_repo_passphrase.txt', 'r', encoding='utf-8') as f:
os.environ['BORG_PASSPHRASE'] = f.read().strip()
known_hosts = '/data/known_hosts'
if os.path.exists(known_hosts):
os.environ.setdefault(
'BORG_RSH',
f'ssh -o UserKnownHostsFile={known_hosts} -o StrictHostKeyChecking=yes',
)
os.makedirs(extract_dir, exist_ok=True)
os.chdir(extract_dir)
subprocess.run(['borg', 'extract', f'{repo}::{archive}', *paths], check=True)
PY
}
write_report() {
local report_file="$1"
shift
mkdir -p "$(dirname "$report_file")"
cat > "$report_file"
}
cleanup_compose() {
local compose_file="$1"
if [ -f "$compose_file" ]; then
docker compose -f "$compose_file" down >/dev/null 2>&1 || true
fi
}
# Hilfsfunktion: bei Fehler-Exit Restore-Lab-Pfad nicht loeschen, sondern in
# einen `_failed/<service>-<date>-<pid>`-Pfad umbenennen, damit Post-Mortem
# moeglich bleibt. Aufrufer setzt vor Erfolg `RESTORE_SUCCESS=1`.
RESTORE_FAILED_ROOT="${RESTORE_FAILED_ROOT:-/mnt/user/backups/restore-lab/_failed}"
preserve_on_failure() {
local service="$1"
local path="$2"
if [ ! -e "$path" ]; then
return 0
fi
mkdir -p "$RESTORE_FAILED_ROOT"
local target="$RESTORE_FAILED_ROOT/${service}-$(date +%F)-$$"
if mv "$path" "$target" 2>/dev/null; then
echo "preserved failed restore data: $target" >&2
else
echo "failed to preserve restore data: $path -> $target" >&2
fi
}