Compare commits
1
Commits
ru1-tproxy
...
d1269162e7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1269162e7 |
@@ -6,6 +6,7 @@
|
||||
- [`servers/de1.md`](servers/de1.md) — 213.136.74.247. **Выведен** (все сервисы остановлены, метрики удалены): был прокси/VPN-нодой (telemt, AmneziaWG, OpenVPN) плюс gitea, openclaw, боты — всё уехало на fr1 или в бэкапы. ⚠️ Сам файл описания под вывод не переписан.
|
||||
- [`servers/fr1.md`](servers/fr1.md) — `fr1.ruzzy.dev` (161.97.93.252). Основная нода (Ubuntu 24.04): мониторинг, gitea, AmneziaWG exit для ru1, боты (claude-channels, openclaw), статика `www`/`share`. Держит апекс `ruzzy.dev`.
|
||||
- [`servers/ru1.md`](servers/ru1.md) — `85.198.109.86` (VDSina, РФ). VPN entry-нода (OpenVPN ×2, AmneziaWG **до fr1** после переключения, WireGuard клиентов), умный роутинг RU/зарубеж; WEB-прокси Telegram (tproxy-server + MTProxy) на `ru.ruzzy.dev:443`, telemt удалён 2026-10-09. ⚠️ Файл описания сверялся до переключения.
|
||||
- [`servers/rpi.md`](servers/rpi.md) — `rpi.local` (192.168.10.25, дом, за NAT). Raspberry Pi 4: бэкап личного OneDrive на HDD 4 ТБ (rclone, вечный архив версий, еженедельная сверка хэшей); SMART и статус бэкапов пушит в pushgateway fr1 по SSH. Код — `stacks/rpi-backup/`.
|
||||
|
||||
## Поддержание описаний в актуальном состоянии
|
||||
Тронул сервер — обнови `servers/<хост>.md` в том же прогоне. Контракт и
|
||||
|
||||
+6
-2
@@ -15,6 +15,7 @@
|
||||
- ⚠️ **Группа `docker` = root-эквивалент.** `docker:x:988:rus,tgclaude` — любой её член монтирует `/` в контейнер и делает что угодно от рута (этим же способом чистились root-овы файлы 2026-09-08). Поэтому «у `tgclaude` нет sudo» **не** означает ограничения прав на хосте.
|
||||
- Вход с Mac: `ssh rus-161` (алиас в `~/.ssh/config`) → ключ `~/.ssh/id_161_97_93_252` (ed25519).
|
||||
- root-доступ по SSH закрыт; для root изнутри — `sudo -i`.
|
||||
- **Пуш-ключ `metrics-push@rpi`** (с 2026-10-11) в `~rus/.ssh/authorized_keys` с `restrict,command="/home/rus/bin/pushgw-ingest rpi"`: этим ключом домашняя малина может только залить метрики в pushgateway — шелла, форвардинга и pty нет. Бэкап прежнего файла — `~/.ssh/authorized_keys.bak-20261011`.
|
||||
|
||||
## SSH (`/etc/ssh/sshd_config.d/00-hardening.conf`)
|
||||
⚠️ В основном `/etc/ssh/sshd_config` строкой 42 стоит `PermitRootLogin yes` (правка шаблона Contabo). Побеждает всё равно дроп-ин: `Include /etc/ssh/sshd_config.d/*.conf` стоит строкой 12, а sshd берёт **первое** встреченное значение. То есть root-вход закрыт, но держится на порядке строк — уберёшь дроп-ин, и root-вход по ключу включится. ⏳ Рядом лежит `50-cloud-init.conf`, который из-под `rus` не читается: под root стоит проверить `sshd -T | grep -i permitrootlogin`.
|
||||
@@ -104,8 +105,8 @@ backend=systemd banaction=ufw
|
||||
- Сертификаты LE: `mon`, `git`, `share`, `www`. `cam.ruzzy.dev` удалён 2026-09-08 вместе с ipcam.
|
||||
|
||||
## Мониторинг (`~/monitoring`, stack в репо `stacks/monitoring/`)
|
||||
- Стек: **Prometheus + Grafana + node-exporter + cAdvisor + Alertmanager** (docker compose).
|
||||
- **Алерты:** Alertmanager → Telegram (бот @ultimate_log_bot, chat 607015). Правила `prometheus/rules/alerts.yml`: TargetDown, HostHighCPU, HostLowMemory, HostDiskLow/Critical. Токен — в `~/monitoring/alertmanager/telegram_token` (chown 65534, не в git).
|
||||
- Стек: **Prometheus + Grafana + node-exporter + cAdvisor + Alertmanager + Pushgateway** (docker compose).
|
||||
- **Алерты:** Alertmanager → Telegram (бот @ultimate_log_bot, chat 607015). Правила `prometheus/rules/alerts.yml`: TargetDown, HostHighCPU, HostLowMemory, HostDiskLow/Critical; группа `rpi` (с 2026-10-11) — SMART и бэкапы малины: PushMetricsStale, SmartHealthFailed, SmartBadSectors(+Growing), SmartUdmaCrcErrors, SmartSelftestFailed, SmartTemperatureHigh, BackupFailed, BackupStale. Токен — в `~/monitoring/alertmanager/telegram_token` (chown 65534, не в git).
|
||||
- Prometheus: retention 90d, слушает `127.0.0.1:9090`. Job'ы: `node` и `cadvisor` (file_sd, instance'ы **fr1 и ru1**), `openvpn` (file_sd, только ru1 — `85.198.109.86:9176`), `gitea` (static, `gitea:3000` через внешнюю docker-сеть `monitoring_monitoring`), `prometheus` (self). На 2026-09-08 все таргеты up, активных алертов нет.
|
||||
- Grafana: `127.0.0.1:3000`, доступ снаружи через **https://mon.ruzzy.dev** (nginx + Let's Encrypt). Datasource Prometheus (uid `prometheus`) и дашборды (Node Exporter Full, cAdvisor, Network/Traffic, Gitea, OpenVPN — папка Infra) провижатся автоматически. Пароль admin — в `~/monitoring/docker-compose.yml` на сервере (в репо вынесен в `${GF_ADMIN_PASSWORD}`).
|
||||
- **Тома:** `monitoring_prometheus_data` — **6.5 ГБ** (при retention 90d; самый крупный том на сервере, учитывать при планировании диска), `monitoring_grafana_data` — 50 МБ, `monitoring_alertmanager_data` — пустой.
|
||||
@@ -113,6 +114,9 @@ backend=systemd banaction=ufw
|
||||
- **node-exporter — `network_mode: host`** (видит реальные интерфейсы хоста: eth0, wg0-туннель и т.д.; иначе показывал бы только сеть docker-бриджа). Слушает host:9100, доступ ограничен ufw (`from 172.16.0.0/12 to port 9100`), Prometheus скрейпит через `host.docker.internal:9100`. nodename берётся с хоста (fr1).
|
||||
- Дашборд **Network / Traffic** (`network.json`): RX/TX по интерфейсам, пакеты, ошибки/дропы, статус — с фильтром по instance/device (lo/veth/docker скрыты).
|
||||
- DNS: `mon.ruzzy.dev` → 161.97.93.252.
|
||||
- **Pushgateway** (с 2026-10-11) — для хостов за NAT, которых не скрейпнуть (домашняя `rpi`, см. `servers/rpi.md`). Слушает только `127.0.0.1:9091`; job `pushgateway` с `honor_labels: true` (иначе job/instance заменились бы на `pushgateway:9091`). Персистентность в томе `monitoring_pushgateway_data` — рестарт не обнуляет метрики. **Как приходят данные:** хост делает `ssh rus@fr1 <job> < metrics.prom`, forced command `~/bin/pushgw-ingest <instance>` делает `PUT /metrics/job/<job>/instance/<instance>`. instance зашит в authorized_keys, а не берётся от клиента — хост не может писать от чужого имени; имя job фильтруется по `[a-z0-9_]`. Наружу порт не открыт, nginx/ufw не трогали.
|
||||
- ⚠️ Pushgateway хранит последнее значение вечно: если хост умер, метрики не исчезнут, а «застынут». Поэтому свежесть ловит отдельный алерт `PushMetricsStale` по `push_time_seconds`.
|
||||
- ⚠️ Живой `~/monitoring/docker-compose.yml` расходится с репо (пароль Grafana вписан литералом), поэтому при деплое 2026-10-11 compose **не копировался**, а блок pushgateway вставлен на месте. Бэкапы: `docker-compose.yml.bak-20261011`, `prometheus/prometheus.yml.bak-20261011`.
|
||||
- Подключён `ru1` (node + cadvisor + openvpn-exporter, напрямую по публичному IP). `de1` выведен — его таргеты удалены. Планируется: экспортёры wireguard/awg и telemt.
|
||||
|
||||
## Gitea (`~/gitea`, stack в репо `stacks/gitea/`)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# rpi — домашняя Raspberry Pi 4 (`rpi.local`, 192.168.10.25)
|
||||
|
||||
Поднята 2026-10-11. Домашний сервер за NAT: первая задача — бэкап личного
|
||||
OneDrive на внешний HDD. Мониторится с fr1 через pushgateway (скрейпить
|
||||
её снаружи нельзя — она за домашним роутером).
|
||||
|
||||
## Железо и ОС
|
||||
- Raspberry Pi 4, SD 64 ГБ (`mmcblk0`), питание штатное — `vcgencmd get_throttled` = `0x0`.
|
||||
- Raspberry Pi OS Lite 64-bit **Trixie** (Debian 13), образ `2026-10-06-raspios-trixie-arm64-lite`.
|
||||
- Headless-настройка — **cloud-init**, а не старые `ssh`/`userconf.txt`: в Trixie Pi Imager пишет `user-data`/`network-config` на `bootfs`. Наш `user-data` создал пользователя, ключ, hostname, таймзону MSK.
|
||||
- Сеть: Ethernet, DHCP. MAC `dc:a6:32:db:b0:f9`, роутер `192.168.10.254`. ⏳ Резерв адреса на роутере — на пользователе (см. хвосты).
|
||||
|
||||
## Доступ
|
||||
- С Mac: `ssh rpi` (алиас → `rpi.local`, user `rus`, ключ `~/.ssh/id_ed25519`). Резолвится через mDNS (avahi в образе по умолчанию).
|
||||
- Только по ключу: `passwordauthentication no`, у `rus` пароль заблокирован, **sudo NOPASSWD** (иначе sudo был бы недоступен вовсе).
|
||||
|
||||
## Диски
|
||||
| Устройство | Модель | Роль |
|
||||
|---|---|---|
|
||||
| `sda` | WD My Passport 4 ТБ (WD40NDZW, s/n WD-WX82D6228YCD) | ext4 `backup` → `/mnt/backup` |
|
||||
| — | WD Elements 1 ТБ (WD10SDRW) | **не подключён**, ждёт хаба (APFS, не отформатирован) |
|
||||
|
||||
- fstab по UUID с `nofail,x-systemd.device-timeout=30s` (`stacks/rpi-backup/fstab.line`): без диска малина грузится, бэкап просто пропускается (`mountpoint -q`).
|
||||
- ⚠️ **Два 2.5" HDD от USB Pi 4 не тянут.** Проверено 2026-10-11: при раскрутке обоих — `over-current` на всех USB3-портах; под нагрузкой 4-ТБ диск отваливался (`USB disconnect`) и залипал в `Unit Not Ready`. Один диск стабилен (20 ГБ чтения 103 МБ/с, mkfs ~60 ГБ записи без сбросов). Второй диск — только через USB3-хаб с собственным питанием.
|
||||
- Залипший после отвала диск лечится power-cycle всех USB (Pi 4 переключает питание портов только разом): `sudo uhubctl -l 2 -a off; sudo uhubctl -l 1-1 -a off; sleep 8; …-a on`.
|
||||
- SMART на момент ввода: 9 897 ч, 0 переназначенных/ожидающих, Start_Stop 21 946, Load_Cycle 42 530.
|
||||
|
||||
## Бэкап OneDrive (код — `stacks/rpi-backup/`)
|
||||
- rclone (официальный .deb, v1.75.2 — в Debian 1.60, старый), remote `onedrive:` = личный OneDrive (drive_type personal). Токен — в `~rus/.config/rclone/rclone.conf` (600), только на малине. Получен через `rclone authorize onedrive` на Mac.
|
||||
- `onedrive-backup` (таймер каждые 4 ч, `00/4:30`): `rclone sync` в `/mnt/backup/onedrive/current`, всё удалённое/перезаписанное в облаке — в `archive/<ts>/` **навсегда** (пользователь хочет иметь всё, что когда-либо было в OneDrive). Хэши архива — `manifests/archive-<ts>.qxh`, лог каждого прогона — `logs/sync-<ts>.log` (INFO: поштучно что скачано/убрано в архив).
|
||||
- Целостность: rclone сверяет QuickXorHash каждого скачанного файла с OneDrive; `onedrive-verify` (вс 05:00) перечитывает весь HDD: `rclone check` current↔облако + `rclone checksum` archive↔манифесты. Отчёт — `logs/verify-<ts>*`.
|
||||
- Оба юнита под общим `flock /run/lock/onedrive-backup.lock` — не идут параллельно и не убивают друг друга.
|
||||
- ⚠️ Файл, загруженный и удалённый в облаке между прогонами (<4 ч), в бэкап не попадёт.
|
||||
- ⚠️ `Personal Vault` исключён — rclone через API туда не ходит.
|
||||
- ⚠️ Архив без ротации: растёт вечно. Скрипт выходит с кодом 2 при заполнении >90 % (→ алерт BackupFailed), плюс HostDisk* если когда-то поставим node-exporter.
|
||||
- Первая полная выгрузка (368 ГБ) запущена 2026-10-11 ~13:13, ~27 МБ/с (упор в домашний канал).
|
||||
|
||||
## Мониторинг → fr1
|
||||
- `smartd` (`stacks/rpi-backup/smartd.conf`): `DEVICESCAN -d removable`, `-n standby,q` (не будить диск), короткий самотест сб 03:00, длинный 1-го числа 05:00. ⚠️ `-d sat` с DEVICESCAN не работает («Not a SAT device») — тип моста пусть определяет сам.
|
||||
- `smart-metrics` (таймер 15 мин): smartctl `-n standby` → метрики `smart_*` → job `smart`. Спящий диск не будится: отдаются значения из кэша `/var/lib/smart-metrics/` + `smart_disk_standby=1`.
|
||||
- `backup-metrics <job>` — `ExecStopPost=+` у юнитов бэкапа: `backup_last_exit_code`, `backup_last_run_timestamp`, `backup_last_success_timestamp` (время успеха хранится локально в `/var/lib/backup-metrics/`, т.к. PUT заменяет группу целиком).
|
||||
- Доставка: `metrics-push <job>` = ssh с ключом `/etc/metrics-push/id_ed25519` (root, 600) на `rus@161.97.93.252`; host key fr1 закреплён в `/etc/metrics-push/known_hosts`. На fr1 ключ ограничен forced command (`servers/fr1.md` → Мониторинг).
|
||||
- Алерты — группа `rpi` в `stacks/monitoring/prometheus/rules/alerts.yml`.
|
||||
|
||||
## Открытые хвосты (на 2026-10-11)
|
||||
- ⏳ Закрепить `192.168.10.25` за MAC `dc:a6:32:db:b0:f9` на роутере.
|
||||
- ⏳ USB3-хаб с питанием → подключить и отформатировать WD Elements 1 ТБ (вторая копия архива — сейчас копия одна).
|
||||
- Нет node-exporter (CPU/RAM/диск самой малины в Prometheus не видны) — пуш через pushgateway для node-метрик неудобен; решить, нужен ли.
|
||||
- Первый прогон бэкапа стартовал до появления `ExecStopPost` — его итог может не дойти в метрики; `backup_last_success_timestamp` появится с первого прогона по таймеру. До этого `BackupStale` молчит (серии нет).
|
||||
- Через неделю снять SMART и посчитать реальный прирост Start_Stop/Load_Cycle — прогноз ресурса пока оценочный.
|
||||
@@ -33,6 +33,22 @@ services:
|
||||
- '127.0.0.1:9093:9093'
|
||||
networks: [monitoring]
|
||||
|
||||
# pushgateway: метрики от хостов за NAT (rpi), которые не скрейпнуть.
|
||||
# Порт только на 127.0.0.1 — пуш идёт по SSH через forced command ~/bin/pushgw-ingest
|
||||
# (см. servers/fr1.md), наружу не торчит. Персистентность — чтобы рестарт не обнулял алерты.
|
||||
pushgateway:
|
||||
image: prom/pushgateway:latest
|
||||
container_name: pushgateway
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- '--persistence.file=/data/metrics'
|
||||
- '--persistence.interval=1m'
|
||||
volumes:
|
||||
- pushgateway_data:/data
|
||||
ports:
|
||||
- '127.0.0.1:9091:9091'
|
||||
networks: [monitoring]
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
container_name: grafana
|
||||
@@ -93,6 +109,7 @@ services:
|
||||
networks: [monitoring]
|
||||
|
||||
volumes:
|
||||
pushgateway_data:
|
||||
prometheus_data:
|
||||
grafana_data:
|
||||
alertmanager_data:
|
||||
|
||||
@@ -35,3 +35,9 @@ scrape_configs:
|
||||
- job_name: openvpn
|
||||
file_sd_configs:
|
||||
- files: ["/etc/prometheus/targets/openvpn/*.yml"]
|
||||
|
||||
# honor_labels: сохраняем job/instance, с которыми хост запушил, а не pushgateway:9091
|
||||
- job_name: pushgateway
|
||||
honor_labels: true
|
||||
static_configs:
|
||||
- targets: ["pushgateway:9091"]
|
||||
|
||||
@@ -35,3 +35,67 @@ groups:
|
||||
labels: {severity: critical}
|
||||
annotations:
|
||||
summary: "КРИТИЧНО: диск почти полон на {{ $labels.instance }} ({{ $labels.mountpoint }}): свободно {{ printf \"%.0f\" $value }}%"
|
||||
|
||||
# Метрики от rpi приходят через pushgateway (SSH-пуш, см. servers/rpi.md).
|
||||
# push_time_seconds — когда группа последний раз обновлялась.
|
||||
- name: rpi
|
||||
rules:
|
||||
- alert: PushMetricsStale
|
||||
expr: time() - push_time_seconds{job=~"smart|backup_.*"} > 3 * 3600
|
||||
for: 10m
|
||||
labels: {severity: warning}
|
||||
annotations:
|
||||
summary: "{{ $labels.instance }}: метрики {{ $labels.job }} не приходят {{ $value | humanizeDuration }} — хост/пуш лежит"
|
||||
|
||||
- alert: SmartHealthFailed
|
||||
expr: smart_health_passed == 0
|
||||
labels: {severity: critical}
|
||||
annotations:
|
||||
summary: "{{ $labels.instance }}: SMART FAILED на диске {{ $labels.model }} ({{ $labels.serial }}) — менять диск"
|
||||
|
||||
- alert: SmartBadSectors
|
||||
expr: smart_attribute_raw{id=~"5|197|198"} > 0
|
||||
labels: {severity: critical}
|
||||
annotations:
|
||||
summary: "{{ $labels.instance }}: {{ $labels.name }} = {{ $value }} на {{ $labels.model }} ({{ $labels.serial }}) — диск начал сыпаться"
|
||||
|
||||
- alert: SmartBadSectorsGrowing
|
||||
expr: increase(smart_attribute_raw{id=~"5|197|198"}[1d]) > 0
|
||||
labels: {severity: critical}
|
||||
annotations:
|
||||
summary: "{{ $labels.instance }}: {{ $labels.name }} растёт на {{ $labels.model }} — срочно вторую копию"
|
||||
|
||||
- alert: SmartUdmaCrcErrors
|
||||
expr: increase(smart_attribute_raw{id="199"}[1d]) > 0
|
||||
labels: {severity: warning}
|
||||
annotations:
|
||||
summary: "{{ $labels.instance }}: CRC-ошибки на {{ $labels.model }} — кабель/USB/питание"
|
||||
|
||||
- alert: SmartSelftestFailed
|
||||
expr: smart_selftest_last_passed == 0
|
||||
labels: {severity: critical}
|
||||
annotations:
|
||||
summary: "{{ $labels.instance }}: самотест SMART провален на {{ $labels.model }} ({{ $labels.serial }})"
|
||||
|
||||
- alert: SmartTemperatureHigh
|
||||
expr: smart_temperature_celsius > 55
|
||||
for: 15m
|
||||
labels: {severity: warning}
|
||||
annotations:
|
||||
summary: "{{ $labels.instance }}: диск {{ $labels.model }} греется: {{ $value }}°C"
|
||||
|
||||
- alert: BackupFailed
|
||||
expr: backup_last_exit_code != 0
|
||||
labels: {severity: warning}
|
||||
annotations:
|
||||
summary: "{{ $labels.instance }}: {{ $labels.job }} завершился с кодом {{ $value }} — см. journalctl на хосте"
|
||||
|
||||
# sync каждые 4ч → 12ч = три пропуска подряд; verify раз в неделю → 8 дней
|
||||
- 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)
|
||||
for: 10m
|
||||
labels: {severity: critical}
|
||||
annotations:
|
||||
summary: "{{ $labels.instance }}: {{ $labels.job }} не было успешного прогона {{ $value | humanizeDuration }}"
|
||||
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
# Forced command для пуш-ключей хостов за NAT (authorized_keys: command="~/bin/pushgw-ingest <instance>").
|
||||
# Клиент передаёт только имя job (ssh ... <job>), метрики — в stdin в text-формате Prometheus.
|
||||
# instance задаётся ключом, а не клиентом: хост не может писать метрики от чужого имени.
|
||||
instance="$1"
|
||||
job="$SSH_ORIGINAL_COMMAND"
|
||||
case "$instance" in ""|*[!a-z0-9_-]*) echo "bad instance" >&2; exit 2;; esac
|
||||
case "$job" in ""|*[!a-z0-9_]*) echo "bad job" >&2; exit 2;; esac
|
||||
exec curl -sS --fail-with-body --max-time 30 -X PUT --data-binary @- \
|
||||
"http://127.0.0.1:9091/metrics/job/$job/instance/$instance"
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# backup-metrics <job> — из ExecStopPost юнита бэкапа: пушит итог прогона (job=backup_<name>).
|
||||
# last_success хранится локально: PUT в pushgateway заменяет группу целиком,
|
||||
# и после неудачи время последнего успеха иначе бы потерялось.
|
||||
set -uo pipefail
|
||||
job="$1"; state=/var/lib/backup-metrics; mkdir -p "$state"
|
||||
now=$(date +%s)
|
||||
code=${EXIT_STATUS:-1}; [ "${SERVICE_RESULT:-}" = success ] && code=0
|
||||
[ "$code" = 0 ] && echo "$now" > "$state/$job.success"
|
||||
last_ok=$(cat "$state/$job.success" 2>/dev/null || echo 0)
|
||||
metrics-push "$job" <<M
|
||||
# TYPE backup_last_exit_code gauge
|
||||
backup_last_exit_code $code
|
||||
backup_last_run_timestamp $now
|
||||
backup_last_success_timestamp $last_ok
|
||||
M
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
# metrics-push <job> < metrics.prom — отправка в pushgateway на fr1.
|
||||
# На fr1 ключ привязан к forced command ~/bin/pushgw-ingest rpi: можно только запушить, больше ничего.
|
||||
exec ssh -i /etc/metrics-push/id_ed25519 \
|
||||
-o UserKnownHostsFile=/etc/metrics-push/known_hosts -o StrictHostKeyChecking=yes \
|
||||
-o BatchMode=yes -o ConnectTimeout=20 \
|
||||
rus@161.97.93.252 "$1"
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
# OneDrive -> /mnt/backup. Ничего не удаляется навсегда:
|
||||
# current/ — точное зеркало OneDrive
|
||||
# archive/<время>/ — всё, что в OneDrive удалили или перезаписали (хранится вечно)
|
||||
# manifests/ — quickxor-хэши архивных файлов (для onedrive-verify)
|
||||
# logs/ — журнал каждого прогона: какие файлы скачаны/изменены/убраны в архив
|
||||
set -euo pipefail
|
||||
ROOT=/mnt/backup/onedrive
|
||||
TS=$(date +%F_%H%M)
|
||||
|
||||
mountpoint -q /mnt/backup || { echo "/mnt/backup не смонтирован — пропуск"; exit 1; }
|
||||
mkdir -p "$ROOT"/{current,archive,manifests,logs}
|
||||
LOG="$ROOT/logs/sync-$TS.log"
|
||||
|
||||
# rclone сам сверяет хэш каждого скачанного файла с хэшем OneDrive
|
||||
rclone sync onedrive: "$ROOT/current" \
|
||||
--backup-dir "$ROOT/archive/$TS" \
|
||||
--exclude "/Personal Vault/**" \
|
||||
--transfers 4 --checkers 8 \
|
||||
--retries 5 --low-level-retries 20 \
|
||||
--log-file "$LOG" --log-level INFO \
|
||||
--stats 10m --stats-one-line
|
||||
|
||||
if [ -d "$ROOT/archive/$TS" ]; then
|
||||
rclone hashsum quickxor "$ROOT/archive/$TS" > "$ROOT/manifests/archive-$TS.qxh"
|
||||
echo "в архив ушло: $(wc -l < "$ROOT/manifests/archive-$TS.qxh") файлов"
|
||||
fi
|
||||
|
||||
echo "OK: current=$(du -sh "$ROOT/current" | cut -f1), диск занят на $(df --output=pcent /mnt/backup | tail -1 | tr -d " ")"
|
||||
[ "$(df --output=pcent /mnt/backup | tail -1 | tr -dc 0-9)" -lt 90 ] || { echo "ВНИМАНИЕ: диск заполнен >90%"; exit 2; }
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
# Еженедельная проверка целостности: перечитывает весь HDD и сверяет хэши
|
||||
# current/ — с хэшами OneDrive (ловит битые/недокачанные/лишние файлы)
|
||||
# archive/ — с сохранёнными манифестами (ловит порчу данных на диске)
|
||||
set -uo pipefail
|
||||
ROOT=/mnt/backup/onedrive
|
||||
TS=$(date +%F_%H%M)
|
||||
LOG="$ROOT/logs/verify-$TS.log"
|
||||
mountpoint -q /mnt/backup || { echo "/mnt/backup не смонтирован — пропуск"; exit 1; }
|
||||
rc=0
|
||||
|
||||
rclone check onedrive: "$ROOT/current" --exclude "/Personal Vault/**" \
|
||||
--checkers 8 --combined "$ROOT/logs/verify-$TS.current.txt" \
|
||||
--log-file "$LOG" --log-level NOTICE || rc=1
|
||||
|
||||
for m in "$ROOT"/manifests/archive-*.qxh; do
|
||||
[ -e "$m" ] || continue
|
||||
d=$(basename "$m" .qxh); d=${d#archive-}
|
||||
rclone checksum quickxor "$m" "$ROOT/archive/$d" --log-file "$LOG" --log-level NOTICE || { echo "архив $d: несовпадение"; rc=1; }
|
||||
done
|
||||
|
||||
grep -vc "^=" "$ROOT/logs/verify-$TS.current.txt" | xargs echo "расхождений в current:"
|
||||
[ $rc -eq 0 ] && echo "OK: целостность подтверждена" || echo "ЕСТЬ РАСХОЖДЕНИЯ — см. $LOG и verify-$TS.current.txt"
|
||||
exit $rc
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
# SMART всех USB/SATA-дисков → pushgateway (job=smart).
|
||||
# -n standby: спящий диск НЕ будим (иначе опрос раз в 15 мин не дал бы ему спать) —
|
||||
# для него отдаём последние снятые значения из кэша и smart_disk_standby=1.
|
||||
set -uo pipefail
|
||||
CACHE=/var/lib/smart-metrics; mkdir -p "$CACHE"
|
||||
out=$(mktemp); trap 'rm -f "$out"' EXIT
|
||||
echo "# TYPE smart_disk_standby gauge" >> "$out"
|
||||
for dev in /dev/sd?; do
|
||||
[ -b "$dev" ] || continue
|
||||
json=$(smartctl -d sat -n standby,7 -j -H -i -A -l selftest "$dev"); rc=$?
|
||||
serial=$(udevadm info -q property -n "$dev" | sed -n 's/^ID_SERIAL_SHORT=//p')
|
||||
[ -n "$serial" ] || continue
|
||||
f="$CACHE/$serial.prom"
|
||||
if [ $rc -eq 7 ]; then
|
||||
standby=1
|
||||
else
|
||||
standby=0
|
||||
echo "$json" | jq -r --arg dev "${dev#/dev/}" '
|
||||
(.model_name // "unknown") as $m | (.serial_number // "unknown") as $s |
|
||||
"{model=\"\($m)\",serial=\"\($s)\",device=\"\($dev)\"" as $l |
|
||||
"smart_health_passed\($l)} \(if .smart_status.passed then 1 else 0 end)",
|
||||
"smart_temperature_celsius\($l)} \(.temperature.current // "NaN")",
|
||||
"smart_power_on_hours\($l)} \(.power_on_time.hours // "NaN")",
|
||||
(.ata_smart_self_test_log.standard.table[0]? // empty |
|
||||
"smart_selftest_last_passed\($l)} \(if .status.passed then 1 else 0 end)"),
|
||||
(.ata_smart_attributes.table[]? |
|
||||
"smart_attribute_raw\($l),id=\"\(.id)\",name=\"\(.name)\"} \(.raw.value)",
|
||||
"smart_attribute_value\($l),id=\"\(.id)\",name=\"\(.name)\"} \(.value)"),
|
||||
"smart_last_read_timestamp\($l)} \(now|floor)"
|
||||
' > "$f.tmp" && [ -s "$f.tmp" ] && mv "$f.tmp" "$f"
|
||||
fi
|
||||
[ -f "$f" ] && cat "$f" >> "$out"
|
||||
echo "smart_disk_standby{serial=\"$serial\",device=\"${dev#/dev/}\"} $standby" >> "$out"
|
||||
done
|
||||
metrics-push smart < "$out"
|
||||
@@ -0,0 +1 @@
|
||||
UUID=6eea3421-da3f-4a0f-b66d-26000b9a1b01 /mnt/backup ext4 defaults,noatime,nofail,x-systemd.device-timeout=30s 0 2
|
||||
@@ -0,0 +1,7 @@
|
||||
# smartd: следит за всеми дисками (DEVICESCAN подхватит и второй диск после хаба).
|
||||
# -d removable — тип моста определяется сам; диска может не быть (не падать)
|
||||
# -n standby,q — не будить спящий диск ради опроса
|
||||
# -s — короткий самотест по субботам 03:00, длинный (~10ч на 4ТБ) 1-го числа в 05:00
|
||||
# -W 4,50,55 — фиксировать скачки температуры ≥4°, порог предупреждения 50, критичный 55
|
||||
# Почту не шлём (-m нет): тревоги идут через Prometheus на fr1 (smart-metrics → pushgateway).
|
||||
DEVICESCAN -d removable -a -o on -S on -n standby,q -s (S/../../6/03|L/../01/./05) -W 4,50,55
|
||||
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=OneDrive -> HDD backup (rclone)
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
RequiresMountsFor=/mnt/backup
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=rus
|
||||
ExecStart=/usr/bin/flock /run/lock/onedrive-backup.lock /usr/local/bin/onedrive-backup
|
||||
ExecStopPost=+/usr/local/bin/backup-metrics backup_onedrive
|
||||
Nice=10
|
||||
IOSchedulingClass=idle
|
||||
TimeoutStartSec=infinity
|
||||
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=OneDrive backup every 4h
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 00/4:30
|
||||
RandomizedDelaySec=5m
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=OneDrive backup integrity check
|
||||
Wants=network-online.target
|
||||
After=network-online.target onedrive-backup.service
|
||||
RequiresMountsFor=/mnt/backup
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=rus
|
||||
ExecStart=/usr/bin/flock /run/lock/onedrive-backup.lock /usr/local/bin/onedrive-verify
|
||||
ExecStopPost=+/usr/local/bin/backup-metrics backup_onedrive_verify
|
||||
Nice=15
|
||||
IOSchedulingClass=idle
|
||||
TimeoutStartSec=infinity
|
||||
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Weekly OneDrive backup integrity check
|
||||
|
||||
[Timer]
|
||||
OnCalendar=Sun *-*-* 05:00
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -0,0 +1,8 @@
|
||||
[Unit]
|
||||
Description=Push SMART metrics to pushgateway on fr1
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/bin/smart-metrics
|
||||
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=SMART metrics push every 15 min
|
||||
|
||||
[Timer]
|
||||
OnBootSec=2min
|
||||
OnUnitActiveSec=15min
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
Reference in New Issue
Block a user