#!/bin/bash # Выгрузка /mnt/backup в Yandex Object Storage (S3) через restic (без каталога photos). # Фото бэкапятся отдельно: backup-restic-yandex-photos.sh. # Запускать на хосте Proxmox под root. # Секреты: из Vaultwarden (объект RESTIC). Требуется файл с мастер-паролем: /root/.bw-master (chmod 600). # Перед первым запуском: установить restic, bw (Bitwarden CLI), jq; bw config server https://vault.katykhin.ru; restic init. # Cron: 0 4 * * * (04:00, после окна 01:00–03:30; 05:00 зарезервировано под перезагрузку). set -e BACKUP_PATH="/mnt/backup" # Время запуска (для логов и уведомлений) START_TS=$(date +%s) START_HUMAN=$(date '+%Y-%m-%d %H:%M:%S') # Исключаем служебные каталоги и photos (фото — отдельный бэкап) EXCLUDE_OPTS=(--exclude="$BACKUP_PATH/lost+found" --exclude="$BACKUP_PATH/photos") if [ "$(id -u)" -ne 0 ]; then echo "Запускайте под root." exit 1 fi # Секреты из Vaultwarden (объект RESTIC) BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" if [ ! -f "$BW_MASTER_PASSWORD_FILE" ]; then echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE (chmod 600). См. docs/vaultwarden-secrets.md" exit 1 fi if ! command -v bw >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then echo "Установите bw (Bitwarden CLI) и jq для получения секретов из Vaultwarden." exit 1 fi export BW_SESSION BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw 2>/dev/null) || { echo "Не удалось разблокировать Vaultwarden. Проверьте мастер-пароль и доступ к vault.katykhin.ru."; exit 1; } RESTIC_ITEM=$(bw get item "RESTIC" 2>/dev/null) || { echo "Не найден объект RESTIC в Vaultwarden."; exit 1; } export RESTIC_REPOSITORY RESTIC_REPOSITORY=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="RESTIC_REPOSITORY") | .value') export AWS_ACCESS_KEY_ID AWS_ACCESS_KEY_ID=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="AWS_ACCESS_KEY_ID") | .value') export AWS_SECRET_ACCESS_KEY AWS_SECRET_ACCESS_KEY=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="AWS_SECRET_ACCESS_KEY") | .value') export AWS_DEFAULT_REGION AWS_DEFAULT_REGION=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="AWS_DEFAULT_REGION") | .value') [ -z "$AWS_DEFAULT_REGION" ] && AWS_DEFAULT_REGION="ru-central1" RESTIC_PASS=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="RESTIC_BACKUP_KEY") | .value') for var in RESTIC_REPOSITORY AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY; do if [ -z "${!var}" ]; then echo "В Vaultwarden (RESTIC) не задано поле для $var" exit 1 fi done RESTIC_PASSWORD_FILE=$(mktemp -u) echo -n "$RESTIC_PASS" > "$RESTIC_PASSWORD_FILE" chmod 600 "$RESTIC_PASSWORD_FILE" trap 'rm -f "$RESTIC_PASSWORD_FILE"' EXIT INT TERM export RESTIC_PASSWORD_FILE if ! command -v restic >/dev/null 2>&1; then echo "restic не установлен. Установите: apt install restic." exit 1 fi echo "Restic backup: $BACKUP_PATH (excl. photos) -> $RESTIC_REPOSITORY" # Показываем прогресс restic (без --quiet), чтобы был виден ход бэкапа restic backup "$BACKUP_PATH" "${EXCLUDE_OPTS[@]}" echo "Restic forget (retention 3 daily, 2 weekly, 2 monthly)..." restic forget --keep-daily 3 --keep-weekly 2 --keep-monthly 2 --prune --quiet echo "Restic prune..." restic prune --quiet # Время окончания и длительность END_TS=$(date +%s) END_HUMAN=$(date '+%Y-%m-%d %H:%M:%S') DURATION_SEC=$(( END_TS - START_TS )) if [ "$DURATION_SEC" -lt 0 ] 2>/dev/null; then DURATION_SEC=0 fi DUR_MIN=$(( DURATION_SEC / 60 )) DUR_SEC=$(( DURATION_SEC % 60 )) echo "Restic backup done." echo "Время запуска: $START_HUMAN" echo "Время завершения: $END_HUMAN" echo "Длительность: ${DUR_MIN} мин ${DUR_SEC} сек" # Уведомление в Telegram (шлюз тихо выходит, если конфига нет) NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}" if [ -x "$NOTIFY_SCRIPT" ]; then STATS=$(restic stats latest 2>/dev/null) || true FILES=$(echo "$STATS" | grep "Total File Count" | sed 's/.*:[[:space:]]*//') SIZE=$(echo "$STATS" | grep "Total Size" | sed 's/.*:[[:space:]]*//') if [ -n "$FILES" ] && [ -n "$SIZE" ]; then BODY="Резервное копирование завершено. Объекты: снимок /mnt/backup в Yandex (без photos). Файлов в снимке: $FILES. Размер копии: ${SIZE}. Время запуска: ${START_HUMAN}. Время завершения: ${END_HUMAN}. Длительность: ${DUR_MIN} мин ${DUR_SEC} сек." "$NOTIFY_SCRIPT" "☁️ Restic Yandex" "$BODY" || true else BODY="Резервное копирование завершено. Объекты: снимок /mnt/backup в Yandex (без photos). Размер копии: — (stats недоступны). Время запуска: ${START_HUMAN}. Время завершения: ${END_HUMAN}. Длительность: ${DUR_MIN} мин ${DUR_SEC} сек." "$NOTIFY_SCRIPT" "☁️ Restic Yandex" "$BODY" || true fi fi