rpi: вторая копия /mnt/backup на 1 ТБ диск в роутере (rclone SFTP) + мониторинг монтирования и заполнения
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3811483577
commit
464e12abdb
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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+ мин — вторая копия бэкапа не пишется"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user