#!/usr/bin/env bash # ============================================================================= # borg-backup-postgres.sh # Pull-basiertes Borg Backup für PostgreSQL via SSH # Läuft auf dem Raspberry Pi (Backup-Ziel) # ============================================================================= set -euo pipefail # --- Konfigurationsdatei laden ----------------------------------------------- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CONFIG_FILE="${SCRIPT_DIR}/../config/backup.conf" if [[ ! -f "$CONFIG_FILE" ]]; then echo "[FEHLER] Konfigurationsdatei nicht gefunden: $CONFIG_FILE" >&2 exit 1 fi # shellcheck source=/dev/null source "$CONFIG_FILE" # --- Farben & Logging -------------------------------------------------------- RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color log() { echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $*"; } log_ok() { echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] ✓${NC} $*"; } log_warn(){ echo -e "${YELLOW}[$(date '+%Y-%m-%d %H:%M:%S')] ⚠${NC} $*"; } log_err() { echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')] ✗${NC} $*" >&2; } # --- Umgebungsvariablen für Borg setzen -------------------------------------- export BORG_PASSPHRASE export BORG_REPO export BORG_RSH="ssh -i ${SSH_KEY_PATH} -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10" # --- Fehlerbehandlung -------------------------------------------------------- BACKUP_START=$(date +%s) FAILED_DBS=() cleanup() { local exit_code=$? if [[ $exit_code -ne 0 ]]; then log_err "Backup mit Fehlercode $exit_code beendet!" send_notification "FEHLER" "Borg Backup fehlgeschlagen (Exit: $exit_code). Fehlgeschlagene DBs: ${FAILED_DBS[*]:-keine}" fi } trap cleanup EXIT # --- Hilfsfunktionen --------------------------------------------------------- check_dependencies() { local missing=() for cmd in borg ssh pg_restore; do if ! command -v "$cmd" &>/dev/null; then missing+=("$cmd") fi done if [[ ${#missing[@]} -gt 0 ]]; then log_err "Fehlende Programme: ${missing[*]}" exit 1 fi } check_ssh_connection() { log "Prüfe SSH-Verbindung zu ${PG_SSH_USER}@${PG_HOST}..." if ! ssh -i "${SSH_KEY_PATH}" \ -o StrictHostKeyChecking=accept-new \ -o ConnectTimeout=10 \ -o BatchMode=yes \ "${PG_SSH_USER}@${PG_HOST}" "echo ok" &>/dev/null; then log_err "SSH-Verbindung zu ${PG_HOST} fehlgeschlagen!" exit 1 fi log_ok "SSH-Verbindung erfolgreich." } check_repo() { log "Prüfe Borg-Repository: ${BORG_REPO}" if ! borg info &>/dev/null; then log_warn "Repository existiert nicht. Initialisiere..." borg init --encryption=repokey "${BORG_REPO}" log_ok "Repository initialisiert." log_warn "WICHTIG: Exportiere den Borg-Key jetzt:" log_warn " borg key export ${BORG_REPO} ~/borg-key-backup.txt" else log_ok "Repository OK." fi } send_notification() { local status="$1" local message="$2" # Nur senden wenn konfiguriert if [[ -n "${NOTIFY_EMAIL:-}" ]] && command -v mail &>/dev/null; then echo "$message" | mail -s "[Borg Backup] ${status}" "${NOTIFY_EMAIL}" fi # Systemd-Journal / Wall-Nachricht if [[ "${NOTIFY_WALL:-false}" == "true" ]]; then wall "[Borg Backup] ${status}: ${message}" 2>/dev/null || true fi } discover_databases() { log "Frage alle Datenbanken vom Server ab..." # psql gibt eine Zeile pro DB aus; Systemdatenbanken ausschließen local list_cmd="sudo -u ${PG_DB_USER} psql -At -c \ \"SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname;\"" local all_dbs all_dbs=$(ssh -i "${SSH_KEY_PATH}" \ -o StrictHostKeyChecking=accept-new \ -o ConnectTimeout=10 \ -o BatchMode=yes \ "${PG_SSH_USER}@${PG_HOST}" \ "${list_cmd}") || { log_err "Datenbankabfrage fehlgeschlagen!" exit 1 } # Ausschlussliste anwenden local filtered=() while IFS= read -r db; do [[ -z "$db" ]] && continue local excluded=false for excl in "${PG_EXCLUDE_DATABASES[@]:-}"; do [[ "$db" == "$excl" ]] && excluded=true && break done if $excluded; then log_warn "Überspringe (ausgeschlossen): ${db}" else filtered+=("$db") fi done <<< "$all_dbs" log_ok "Gefundene Datenbanken: ${filtered[*]}" # Ergebnis per nameref zurückgeben local -n _result=$1 _result=("${filtered[@]}") } backup_database() { local db="$1" local archive_name="${db}-$(date +%Y-%m-%dT%H:%M)" log "Sichere Datenbank: ${db} → ${archive_name}" local dump_cmd="sudo -u ${PG_DB_USER} pg_dump --format=custom --no-password ${db}" if ssh -i "${SSH_KEY_PATH}" \ -o StrictHostKeyChecking=accept-new \ -o ConnectTimeout=10 \ -o BatchMode=yes \ "${PG_SSH_USER}@${PG_HOST}" \ "${dump_cmd}" \ | borg create \ --stdin-name "${db}.pgdump" \ --compression "${BORG_COMPRESSION:-lz4}" \ --stats \ --show-rc \ "${BORG_REPO}::${archive_name}" -; then log_ok "Datenbank '${db}' erfolgreich gesichert." else log_err "Backup von Datenbank '${db}' fehlgeschlagen!" FAILED_DBS+=("$db") return 1 fi } run_prune() { log "Bereinige alte Backups (Retention Policy)..." borg prune \ --keep-daily "${KEEP_DAILY:-7}" \ --keep-weekly "${KEEP_WEEKLY:-4}" \ --keep-monthly "${KEEP_MONTHLY:-6}" \ --glob-archives '*' \ --stats \ --show-rc \ "${BORG_REPO}" log_ok "Bereinigung abgeschlossen." } run_compact() { # borg compact erst ab Version 1.2 verfügbar if borg --version | grep -qE "borg 1\.[2-9]|borg [2-9]"; then log "Kompaktiere Repository..." borg compact "${BORG_REPO}" && log_ok "Kompaktierung abgeschlossen." fi } # --- Hauptprogramm ----------------------------------------------------------- main() { log "========================================" log " Borg PostgreSQL Backup gestartet" log " Host: ${PG_HOST}" log " Repo: ${BORG_REPO}" log "========================================" check_dependencies check_ssh_connection check_repo # Datenbankliste ermitteln: automatisch oder manuell local databases=() if [[ "${PG_BACKUP_ALL:-false}" == "true" ]]; then discover_databases databases else databases=("${PG_DATABASES[@]}") log "Manuell konfigurierte Datenbanken: ${databases[*]}" fi if [[ ${#databases[@]} -eq 0 ]]; then log_err "Keine Datenbanken zum Sichern gefunden!" exit 1 fi # Alle Datenbanken sichern local success_count=0 for db in "${databases[@]}"; do if backup_database "$db"; then ((success_count++)) || true fi done # Prune & Compact nur wenn mindestens ein Backup erfolgreich if [[ $success_count -gt 0 ]]; then run_prune run_compact fi # Zusammenfassung local duration=$(( $(date +%s) - BACKUP_START )) local total=${#databases[@]} log "========================================" log_ok "Backup abgeschlossen in ${duration}s" log_ok "Erfolgreich: ${success_count}/${total} Datenbanken" if [[ ${#FAILED_DBS[@]} -gt 0 ]]; then log_err "Fehlgeschlagen: ${FAILED_DBS[*]}" send_notification "WARNUNG" "${success_count}/${total} DBs gesichert. Fehlgeschlagen: ${FAILED_DBS[*]}" exit 1 else send_notification "OK" "Alle ${total} Datenbanken erfolgreich gesichert (${duration}s)." fi log "========================================" } main "$@"