From d780c91bd512da4db7914140108e7a03858630dc Mon Sep 17 00:00:00 2001 From: gilfanovr Date: Sun, 11 Oct 2026 15:21:32 +0300 Subject: [PATCH] =?UTF-8?q?rpi:=20=D0=B5=D0=B6=D0=B5=D0=BC=D0=B5=D1=81?= =?UTF-8?q?=D1=8F=D1=87=D0=BD=D0=B0=D1=8F=20=D0=B2=D1=8B=D0=B1=D0=BE=D1=80?= =?UTF-8?q?=D0=BE=D1=87=D0=BD=D0=B0=D1=8F=20=D0=BF=D0=B5=D1=80=D0=B5=D1=87?= =?UTF-8?q?=D0=B8=D1=82=D0=BA=D0=B0=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2?= =?UTF-8?q?=20OneDrive=20=D1=81=20=D0=B4=D0=B8=D1=81=D0=BA=D0=B0=20=D1=80?= =?UTF-8?q?=D0=BE=D1=83=D1=82=D0=B5=D1=80=D0=B0=20(sha256)=20+=20=D0=B0?= =?UTF-8?q?=D0=BB=D0=B5=D1=80=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- servers/rpi.md | 1 + stacks/monitoring/prometheus/rules/alerts.yml | 7 ++++++ stacks/rpi-backup/bin/router-backup | 25 ++++++++++++++++++- .../rpi-backup/systemd/router-sample.service | 14 +++++++++++ stacks/rpi-backup/systemd/router-sample.timer | 9 +++++++ 5 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 stacks/rpi-backup/systemd/router-sample.service create mode 100644 stacks/rpi-backup/systemd/router-sample.timer diff --git a/servers/rpi.md b/servers/rpi.md index a990408..054dc04 100644 --- a/servers/rpi.md +++ b/servers/rpi.md @@ -43,6 +43,7 @@ OneDrive на внешний HDD. Мониторится с fr1 через pushg - `router-backup` (`stacks/rpi-backup/`, таймер 06:00, `flock`): в `router:rpi-backup/` — `onedrive/{archive,manifests,logs}` только `copy` (там ничего не удаляется), затем зеркало `onedrive/current` с `--max-delete 1000`, затем зеркало зашифрованных `servers/` с `--max-delete 50`. Порядок важен: удалённое из current к этому моменту уже скопировано в archive. Логи — `/mnt/backup/router-logs/`. - ⚠️ `--max-delete` — предохранитель: если локально «пропадёт» много файлов (сбой диска/монтирования), копия на роутере не зачистится, а прогон упадёт с ошибкой → алерт `BackupFailed`. - По воскресеньям `verify`: сверка размеров деревьев + перечитать с роутера последний daily каждого сервера и сравнить sha256. +- **Ежемесячно (1-го, 08:00) `router-sample`**: случайные файлы OneDrive из `current/` и `archive/` старше суток, ~2 ГБ / до 300 шт., перечитываются с роутера и сверяются sha256 с HDD — ловит тихую порчу, которую сверка размеров не видит. Отсутствующий на роутере файл = ошибка. Итог — `backup_*{job="backup_router_sample"}` → `BackupFailed`, `BackupStale` (>35 дней). Проверено 2026-10-11: 20 из 20 скопированных файлов совпали. - Мониторинг: `router_mount_up` и заполнение `/mnt/router` (в `smart-metrics`, `df` под `timeout 20`), итог прогона — `backup_*{job="backup_router"}`; алерты `RouterDiskMountDown` (30 мин), `BackupDiskFull` (>90 %, любой диск бэкапа), `BackupStale` (>30 ч); ряд «Вторая копия» на дашборде rpi-disk. - ⚠️ 1 ТБ меньше основного диска: сейчас влезает (~370 ГБ OneDrive + ~100 ГБ серверов), но архив версий растёт вечно — следить за `BackupDiskFull`. diff --git a/stacks/monitoring/prometheus/rules/alerts.yml b/stacks/monitoring/prometheus/rules/alerts.yml index 724b6fd..3861a6c 100644 --- a/stacks/monitoring/prometheus/rules/alerts.yml +++ b/stacks/monitoring/prometheus/rules/alerts.yml @@ -97,6 +97,7 @@ groups: or (time() - backup_last_success_timestamp{job="backup_onedrive_verify"} > 8 * 86400) or (time() - backup_last_success_timestamp{job="backup_servers"} > 26 * 3600) or (time() - backup_last_success_timestamp{job="backup_router"} > 30 * 3600) + or (time() - backup_last_success_timestamp{job="backup_router_sample"} > 35 * 86400) for: 10m labels: {severity: critical} annotations: @@ -150,3 +151,9 @@ groups: labels: {severity: warning, instance: rpi, job: smart} annotations: summary: "rpi: метрики SMART в pushgateway отсутствуют 1+ ч — малина/пуш лежат или pushgateway потерял данные" + - alert: BackupNeverReported + expr: absent(backup_last_success_timestamp{instance="rpi",job="backup_router_sample"}) + for: 35d + labels: {severity: warning, instance: rpi, job: backup_router_sample} + annotations: + summary: "rpi: ежемесячная перечитка файлов с диска роутера не отчитывалась 35+ дней — проверь router-sample" diff --git a/stacks/rpi-backup/bin/router-backup b/stacks/rpi-backup/bin/router-backup index c804adf..ae6727c 100644 --- a/stacks/rpi-backup/bin/router-backup +++ b/stacks/rpi-backup/bin/router-backup @@ -4,6 +4,7 @@ # # router-backup run — ежедневно (таймер 06:00) # router-backup verify — по воскресеньям после run: размеры + перечитать свежие архивы серверов по sha256 +# router-backup sample — ежемесячно: случайные файлы OneDrive (~2 ГБ) перечитать с роутера и сверить sha256 # # Пишем прямо в remote, а не через FUSE /mnt/router: так надёжнее и есть коды ошибок. # Роутер не умеет хэши (shell_type=none) → сравнение по размеру+mtime, целостность — перечиткой. @@ -51,8 +52,30 @@ verify() { return $r } +sample() { + # Роутер хэшей не считает, verify сверяет OneDrive только по размеру — тихую порчу внутри + # файла того же размера ловит только перечитка. Берём файлы старше суток (точно должны + # быть скопированы), до SAMPLE_BYTES суммарно и не больше SAMPLE_MAX штук. + local budget=${SAMPLE_BYTES:-2147483648} max=${SAMPLE_MAX:-300} total=0 n=0 bad=0 miss=0 size rel want got + while IFS=' ' read -r size rel; do + [ $n -ge $max ] && break + [ $total -gt 0 ] && [ $((total + size)) -gt $budget ] && continue + want=$(sha256sum "$SRC/onedrive/$rel" | cut -d' ' -f1) + if ! got=$(rclone --config "$C" cat "$R/onedrive/$rel" 2>>"$LOG" | sha256sum | cut -d' ' -f1) \ + || [ "$got" = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" -a "$size" != 0 ]; then + echo "MISSING $rel"; miss=$((miss + 1)) + elif [ "$want" != "$got" ]; then + echo "BAD $rel"; bad=$((bad + 1)) + fi + n=$((n + 1)); total=$((total + size)) + done < <(cd "$SRC/onedrive" && find current archive -type f ! -name '*.partial' -mmin +1440 -printf '%s %p\n' | shuf) + echo "sample: проверено $n файлов, $((total / 1048576)) МиБ; повреждено $bad, нет на роутере $miss" + [ $bad = 0 ] && { [ $miss = 0 ] || [ -n "${SAMPLE_ALLOW_MISSING:-}" ]; } +} + case "${1:-run}" in run) run; r=$?; [ "$(date +%u)" = 7 ] && { verify || r=1; }; exit $r ;; verify) verify ;; - *) echo "usage: router-backup run|verify" >&2; exit 2 ;; + sample) sample ;; + *) echo "usage: router-backup run|verify|sample" >&2; exit 2 ;; esac diff --git a/stacks/rpi-backup/systemd/router-sample.service b/stacks/rpi-backup/systemd/router-sample.service new file mode 100644 index 0000000..d15d951 --- /dev/null +++ b/stacks/rpi-backup/systemd/router-sample.service @@ -0,0 +1,14 @@ +[Unit] +Description=Monthly random read-back of OneDrive files from router disk (sha256) +Wants=network-online.target +After=network-online.target +RequiresMountsFor=/mnt/backup + +[Service] +Type=oneshot +# ждёт, если идёт router-backup (общий lock), а не падает +ExecStart=/usr/bin/flock /run/lock/router-backup.lock /usr/local/bin/router-backup sample +ExecStopPost=/usr/local/bin/backup-metrics backup_router_sample +Nice=10 +IOSchedulingClass=idle +TimeoutStartSec=6h diff --git a/stacks/rpi-backup/systemd/router-sample.timer b/stacks/rpi-backup/systemd/router-sample.timer new file mode 100644 index 0000000..327102e --- /dev/null +++ b/stacks/rpi-backup/systemd/router-sample.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Monthly router disk read-back sample + +[Timer] +OnCalendar=*-*-01 08:00 +Persistent=true + +[Install] +WantedBy=timers.target