79 lines
3.1 KiB
Bash
Executable File
79 lines
3.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Encrypted off-host backup for data-disk and total-loss recovery.
|
|
set -Eeuo pipefail
|
|
umask 077
|
|
|
|
CONFIG=${DISASTER_BACKUP_CONFIG:-/etc/mike-ai/disaster-backup.env}
|
|
STATE=/var/lib/mike-ai-disaster-backup
|
|
|
|
log() { printf '\n==> %s\n' "$*"; }
|
|
die() { printf 'FEHLER: %s\n' "$*" >&2; exit 1; }
|
|
|
|
[[ $EUID -eq 0 ]] || die "Bitte als root ausführen."
|
|
[[ -r $CONFIG ]] || die "Konfiguration fehlt: $CONFIG"
|
|
# shellcheck disable=SC1090
|
|
source "$CONFIG"
|
|
[[ ${DISASTER_BACKUP_ENABLED:-false} == true ]] || die \
|
|
"Externes Backup ist noch nicht freigeschaltet (DISASTER_BACKUP_ENABLED=true)."
|
|
[[ -n ${RESTIC_REPOSITORY:-} ]] || die "RESTIC_REPOSITORY fehlt."
|
|
if [[ -n ${RESTIC_REQUIRE_MOUNT:-} ]]; then
|
|
mountpoint -q "$RESTIC_REQUIRE_MOUNT" || die \
|
|
"Externes Backupziel ist nicht eingehängt: $RESTIC_REQUIRE_MOUNT"
|
|
fi
|
|
[[ -n ${RESTIC_PASSWORD_FILE:-} && -r $RESTIC_PASSWORD_FILE ]] || die \
|
|
"RESTIC_PASSWORD_FILE fehlt oder ist nicht lesbar."
|
|
command -v restic >/dev/null || die "restic ist nicht installiert."
|
|
command -v docker >/dev/null || die "Docker ist nicht installiert."
|
|
exec 9>/run/lock/athena-disaster-backup.lock
|
|
flock -n 9 || die "Ein Disaster-Backup läuft bereits."
|
|
|
|
install -d -m 0700 "$STATE/latest"
|
|
|
|
log "Konsistentes Docker-Schnellbackup erzeugen"
|
|
docker inspect mike-ai-backup >/dev/null 2>&1 || die "mike-ai-backup fehlt."
|
|
docker exec mike-ai-backup backup
|
|
latest=$(readlink -f /data/docker-backups/athena-latest.tar.gz)
|
|
[[ -s $latest ]] || die "Lokales Docker-Backup wurde nicht erzeugt."
|
|
gzip -t "$latest" || die "Lokales Docker-Backup ist beschädigt."
|
|
install -m 0600 "$latest" "$STATE/latest/docker-state.tar.gz"
|
|
sha256sum "$STATE/latest/docker-state.tar.gz" >"$STATE/latest/docker-state.tar.gz.sha256"
|
|
|
|
log "Wiederaufbau-Metadaten erfassen"
|
|
{
|
|
printf 'created_utc=%s\n' "$(date -u +%FT%TZ)"
|
|
printf 'hostname=%s\n' "$(hostname)"
|
|
printf 'source_commit=%s\n' "$(git -C /opt/mike-ai/stack rev-parse HEAD 2>/dev/null || printf unknown)"
|
|
findmnt -rn -o SOURCE,UUID,FSTYPE,TARGET / /data 2>/dev/null || true
|
|
} >"$STATE/latest/manifest.txt"
|
|
find /data/models -type f -printf '%P\t%s\n' 2>/dev/null | sort \
|
|
>"$STATE/latest/model-manifest.tsv"
|
|
docker ps -a --format '{{.Names}}\t{{.Image}}\t{{.Status}}' \
|
|
>"$STATE/latest/container-manifest.tsv"
|
|
|
|
paths=(/etc/mike-ai /opt/mike-ai "$STATE/latest")
|
|
for path in \
|
|
/data/voice /data/music /data/audio /data/llama-dashboard \
|
|
/data/mike-ai-operator /data/benchmarks /data/model-benchmarks \
|
|
/data/image-comparison /data/backups /data/deploy-backups; do
|
|
[[ ! -e $path ]] || paths+=("$path")
|
|
done
|
|
|
|
tag=${RESTIC_TAG:-athena-disaster}
|
|
log "Verschlüsseltes externes Backup schreiben"
|
|
if ! restic snapshots >/dev/null 2>&1; then
|
|
log "Neues Restic-Repository initialisieren"
|
|
restic init
|
|
fi
|
|
restic backup --tag "$tag" "${paths[@]}"
|
|
|
|
log "Aufbewahrung anwenden"
|
|
restic forget --tag "$tag" \
|
|
--keep-daily "${RESTIC_KEEP_DAILY:-14}" \
|
|
--keep-weekly "${RESTIC_KEEP_WEEKLY:-8}" \
|
|
--keep-monthly "${RESTIC_KEEP_MONTHLY:-12}" --prune
|
|
|
|
log "Letzten Snapshot verifizieren"
|
|
restic snapshots --tag "$tag" --latest 1
|
|
restic check
|
|
printf 'ATHENA_DISASTER_BACKUP_OK\n'
|