#!/bin/bash # Бэкап серверов на rpi (вариант «малина забирает»): ssh к серверу → forced command отдаёт # tar|zstd|age, зашифрованный ОТКРЫТЫМ ключом. Закрытого ключа на малине нет — расшифровать # здесь ничего нельзя (ключ у владельца: Mac ~/.config/age/servers-backup.key + резервная копия). # # servers-backup all — ежедневный прогон (таймер): fr1 daily, ru1 daily, по воскресеньям fr1 weekly # servers-backup verify — сверка sha256 всех хранимых архивов (порча на диске) # # Хранение: /daily — 14 последних, /weekly — 8, /monthly — 12 (хардлинк # первого daily за месяц, места не занимает). set -uo pipefail ROOT=/mnt/backup/servers KEY=/etc/server-backup/id_ed25519 KH=/etc/server-backup/known_hosts declare -A TARGET=([fr1]=rus@161.97.93.252 [ru1]=mik@85.198.109.86) declare -A KEEP=([daily]=14 [weekly]=8 [monthly]=12) log() { echo "$*"; } pull() { # pull local host=$1 profile=$2 dir="$ROOT/$1/$2" ts f rc want got mkdir -p "$dir" ts=$(date +%F_%H%M); f="$dir/$host-$profile-$ts.tar.zst.age" ssh -i "$KEY" -o UserKnownHostsFile="$KH" -o StrictHostKeyChecking=yes -o BatchMode=yes \ -o ConnectTimeout=30 -o ServerAliveInterval=30 -o ServerAliveCountMax=6 \ "${TARGET[$host]}" "$profile" > "$f.part" 2> "$f.stderr" rc=$? want=$(grep '^SHA256 ' "$f.stderr" | tail -1 | cut -d' ' -f2) got=$(sha256sum "$f.part" | cut -d' ' -f1) if [ $rc -ne 0 ] || [ -z "$want" ] || [ "$want" != "$got" ] || [ ! -s "$f.part" ]; then log "FAIL $host/$profile: ssh rc=$rc, sha сервера=${want:-нет}, sha принятого=$got" sed 's/^/ stderr: /' "$f.stderr" | tail -20 rm -f "$f.part" "$f.stderr" return 1 fi mv "$f.part" "$f" echo "$got $(basename "$f")" > "$f.sha256" grep -v '^SHA256 ' "$f.stderr" | tail -50 > "$f.log"; [ -s "$f.log" ] || rm -f "$f.log" rm -f "$f.stderr" log "OK $host/$profile: $(du -h "$f" | cut -f1) $(basename "$f")" # первый daily за месяц → monthly (хардлинк) if [ "$profile" = daily ] && ! ls "$ROOT/$host/monthly/$host-daily-$(date +%Y-%m)"-* >/dev/null 2>&1; then mkdir -p "$ROOT/$host/monthly"; ln "$f" "$f.sha256" "$ROOT/$host/monthly/" fi } prune() { local d keep for d in "$ROOT"/*/{daily,weekly,monthly}; do [ -d "$d" ] || continue keep=${KEEP[$(basename "$d")]} ls -1t "$d"/*.age 2>/dev/null | tail -n +$((keep + 1)) | while read -r old; do rm -f "$old" "$old.sha256" "$old.log"; log "prune $(basename "$old")" done done } verify() { local bad=0 n=0 s while read -r s; do n=$((n + 1)) (cd "$(dirname "$s")" && sha256sum --quiet -c "$(basename "$s")") || { log "BAD $s"; bad=1; } done < <(find "$ROOT" -name '*.sha256') log "verify: проверено $n архивов, $([ $bad = 0 ] && echo 'всё цело' || echo 'ЕСТЬ ПОВРЕЖДЁННЫЕ')" return $bad } mountpoint -q /mnt/backup || { echo "/mnt/backup не смонтирован — пропуск"; exit 1; } install -d -m 700 "$ROOT" case "${1:-all}" in all) rc=0 pull fr1 daily || rc=1 pull ru1 daily || rc=1 [ "$(date +%u)" = 7 ] && { pull fr1 weekly || rc=1; } prune [ "$(date +%u)" = 7 ] && { verify || rc=1; } exit $rc ;; verify) verify ;; pull) pull "$2" "$3" ;; *) echo "usage: servers-backup all|verify|pull " >&2; exit 2 ;; esac