Add notification feature to backup scripts for various services
Enhance backup scripts for Nextcloud, Gitea, Paperless, Vaultwarden, Immich, and VPS configurations by adding Telegram notifications upon completion. Include details such as backup size and objects backed up. Update backup documentation to reflect these changes and ensure clarity on backup processes and retention policies.
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
# Запускать на хосте Proxmox под root. Использует pct exec (SSH не нужен).
|
||||
# Результат: /mnt/backup/databases/ct101-nextcloud/nextcloud-db-YYYYMMDD-HHMM.sql.gz
|
||||
set -e
|
||||
# Чтобы из cron находились bw и jq (часто в /usr/local/bin)
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||||
|
||||
CT_ID=101
|
||||
BACKUP_DIR="/mnt/backup/databases/ct101-nextcloud"
|
||||
@@ -16,18 +18,56 @@ if [ "$(id -u)" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Минимальный размер дампа (байт). Пустой gzip ≈ 20 байт — значит pg_dump не отдал данные.
|
||||
MIN_BACKUP_BYTES=512
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
DATE=$(date +%Y%m%d-%H%M)
|
||||
OUTPUT="$BACKUP_DIR/nextcloud-db-$DATE.sql.gz"
|
||||
ERR=$(mktemp)
|
||||
trap "rm -f '$ERR'" EXIT
|
||||
|
||||
pct exec $CT_ID -- docker exec "$PG_CONTAINER" pg_dump -U nextcloud nextcloud 2>/dev/null | gzip > "$OUTPUT"
|
||||
# PGPASSWORD из Vaultwarden (объект NEXTCLOUD: поле dbpassword или пароль). См. docs/backup/proxmox-phase1-backup.md
|
||||
PG_ENV_ARGS=""
|
||||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
if [ -f "$BW_MASTER_PASSWORD_FILE" ] && command -v bw >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then
|
||||
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw 2>/dev/null) || true
|
||||
if [ -n "${BW_SESSION:-}" ]; then
|
||||
PGPASS=$(bw get item "NEXTCLOUD" 2>/dev/null | jq -r '.fields[] | select(.name=="dbpassword") | .value')
|
||||
[ -z "$PGPASS" ] && PGPASS=$(bw get password "NEXTCLOUD" 2>/dev/null)
|
||||
[ -n "$PGPASS" ] && PG_ENV_ARGS="-e PGPASSWORD=$PGPASS"
|
||||
fi
|
||||
fi
|
||||
pct exec $CT_ID -- docker exec $PG_ENV_ARGS "$PG_CONTAINER" pg_dump -U nextcloud nextcloud 2>"$ERR" | gzip > "$OUTPUT"
|
||||
|
||||
if [ -s "$OUTPUT" ]; then
|
||||
echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
|
||||
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||
if [ -s "$OUTPUT" ] && [ "$SIZE_BYTES" -ge "$MIN_BACKUP_BYTES" ]; then
|
||||
echo "Создан: $OUTPUT ($(du --apparent-size -h "$OUTPUT" | cut -f1))"
|
||||
# Проверка: несжатый размер дампа (2GB БД на диске → 200–600MB SQL нормально: индексы не в дампе, потом gzip)
|
||||
if [ "${VERIFY_BACKUP:-0}" = "1" ]; then
|
||||
UNCOMPRESSED=$(gunzip -c "$OUTPUT" 2>/dev/null | wc -c)
|
||||
UNCOMPRESSED_MB=$(( UNCOMPRESSED / 1024 / 1024 ))
|
||||
echo "Несжатый размер дампа: ${UNCOMPRESSED_MB} MB (${UNCOMPRESSED} B)"
|
||||
TABLES_IN_DUMP=$(gunzip -c "$OUTPUT" 2>/dev/null | grep -c '^CREATE TABLE ' || true)
|
||||
echo "Таблиц в дампе: $TABLES_IN_DUMP"
|
||||
fi
|
||||
else
|
||||
echo "Ошибка: дамп пустой или контейнер недоступен."
|
||||
echo "Ошибка: дамп пустой или слишком мал (${SIZE_BYTES} байт). Проверьте контейнер $PG_CONTAINER и пароль БД в Vaultwarden (NEXTCLOUD)."
|
||||
[ -s "$ERR" ] && cat "$ERR" >&2
|
||||
rm -f "$OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
find "$BACKUP_DIR" -name 'nextcloud-db-*.sql.gz' -mtime +$RETENTION_DAYS -delete
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
SIZE=$(du --apparent-size -h "$OUTPUT" | cut -f1)
|
||||
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: дамп БД Nextcloud (PostgreSQL).
|
||||
Размер копии: ${SIZE}."
|
||||
[ "$SIZE_BYTES" -lt 10240 ] 2>/dev/null && BODY="${BODY}
|
||||
⚠️ Подозрительно малый размер — проверьте контейнер nextcloud-db-1 и наличие данных в БД."
|
||||
"$NOTIFY_SCRIPT" "🗄️ Nextcloud (БД)" "$BODY" || true
|
||||
fi
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# Запускать на хосте Proxmox под root. Использует pct exec.
|
||||
# Результат: /mnt/backup/databases/ct103-gitea/gitea-db-YYYYMMDD-HHMM.sql.gz
|
||||
set -e
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||||
|
||||
CT_ID=103
|
||||
BACKUP_DIR="/mnt/backup/databases/ct103-gitea"
|
||||
@@ -14,18 +15,47 @@ if [ "$(id -u)" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Минимальный размер дампа (байт). Пустой gzip ≈ 20 байт — значит pg_dump не отдал данные.
|
||||
MIN_BACKUP_BYTES=512
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
DATE=$(date +%Y%m%d-%H%M)
|
||||
OUTPUT="$BACKUP_DIR/gitea-db-$DATE.sql.gz"
|
||||
ERR=$(mktemp)
|
||||
trap "rm -f '$ERR'" EXIT
|
||||
|
||||
pct exec $CT_ID -- docker exec "$PG_CONTAINER" pg_dump -U gitea gitea 2>/dev/null | gzip > "$OUTPUT"
|
||||
# PGPASSWORD из Vaultwarden (объект GITEA, пароль). См. docs/backup/proxmox-phase1-backup.md
|
||||
PG_ENV_ARGS=""
|
||||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
if [ -f "$BW_MASTER_PASSWORD_FILE" ] && command -v bw >/dev/null 2>&1; then
|
||||
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw 2>/dev/null) || true
|
||||
if [ -n "${BW_SESSION:-}" ]; then
|
||||
PGPASS=$(bw get password "GITEA" 2>/dev/null)
|
||||
[ -n "$PGPASS" ] && PG_ENV_ARGS="-e PGPASSWORD=$PGPASS"
|
||||
fi
|
||||
fi
|
||||
pct exec $CT_ID -- docker exec $PG_ENV_ARGS "$PG_CONTAINER" pg_dump -U gitea gitea 2>"$ERR" | gzip > "$OUTPUT"
|
||||
|
||||
if [ -s "$OUTPUT" ]; then
|
||||
echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
|
||||
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||
if [ -s "$OUTPUT" ] && [ "$SIZE_BYTES" -ge "$MIN_BACKUP_BYTES" ]; then
|
||||
echo "Создан: $OUTPUT ($(du --apparent-size -h "$OUTPUT" | cut -f1))"
|
||||
else
|
||||
echo "Ошибка: дамп пустой или контейнер недоступен."
|
||||
echo "Ошибка: дамп пустой или слишком мал (${SIZE_BYTES} байт). Проверьте контейнер $PG_CONTAINER и пароль БД в Vaultwarden (GITEA)."
|
||||
[ -s "$ERR" ] && cat "$ERR" >&2
|
||||
rm -f "$OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
find "$BACKUP_DIR" -name 'gitea-db-*.sql.gz' -mtime +$RETENTION_DAYS -delete
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
SIZE=$(du --apparent-size -h "$OUTPUT" | cut -f1)
|
||||
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: дамп БД Gitea (PostgreSQL).
|
||||
Размер копии: ${SIZE}."
|
||||
[ "$SIZE_BYTES" -lt 10240 ] 2>/dev/null && BODY="${BODY}
|
||||
⚠️ Подозрительно малый размер — проверьте контейнер gitea-db-1 и наличие данных в БД."
|
||||
"$NOTIFY_SCRIPT" "🗄️ Gitea (БД)" "$BODY" || true
|
||||
fi
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# Запускать на хосте Proxmox под root. Использует pct exec.
|
||||
# Результат: /mnt/backup/databases/ct104-paperless/paperless-db-YYYYMMDD-HHMM.sql.gz
|
||||
set -e
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||||
|
||||
CT_ID=104
|
||||
BACKUP_DIR="/mnt/backup/databases/ct104-paperless"
|
||||
@@ -14,18 +15,47 @@ if [ "$(id -u)" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Минимальный размер дампа (байт). Пустой gzip ≈ 20 байт — значит pg_dump не отдал данные.
|
||||
MIN_BACKUP_BYTES=512
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
DATE=$(date +%Y%m%d-%H%M)
|
||||
OUTPUT="$BACKUP_DIR/paperless-db-$DATE.sql.gz"
|
||||
ERR=$(mktemp)
|
||||
trap "rm -f '$ERR'" EXIT
|
||||
|
||||
pct exec $CT_ID -- docker exec "$PG_CONTAINER" pg_dump -U paperless paperless 2>/dev/null | gzip > "$OUTPUT"
|
||||
# PGPASSWORD из Vaultwarden (объект PAPERLESS, пароль). См. docs/backup/proxmox-phase1-backup.md
|
||||
PG_ENV_ARGS=""
|
||||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
if [ -f "$BW_MASTER_PASSWORD_FILE" ] && command -v bw >/dev/null 2>&1; then
|
||||
export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw 2>/dev/null) || true
|
||||
if [ -n "${BW_SESSION:-}" ]; then
|
||||
PGPASS=$(bw get password "PAPERLESS" 2>/dev/null)
|
||||
[ -n "$PGPASS" ] && PG_ENV_ARGS="-e PGPASSWORD=$PGPASS"
|
||||
fi
|
||||
fi
|
||||
pct exec $CT_ID -- docker exec $PG_ENV_ARGS "$PG_CONTAINER" pg_dump -U paperless paperless 2>"$ERR" | gzip > "$OUTPUT"
|
||||
|
||||
if [ -s "$OUTPUT" ]; then
|
||||
echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
|
||||
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||
if [ -s "$OUTPUT" ] && [ "$SIZE_BYTES" -ge "$MIN_BACKUP_BYTES" ]; then
|
||||
echo "Создан: $OUTPUT ($(du --apparent-size -h "$OUTPUT" | cut -f1))"
|
||||
else
|
||||
echo "Ошибка: дамп пустой или контейнер недоступен."
|
||||
echo "Ошибка: дамп пустой или слишком мал (${SIZE_BYTES} байт). Проверьте контейнер $PG_CONTAINER и пароль БД в Vaultwarden (PAPERLESS)."
|
||||
[ -s "$ERR" ] && cat "$ERR" >&2
|
||||
rm -f "$OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
find "$BACKUP_DIR" -name 'paperless-db-*.sql.gz' -mtime +$RETENTION_DAYS -delete
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
SIZE=$(du --apparent-size -h "$OUTPUT" | cut -f1)
|
||||
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: дамп БД Paperless (PostgreSQL).
|
||||
Размер копии: ${SIZE}."
|
||||
[ "$SIZE_BYTES" -lt 10240 ] 2>/dev/null && BODY="${BODY}
|
||||
⚠️ Подозрительно малый размер — проверьте контейнер paperless-db-1 и наличие данных в БД."
|
||||
"$NOTIFY_SCRIPT" "🗄️ Paperless (БД)" "$BODY" || true
|
||||
fi
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
# Запускать на хосте Proxmox под root. Использует pct exec.
|
||||
# Результат: /mnt/backup/other/ct105-vectors/vectors-YYYYMMDD-HHMM.tar.gz
|
||||
set -e
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||||
|
||||
CT_ID=105
|
||||
REMOTE_PATH="/home/rag-service/data/vectors"
|
||||
BACKUP_DIR="/mnt/backup/other/ct105-vectors"
|
||||
RETENTION_DAYS=14
|
||||
|
||||
# Минимальный размер архива (байт). Пустой gzip ≈ 20 байт — каталог пуст или путь неверный.
|
||||
MIN_BACKUP_BYTES=512
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Запускайте под root."
|
||||
exit 1
|
||||
@@ -17,15 +20,31 @@ fi
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
DATE=$(date +%Y%m%d-%H%M)
|
||||
OUTPUT="$BACKUP_DIR/vectors-$DATE.tar.gz"
|
||||
ERR=$(mktemp)
|
||||
trap "rm -f '$ERR'" EXIT
|
||||
|
||||
pct exec $CT_ID -- tar cf - -C /home/rag-service/data vectors 2>/dev/null | gzip > "$OUTPUT"
|
||||
pct exec $CT_ID -- tar cf - -C /home/rag-service/data vectors 2>"$ERR" | gzip > "$OUTPUT"
|
||||
|
||||
if [ -s "$OUTPUT" ]; then
|
||||
echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
|
||||
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||
if [ -s "$OUTPUT" ] && [ "$SIZE_BYTES" -ge "$MIN_BACKUP_BYTES" ]; then
|
||||
echo "Создан: $OUTPUT ($(du --apparent-size -h "$OUTPUT" | cut -f1))"
|
||||
else
|
||||
echo "Ошибка: архив пустой или каталог недоступен."
|
||||
echo "Ошибка: архив пустой или слишком мал (${SIZE_BYTES} байт). Проверьте /home/rag-service/data/vectors в CT $CT_ID."
|
||||
[ -s "$ERR" ] && cat "$ERR" >&2
|
||||
rm -f "$OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
find "$BACKUP_DIR" -name 'vectors-*.tar.gz' -mtime +$RETENTION_DAYS -delete
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
SIZE=$(du --apparent-size -h "$OUTPUT" | cut -f1)
|
||||
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: архив векторов RAG (CT 105, vectors.npz).
|
||||
Размер копии: ${SIZE}."
|
||||
[ "$SIZE_BYTES" -lt 10240 ] 2>/dev/null && BODY="${BODY}
|
||||
⚠️ Подозрительно малый размер — проверьте /home/rag-service/data/vectors в CT 105."
|
||||
"$NOTIFY_SCRIPT" "📐 Векторы RAG" "$BODY" || true
|
||||
fi
|
||||
|
||||
@@ -21,3 +21,12 @@ chmod 600 "$BACKUP_ROOT"/etc-pve-*.tar.gz "$BACKUP_ROOT"/etc-host-configs-*.tar.
|
||||
|
||||
find "$BACKUP_ROOT" -name 'etc-pve-*.tar.gz' -mtime +$RETENTION_DAYS -delete
|
||||
find "$BACKUP_ROOT" -name 'etc-host-configs-*.tar.gz' -mtime +$RETENTION_DAYS -delete
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
SIZE=$(du -sh "$BACKUP_ROOT" 2>/dev/null | cut -f1) || true
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: архивы /etc/pve, конфиги сети (interfaces, hosts, resolv.conf).
|
||||
Размер копии: ${SIZE:-—}."
|
||||
"$NOTIFY_SCRIPT" "⚙️ Конфиги хоста" "$BODY" || true
|
||||
fi
|
||||
|
||||
@@ -19,3 +19,12 @@ mkdir -p "$BACKUP_PATH"
|
||||
rsync -az --timeout=3600 \
|
||||
--exclude=".stfolder" \
|
||||
"$VM_SSH:$REMOTE_PATH/" "$BACKUP_PATH/"
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
SIZE=$(du -sh "$BACKUP_PATH" 2>/dev/null | cut -f1) || true
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: библиотека фото Immich (rsync с VM 200).
|
||||
Размер копии: ${SIZE:-—}."
|
||||
"$NOTIFY_SCRIPT" "📷 Фото Immich (rsync)" "$BODY" || true
|
||||
fi
|
||||
|
||||
@@ -1,46 +1,54 @@
|
||||
#!/bin/bash
|
||||
# Выгрузка только /mnt/backup/photos в Yandex Object Storage (S3) через restic.
|
||||
# Тот же репозиторий, что и backup-restic-yandex.sh; фото вынесены в отдельный снимок (больше всего данных).
|
||||
# Запускать на хосте Proxmox под root. Требуется тот же /root/.restic-yandex.env и /root/.restic-password.
|
||||
# Cron: 0 1 * * * (01:00).
|
||||
# Запускать на хосте Proxmox под root. Секреты из Vaultwarden (объект RESTIC), файл /root/.bw-master (chmod 600).
|
||||
# Cron: 10 4 * * * (04:10, после основного restic в 04:00).
|
||||
set -e
|
||||
|
||||
ENV_FILE="/root/.restic-yandex.env"
|
||||
BACKUP_PATH="/mnt/backup/photos"
|
||||
# Время запуска (для логов и уведомлений)
|
||||
START_TS=$(date +%s)
|
||||
START_HUMAN=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Запускайте под root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "Нет файла $ENV_FILE. Скопируйте restic-yandex-env.example и задайте ключи."
|
||||
# Секреты из 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/backup/proxmox-phase1-backup.md"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
source "$ENV_FILE"
|
||||
|
||||
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."; 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 "В $ENV_FILE не задано: $var"
|
||||
echo "В Vaultwarden (RESTIC) не задано поле для $var"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$RESTIC_PASSWORD_FILE" ]; then
|
||||
RESTIC_PASSWORD_FILE="/root/.restic-password"
|
||||
fi
|
||||
if [ ! -f "$RESTIC_PASSWORD_FILE" ]; then
|
||||
echo "Файл с паролем репозитория не найден: $RESTIC_PASSWORD_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export AWS_ACCESS_KEY_ID
|
||||
export AWS_SECRET_ACCESS_KEY
|
||||
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
|
||||
export RESTIC_REPOSITORY
|
||||
export AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-ru-central1}"
|
||||
|
||||
if ! command -v restic >/dev/null 2>&1; then
|
||||
echo "restic не установлен. Установите: apt install restic."
|
||||
@@ -53,7 +61,8 @@ if [ ! -d "$BACKUP_PATH" ]; then
|
||||
fi
|
||||
|
||||
echo "Restic backup (photos): $BACKUP_PATH -> $RESTIC_REPOSITORY"
|
||||
restic backup "$BACKUP_PATH" --quiet
|
||||
# Показываем прогресс restic (без --quiet), чтобы был виден ход бэкапа
|
||||
restic backup "$BACKUP_PATH"
|
||||
|
||||
echo "Restic forget (retention 3 daily, 2 weekly, 2 monthly)..."
|
||||
restic forget --keep-daily 3 --keep-weekly 2 --keep-monthly 2 --prune --quiet
|
||||
@@ -61,4 +70,42 @@ 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 photos 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/photos в Yandex. Файлов в снимке: $FILES.
|
||||
Размер копии: ${SIZE}.
|
||||
Время запуска: ${START_HUMAN}.
|
||||
Время завершения: ${END_HUMAN}.
|
||||
Длительность: ${DUR_MIN} мин ${DUR_SEC} сек."
|
||||
"$NOTIFY_SCRIPT" "📷 Restic Yandex (photos)" "$BODY" || true
|
||||
else
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: снимок /mnt/backup/photos в Yandex.
|
||||
Размер копии: — (stats недоступны).
|
||||
Время запуска: ${START_HUMAN}.
|
||||
Время завершения: ${END_HUMAN}.
|
||||
Длительность: ${DUR_MIN} мин ${DUR_SEC} сек."
|
||||
"$NOTIFY_SCRIPT" "📷 Restic Yandex (photos)" "$BODY" || true
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
# Выгрузка /mnt/backup в Yandex Object Storage (S3) через restic (без каталога photos).
|
||||
# Фото бэкапятся отдельно: backup-restic-yandex-photos.sh.
|
||||
# Запускать на хосте Proxmox под root.
|
||||
# Перед первым запуском: установить restic, создать /root/.restic-yandex.env и /root/.restic-password, выполнить restic init.
|
||||
# Секреты: из 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
|
||||
|
||||
ENV_FILE="/root/.restic-yandex.env"
|
||||
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")
|
||||
|
||||
@@ -16,34 +19,40 @@ if [ "$(id -u)" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "Нет файла $ENV_FILE. Скопируйте restic-yandex-env.example и задайте ключи."
|
||||
# Секреты из 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/backup/proxmox-phase1-backup.md"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
source "$ENV_FILE"
|
||||
|
||||
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 "В $ENV_FILE не задано: $var"
|
||||
echo "В Vaultwarden (RESTIC) не задано поле для $var"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$RESTIC_PASSWORD_FILE" ]; then
|
||||
RESTIC_PASSWORD_FILE="/root/.restic-password"
|
||||
fi
|
||||
if [ ! -f "$RESTIC_PASSWORD_FILE" ]; then
|
||||
echo "Файл с паролем репозитория не найден: $RESTIC_PASSWORD_FILE. Создайте его и выполните restic init."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export AWS_ACCESS_KEY_ID
|
||||
export AWS_SECRET_ACCESS_KEY
|
||||
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
|
||||
export RESTIC_REPOSITORY
|
||||
export AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-ru-central1}"
|
||||
|
||||
if ! command -v restic >/dev/null 2>&1; then
|
||||
echo "restic не установлен. Установите: apt install restic."
|
||||
@@ -51,7 +60,8 @@ if ! command -v restic >/dev/null 2>&1; then
|
||||
fi
|
||||
|
||||
echo "Restic backup: $BACKUP_PATH (excl. photos) -> $RESTIC_REPOSITORY"
|
||||
restic backup "$BACKUP_PATH" "${EXCLUDE_OPTS[@]}" --quiet
|
||||
# Показываем прогресс 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
|
||||
@@ -59,4 +69,42 @@ 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
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
# Запускать на хосте Proxmox под root. Использует pct exec.
|
||||
# Результат: /mnt/backup/other/vaultwarden/vaultwarden-data-YYYYMMDD-HHMM.tar.gz
|
||||
set -e
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||||
|
||||
CT_ID=103
|
||||
REMOTE_PATH="/opt/docker/vaultwarden/data"
|
||||
@@ -15,19 +16,38 @@ if [ "$(id -u)" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Минимальный размер архива (байт). Пустой tar.gz ≈ 20 байт — значит каталог пуст или путь неверный.
|
||||
MIN_BACKUP_BYTES=512
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
DATE=$(date +%Y%m%d-%H%M)
|
||||
OUTPUT="$BACKUP_DIR/vaultwarden-data-$DATE.tar.gz"
|
||||
ERR=$(mktemp)
|
||||
trap "rm -f '$ERR'" EXIT
|
||||
|
||||
pct exec $CT_ID -- tar cf - -C /opt/docker/vaultwarden data 2>/dev/null | gzip > "$OUTPUT"
|
||||
pct exec $CT_ID -- tar cf - -C /opt/docker/vaultwarden data 2>"$ERR" | gzip > "$OUTPUT"
|
||||
|
||||
if [ -s "$OUTPUT" ]; then
|
||||
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||
if [ -s "$OUTPUT" ] && [ "$SIZE_BYTES" -ge "$MIN_BACKUP_BYTES" ]; then
|
||||
chmod 600 "$OUTPUT"
|
||||
echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
|
||||
echo "Создан: $OUTPUT ($(du --apparent-size -h "$OUTPUT" | cut -f1))"
|
||||
else
|
||||
echo "Ошибка: архив пустой или каталог недоступен."
|
||||
echo "Ошибка: архив пустой или слишком мал (${SIZE_BYTES} байт). Проверьте путь /opt/docker/vaultwarden/data в CT $CT_ID."
|
||||
[ -s "$ERR" ] && cat "$ERR" >&2
|
||||
rm -f "$OUTPUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
find "$BACKUP_DIR" -name 'vaultwarden-data-*.tar.gz' -mtime +$RETENTION_DAYS -delete
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
SIZE=$(du --apparent-size -h "$OUTPUT" | cut -f1)
|
||||
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: данные Vaultwarden (пароли).
|
||||
Размер копии: ${SIZE}."
|
||||
[ "$SIZE_BYTES" -lt 10240 ] 2>/dev/null && BODY="${BODY}
|
||||
⚠️ Подозрительно малый размер — проверьте /opt/docker/vaultwarden/data в CT 103."
|
||||
"$NOTIFY_SCRIPT" "🔐 Vaultwarden" "$BODY" || true
|
||||
fi
|
||||
|
||||
@@ -30,7 +30,7 @@ else
|
||||
fi
|
||||
|
||||
if [ -s "$OUTPUT" ]; then
|
||||
echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
|
||||
echo "Создан: $OUTPUT ($(du --apparent-size -h "$OUTPUT" | cut -f1))"
|
||||
else
|
||||
echo "Ошибка: дамп пустой или не создан."
|
||||
rm -f "$OUTPUT"
|
||||
@@ -39,3 +39,15 @@ fi
|
||||
|
||||
# Удалить дампы старше RETENTION_DAYS
|
||||
find "$BACKUP_DIR" -name 'immich-db-*.sql.gz' -mtime +$RETENTION_DAYS -delete
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
SIZE=$(du --apparent-size -h "$OUTPUT" | cut -f1)
|
||||
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: дамп БД Immich (PostgreSQL, VM 200).
|
||||
Размер копии: ${SIZE}."
|
||||
[ "$SIZE_BYTES" -lt 10240 ] 2>/dev/null && BODY="${BODY}
|
||||
⚠️ Подозрительно малый размер — проверьте скрипт на VM 200 и наличие данных в БД."
|
||||
"$NOTIFY_SCRIPT" "🗄️ Immich (БД)" "$BODY" || true
|
||||
fi
|
||||
|
||||
@@ -71,3 +71,12 @@ if [ -f "$S3_ENV" ]; then
|
||||
else
|
||||
echo "Подсказка: для бэкапа S3 создайте $S3_ENV с S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET_NAME."
|
||||
fi
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
SIZE=$(du -sh "$BACKUP_ROOT" 2>/dev/null | cut -f1) || true
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: БД, voice_users, S3 (telegram-helper-bot).
|
||||
Размер копии: ${SIZE:-—}."
|
||||
"$NOTIFY_SCRIPT" "🖥️ VPS Миран" "$BODY" || true
|
||||
fi
|
||||
|
||||
@@ -37,6 +37,18 @@ ssh "${SSH_OPTS[@]}" "${VPS_USER}@${VPS_HOST}" "tar -chzf - -C / \
|
||||
var/www/katykhin.store" > "$ARCHIVE"
|
||||
|
||||
chmod 600 "$ARCHIVE"
|
||||
echo "Бэкап MTProto (VPS DE): $ARCHIVE ($(du -h "$ARCHIVE" | cut -f1))"
|
||||
echo "Бэкап MTProto (VPS DE): $ARCHIVE ($(du --apparent-size -h "$ARCHIVE" | cut -f1))"
|
||||
|
||||
find "$BACKUP_ROOT" -name 'mtproto-config-*.tar.gz' -mtime +$RETENTION_DAYS -delete
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
SIZE=$(du --apparent-size -h "$ARCHIVE" | cut -f1)
|
||||
SIZE_BYTES=$(stat -c%s "$ARCHIVE" 2>/dev/null || echo 0)
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: конфиги MTProto, nginx, Let's Encrypt, сайт (VPS DE).
|
||||
Размер копии: ${SIZE}."
|
||||
[ "$SIZE_BYTES" -lt 1024 ] 2>/dev/null && BODY="${BODY}
|
||||
⚠️ Подозрительно малый размер — проверьте SSH и наличие файлов на VPS."
|
||||
"$NOTIFY_SCRIPT" "🌐 VPS MTProto (DE)" "$BODY" || true
|
||||
fi
|
||||
|
||||
63
scripts/notify-telegram.sh
Normal file
63
scripts/notify-telegram.sh
Normal file
@@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
# Единая точка отправки уведомлений в Telegram (шлюз).
|
||||
# Вызывают скрипты бэкапов на хосте Proxmox. Позже тот же шлюз можно вызывать с VM 200 / VPS по SSH.
|
||||
# Использование: notify-telegram.sh "Заголовок" "Текст сообщения"
|
||||
# Секреты: из Vaultwarden (токен — пароль объекта HOME_BOT_TOKEN, chat_id — поле TELEGRAM_SELF_CHAT_ID объекта RESTIC).
|
||||
# Файл с мастер-паролем: /root/.bw-master (chmod 600). Если его нет — тихо выходим с 0, не ломаем вызывающий скрипт.
|
||||
|
||||
set -e
|
||||
|
||||
TITLE="${1:-Notification}"
|
||||
BODY="${2:-}"
|
||||
|
||||
# Креды из Vaultwarden или из старого конфига (fallback)
|
||||
TELEGRAM_BOT_TOKEN=""
|
||||
TELEGRAM_CHAT_ID=""
|
||||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
if [ -f "$BW_MASTER_PASSWORD_FILE" ] && command -v bw >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then
|
||||
BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw 2>/dev/null) || true
|
||||
if [ -n "$BW_SESSION" ]; then
|
||||
export BW_SESSION
|
||||
TELEGRAM_BOT_TOKEN=$(bw get password "HOME_BOT_TOKEN" 2>/dev/null) || true
|
||||
RESTIC_ITEM=$(bw get item "RESTIC" 2>/dev/null) || true
|
||||
if [ -n "$RESTIC_ITEM" ]; then
|
||||
TELEGRAM_CHAT_ID=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="TELEGRAM_SELF_CHAT_ID") | .value' 2>/dev/null) || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
|
||||
ENV_FILE="${TELEGRAM_NOTIFY_ENV:-/root/.telegram-notify.env}"
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
source "$ENV_FILE"
|
||||
fi
|
||||
fi
|
||||
if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -z "$BODY" ]; then
|
||||
TEXT="$TITLE"
|
||||
else
|
||||
TEXT="$TITLE
|
||||
|
||||
$BODY"
|
||||
fi
|
||||
|
||||
URL="https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage"
|
||||
if [ -n "${TELEGRAM_DEBUG:-}" ]; then
|
||||
curl -s -w "\nHTTP_CODE:%{http_code}\n" -X POST "$URL" \
|
||||
--data-urlencode "chat_id=$TELEGRAM_CHAT_ID" \
|
||||
--data-urlencode "text=$TEXT" \
|
||||
-d "disable_web_page_preview=true" \
|
||||
--max-time 10
|
||||
else
|
||||
curl -sf -X POST "$URL" \
|
||||
--data-urlencode "chat_id=$TELEGRAM_CHAT_ID" \
|
||||
--data-urlencode "text=$TEXT" \
|
||||
-d "disable_web_page_preview=true" \
|
||||
--max-time 10 \
|
||||
>/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
48
scripts/notify-vzdump-success.sh
Normal file
48
scripts/notify-vzdump-success.sh
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
# Проверяет каталог локальных vzdump за последние 2 часа и отправляет в Telegram сводку.
|
||||
# Задание Proxmox Backup выполняется в 02:00; этот скрипт запускают по cron в 03:00.
|
||||
# Использование: notify-vzdump-success.sh [путь_к_dump]
|
||||
# По умолчанию: /mnt/backup/proxmox/dump/dump/
|
||||
|
||||
DUMP_DIR="${1:-/mnt/backup/proxmox/dump/dump}"
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
# Файлы, изменённые за последние 120 минут (2 часа)
|
||||
MAX_AGE_MIN=120
|
||||
|
||||
if [ ! -d "$DUMP_DIR" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -x "$NOTIFY_SCRIPT" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Список файлов vzdump, изменённых за последние MAX_AGE_MIN минут
|
||||
RECENT=$(find "$DUMP_DIR" -maxdepth 1 -type f \( -name 'vzdump-*.tar.zst' -o -name 'vzdump-*.vma.zst' -o -name 'vzdump-*.vma' \) -mmin "-$MAX_AGE_MIN" 2>/dev/null)
|
||||
|
||||
if [ -z "$RECENT" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
COUNT=$(echo "$RECENT" | grep -c . 2>/dev/null || echo 0)
|
||||
[ "$COUNT" -eq 0 ] && exit 0
|
||||
|
||||
TOTAL_BYTES=$(echo "$RECENT" | while read -r f; do stat -c %s "$f" 2>/dev/null; done | awk '{s+=$1} END {print s+0}')
|
||||
[ -z "$TOTAL_BYTES" ] && TOTAL_BYTES=0
|
||||
|
||||
# Размер в ГБ (округление до 2 знаков; если bc нет — целое число)
|
||||
TOTAL_GB=$(echo "scale=2; $TOTAL_BYTES / 1024 / 1024 / 1024" | bc 2>/dev/null)
|
||||
[ -z "$TOTAL_GB" ] && TOTAL_GB="$((TOTAL_BYTES / 1024 / 1024 / 1024))"
|
||||
|
||||
# Время последнего изменения (последний записанный файл = время завершения бэкапа)
|
||||
LATEST_MTIME=$(echo "$RECENT" | while read -r f; do stat -c %Y "$f" 2>/dev/null; done | sort -n | tail -1)
|
||||
FINISH_TIME=""
|
||||
[ -n "$LATEST_MTIME" ] && FINISH_TIME=$(date -d "@$LATEST_MTIME" +%H:%M 2>/dev/null) || true
|
||||
|
||||
BODY="Резервное копирование завершено.
|
||||
Объекты: локальный vzdump (LXC/VM). Контейнеров/ВМ: $COUNT.
|
||||
Размер копии: ${TOTAL_GB} ГБ."
|
||||
[ -n "$FINISH_TIME" ] && BODY="${BODY}
|
||||
Время завершения: ${FINISH_TIME}."
|
||||
"$NOTIFY_SCRIPT" "💾 Backup local" "$BODY" || true
|
||||
exit 0
|
||||
@@ -2,6 +2,7 @@
|
||||
# Восстановление одного файла vzdump из restic (Yandex S3) через mount.
|
||||
# Не выкачивает весь репозиторий — подгружаются только нужные данные для выбранного файла.
|
||||
# Запускать на хосте Proxmox под root. Требуется FUSE (restic mount).
|
||||
# Секреты из Vaultwarden (объект RESTIC), файл /root/.bw-master (chmod 600).
|
||||
#
|
||||
# Использование:
|
||||
# restore-one-vzdump-from-restic.sh [SNAPSHOT] [ПУТЬ_В_СНИМКЕ] [КУДА_СОХРАНИТЬ]
|
||||
@@ -14,9 +15,7 @@
|
||||
# Список файлов в снимке: restic ls SNAPSHOT
|
||||
set -e
|
||||
|
||||
ENV_FILE="${ENV_FILE:-/root/.restic-yandex.env}"
|
||||
MOUNT_DIR="${MOUNT_DIR:-/mnt/backup/restic-mount}"
|
||||
RESTIC_PASSWORD_FILE="${RESTIC_PASSWORD_FILE:-/root/.restic-password}"
|
||||
|
||||
SNAPSHOT="${1:-latest}"
|
||||
# Путь к файлу внутри снимка (как в restic ls) — бэкапим /mnt/backup, пути вида /mnt/backup/proxmox/dump/dump/vzdump-lxc-107-...
|
||||
@@ -28,22 +27,40 @@ if [ "$(id -u)" -ne 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
echo "Нет файла $ENV_FILE. Скопируйте restic-yandex-env.example и задайте ключи."
|
||||
# Секреты из 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/backup/proxmox-phase1-backup.md"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
source "$ENV_FILE"
|
||||
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."; 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 "В $ENV_FILE не задано: $var"
|
||||
echo "В Vaultwarden (RESTIC) не задано поле для $var"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY RESTIC_REPOSITORY
|
||||
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
|
||||
export AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-ru-central1}"
|
||||
|
||||
if ! command -v restic >/dev/null 2>&1; then
|
||||
echo "restic не установлен. Установите: apt install restic."
|
||||
@@ -80,7 +97,7 @@ fi
|
||||
echo "Монтируем репозиторий в $MOUNT_DIR ..."
|
||||
restic mount "$MOUNT_DIR" &
|
||||
MOUNT_PID=$!
|
||||
trap 'kill $MOUNT_PID 2>/dev/null; fusermount -u "$MOUNT_DIR" 2>/dev/null; exit' EXIT INT TERM
|
||||
trap 'rm -f "$RESTIC_PASSWORD_FILE"; kill $MOUNT_PID 2>/dev/null; fusermount -u "$MOUNT_DIR" 2>/dev/null; exit' EXIT INT TERM
|
||||
|
||||
# В смонтированном репо файлы снимка лежат в ids/<short_id>/<путь>
|
||||
SOURCE_FILE="$MOUNT_DIR/ids/$SNAPSHOT_ID/$FILE_IN_SNAPSHOT"
|
||||
|
||||
13
scripts/telegram-notify.env.example
Normal file
13
scripts/telegram-notify.env.example
Normal file
@@ -0,0 +1,13 @@
|
||||
# Пример конфига для уведомлений в Telegram (скрипт notify-telegram.sh).
|
||||
# Скопируйте на хост Proxmox в /root/.telegram-notify.env и подставьте свои значения.
|
||||
#
|
||||
# Как получить:
|
||||
# 1. Создать бота: в Telegram написать @BotFather, команда /newbot, получить токен.
|
||||
# 2. Узнать chat_id: написать боту любое сообщение, затем открыть в браузере:
|
||||
# https://api.telegram.org/bot<TOKEN>/getUpdates
|
||||
# В ответе в updates[].message.chat.id — ваш chat_id (число или отрицательное для групп).
|
||||
#
|
||||
# На хосте: cp telegram-notify.env.example /root/.telegram-notify.env && chmod 600 /root/.telegram-notify.env
|
||||
|
||||
TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz
|
||||
TELEGRAM_CHAT_ID=123456789
|
||||
Reference in New Issue
Block a user