rpi: вторая копия /mnt/backup на 1 ТБ диск в роутере (rclone SFTP) + мониторинг монтирования и заполнения

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
gilfanovr
2026-10-11 15:06:53 +03:00
co-authored by Claude Opus 5.5
parent 3811483577
commit 464e12abdb
7 changed files with 314 additions and 1 deletions
@@ -1096,6 +1096,199 @@
"overrides": []
},
"options": {}
},
{
"type": "row",
"title": "Вторая копия — диск в роутере",
"collapsed": false,
"gridPos": {
"x": 0,
"y": 31,
"w": 24,
"h": 1
},
"id": 200,
"panels": []
},
{
"id": 201,
"type": "stat",
"title": "Диск в роутере (SFTP)",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": {
"x": 0,
"y": 32,
"w": 6,
"h": 4
},
"targets": [
{
"refId": "A",
"expr": "router_mount_up{instance=\"$instance\"}",
"instant": true,
"range": false
}
],
"fieldConfig": {
"defaults": {
"mappings": [
{
"type": "value",
"options": {
"1": {
"text": "доступен",
"color": "green"
},
"0": {
"text": "НЕДОСТУПЕН",
"color": "red"
}
}
}
]
},
"overrides": []
},
"description": "rclone-монтирование /mnt/router (Huawei SFTP :8022). Диск питается от роутера, не от малины.",
"options": {
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"colorMode": "background",
"graphMode": "none",
"textMode": "value",
"justifyMode": "center"
}
},
{
"id": 202,
"type": "stat",
"title": "С последней копии на роутер",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": {
"x": 6,
"y": 32,
"w": 9,
"h": 4
},
"targets": [
{
"refId": "A",
"expr": "time() - backup_last_success_timestamp{instance=\"$instance\",job=\"backup_router\"}",
"instant": true,
"range": false
}
],
"fieldConfig": {
"defaults": {
"unit": "s",
"noValue": "ещё не было",
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": null,
"color": "green"
},
{
"value": 93600,
"color": "orange"
},
{
"value": 108000,
"color": "red"
}
]
}
},
"overrides": []
},
"description": "Ежедневно 06:00: OneDrive (архив — copy, current — зеркало с --max-delete) и зашифрованные бэкапы серверов.",
"options": {
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"colorMode": "background",
"graphMode": "none",
"textMode": "value",
"justifyMode": "center"
}
},
{
"id": 203,
"type": "stat",
"title": "Заполнение диска в роутере",
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": {
"x": 15,
"y": 32,
"w": 9,
"h": 4
},
"targets": [
{
"refId": "A",
"expr": "1 - backup_disk_avail_bytes{instance=\"$instance\",mountpoint=\"/mnt/router\"} / backup_disk_size_bytes{instance=\"$instance\",mountpoint=\"/mnt/router\"}",
"instant": true,
"range": false
}
],
"fieldConfig": {
"defaults": {
"unit": "percentunit",
"min": 0,
"max": 1,
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": null,
"color": "green"
},
{
"value": 0.8,
"color": "orange"
},
{
"value": 0.9,
"color": "red"
}
]
}
},
"overrides": []
},
"description": "",
"options": {
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"colorMode": "background",
"graphMode": "none",
"textMode": "value",
"justifyMode": "center"
}
}
]
}
+17 -1
View File
@@ -90,13 +90,29 @@ groups:
annotations:
summary: "{{ $labels.instance }}: {{ $labels.job }} завершился с кодом {{ $value }} — см. journalctl на хосте"
# sync каждые 4ч → 12ч = три пропуска подряд; verify раз в неделю → 8 дней; бэкап серверов ежедневно → 26ч
# sync каждые 4ч → 12ч = три пропуска подряд; verify раз в неделю → 8 дней; бэкап серверов ежедневно → 26ч; копия на роутер ежедневно → 30ч (первичная ~13ч)
- alert: BackupStale
expr: |
(time() - backup_last_success_timestamp{job="backup_onedrive"} > 12 * 3600)
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)
for: 10m
labels: {severity: critical}
annotations:
summary: "{{ $labels.instance }}: {{ $labels.job }} не было успешного прогона {{ $value | humanizeDuration }}"
# /mnt/backup (HDD 4 ТБ) и /mnt/router (1 ТБ диск в роутере) — заполнение
- alert: BackupDiskFull
expr: 1 - backup_disk_avail_bytes / backup_disk_size_bytes > 0.9
for: 15m
labels: {severity: warning}
annotations:
summary: "{{ $labels.instance }}: диск бэкапа {{ $labels.mountpoint }} заполнен на {{ $value | humanizePercentage }}"
- alert: RouterDiskMountDown
expr: router_mount_up == 0
for: 30m
labels: {severity: warning}
annotations:
summary: "{{ $labels.instance }}: диск в роутере (/mnt/router, SFTP) недоступен 30+ мин — вторая копия бэкапа не пишется"
+58
View File
@@ -0,0 +1,58 @@
#!/bin/bash
# Вторая копия /mnt/backup на 1 ТБ диск в роутере Huawei (SFTP :8022, remote `router:` в
# ~rus/.config/rclone/rclone.conf). Диск питается от роутера — не нагружает USB малины.
#
# router-backup run — ежедневно (таймер 06:00)
# router-backup verify — по воскресеньям после run: размеры + перечитать свежие архивы серверов по sha256
#
# Пишем прямо в remote, а не через FUSE /mnt/router: так надёжнее и есть коды ошибок.
# Роутер не умеет хэши (shell_type=none) → сравнение по размеру+mtime, целостность — перечиткой.
set -uo pipefail
C=/home/rus/.config/rclone/rclone.conf
R=router:rpi-backup
SRC=/mnt/backup
TS=$(date +%F_%H%M)
LOGDIR=$SRC/router-logs; mkdir -p "$LOGDIR"
LOG="$LOGDIR/router-$TS.log"
rc() { rclone --config "$C" --log-file "$LOG" --log-level INFO --stats 10m --stats-log-level NOTICE \
--transfers 2 --checkers 4 --sftp-concurrency 8 --retries 5 --low-level-retries 20 "$@"; }
mountpoint -q "$SRC" || { echo "$SRC не смонтирован — пропуск"; exit 1; }
rclone --config "$C" lsd router: >/dev/null 2>&1 || { echo "роутер недоступен по SFTP"; exit 1; }
run() {
local r=0
# 1) то, что никогда не удаляется: архив версий, манифесты, логи — только copy
for d in archive manifests logs; do
[ -d "$SRC/onedrive/$d" ] && { rc copy "$SRC/onedrive/$d" "$R/onedrive/$d" || r=1; }
done
# 2) зеркало current. Удалённое из current локально уже лежит в archive (скопированном выше).
# --max-delete: если локально «пропало» много файлов (сбой диска/монтирования) — стоп, а не зачистка копии
rc sync "$SRC/onedrive/current" "$R/onedrive/current" --exclude '*.partial' --max-delete 1000 || r=1
# 3) зашифрованные бэкапы серверов — зеркало (ротация 14/8/12 → удаляется пара файлов в день)
rc sync "$SRC/servers" "$R/servers" --max-delete 50 || r=1
echo "run: $([ $r = 0 ] && echo OK || echo 'ЕСТЬ ОШИБКИ') — лог $LOG"
return $r
}
verify() {
local r=0 s rel want got
# размеры всего дерева (только листинг, без чтения данных)
rc check "$SRC/onedrive/current" "$R/onedrive/current" --size-only --one-way --exclude '*.partial' || r=1
rc check "$SRC/servers" "$R/servers" --size-only --one-way || r=1
# перечитать с роутера последний daily каждого сервера и сверить с sha256
for s in $(ls -1t "$SRC"/servers/*/daily/*.age.sha256 2>/dev/null | awk -F/ '!seen[$(NF-2)]++'); do
rel=${s#"$SRC/servers/"}; rel=${rel%.sha256}
want=$(cut -d' ' -f1 "$s")
got=$(rclone --config "$C" cat "$R/servers/$rel" 2>>"$LOG" | sha256sum | cut -d' ' -f1)
if [ "$want" = "$got" ]; then echo "read-back OK $rel"; else echo "read-back BAD $rel"; r=1; fi
done
echo "verify: $([ $r = 0 ] && echo OK || echo 'ЕСТЬ РАСХОЖДЕНИЯ')"
return $r
}
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 ;;
esac
+11
View File
@@ -39,4 +39,15 @@ if mountpoint -q /mnt/backup; then
echo "backup_disk_size_bytes{mountpoint=\"/mnt/backup\"} $size" >> "$out"
echo "backup_disk_avail_bytes{mountpoint=\"/mnt/backup\"} $avail" >> "$out"
fi
# диск в роутере (rclone SFTP-монтирование /mnt/router): жив ли и сколько занято.
# timeout — зависший FUSE не должен вешать сбор метрик
echo "# TYPE router_mount_up gauge" >> "$out"
if mountpoint -q /mnt/router && r=$(timeout 20 df -B1 --output=size,avail /mnt/router 2>/dev/null | tail -1) && [ -n "$r" ]; then
read -r size avail <<< "$r"
echo "router_mount_up 1" >> "$out"
echo "backup_disk_size_bytes{mountpoint=\"/mnt/router\"} $size" >> "$out"
echo "backup_disk_avail_bytes{mountpoint=\"/mnt/router\"} $avail" >> "$out"
else
echo "router_mount_up 0" >> "$out"
fi
metrics-push smart < "$out"
@@ -0,0 +1,14 @@
[Unit]
Description=Second copy of /mnt/backup to router USB disk (rclone SFTP)
Wants=network-online.target
After=network-online.target
RequiresMountsFor=/mnt/backup
[Service]
Type=oneshot
ExecStart=/usr/bin/flock -n /run/lock/router-backup.lock /usr/local/bin/router-backup run
ExecStopPost=/usr/local/bin/backup-metrics backup_router
Nice=10
IOSchedulingClass=idle
# первичная копия ~370 ГБ при ~8 МБ/с — около 13 ч
TimeoutStartSec=infinity
@@ -0,0 +1,10 @@
[Unit]
Description=Daily copy of /mnt/backup to router disk
[Timer]
OnCalendar=*-*-* 06:00
RandomizedDelaySec=10m
Persistent=true
[Install]
WantedBy=timers.target