Compare commits

1 Commits
Author SHA1 Message Date
gilfanovrandClaude Opus 5.5 d1269162e7 rpi: бэкап OneDrive на HDD + SMART/бэкап-метрики через pushgateway на fr1
- servers/rpi.md: новая домашняя Raspberry Pi 4 (Trixie, cloud-init), HDD 4 ТБ,
  грабли с питанием двух HDD от USB
- stacks/rpi-backup: rclone sync с вечным архивом версий, еженедельная сверка
  хэшей, smartd, пуш SMART и статуса бэкапов
- fr1: pushgateway (127.0.0.1:9091), приём по SSH через forced command
  ~/bin/pushgw-ingest, алерты группы rpi

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 13:28:20 +03:00
20 changed files with 339 additions and 2 deletions
+1
View File
@@ -6,6 +6,7 @@
- [`servers/de1.md`](servers/de1.md) — 213.136.74.247. **Выведен** (все сервисы остановлены, метрики удалены): был прокси/VPN-нодой (telemt, AmneziaWG, OpenVPN) плюс gitea, openclaw, боты — всё уехало на fr1 или в бэкапы. ⚠️ Сам файл описания под вывод не переписан. - [`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/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/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` в том же прогоне. Контракт и Тронул сервер — обнови `servers/<хост>.md` в том же прогоне. Контракт и
+6 -2
View File
@@ -15,6 +15,7 @@
- ⚠️ **Группа `docker` = root-эквивалент.** `docker:x:988:rus,tgclaude` — любой её член монтирует `/` в контейнер и делает что угодно от рута (этим же способом чистились root-овы файлы 2026-09-08). Поэтому «у `tgclaude` нет sudo» **не** означает ограничения прав на хосте. - ⚠️ **Группа `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). - Вход с Mac: `ssh rus-161` (алиас в `~/.ssh/config`) → ключ `~/.ssh/id_161_97_93_252` (ed25519).
- root-доступ по SSH закрыт; для root изнутри — `sudo -i`. - 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`) ## 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`. ⚠️ В основном `/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. - Сертификаты LE: `mon`, `git`, `share`, `www`. `cam.ruzzy.dev` удалён 2026-09-08 вместе с ipcam.
## Мониторинг (`~/monitoring`, stack в репо `stacks/monitoring/`) ## Мониторинг (`~/monitoring`, stack в репо `stacks/monitoring/`)
- Стек: **Prometheus + Grafana + node-exporter + cAdvisor + Alertmanager** (docker compose). - Стек: **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. Токен — в `~/monitoring/alertmanager/telegram_token` (chown 65534, не в git). - **Алерты:** 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, активных алертов нет. - 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}`). - 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` — пустой. - **Тома:** `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). - **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 скрыты). - Дашборд **Network / Traffic** (`network.json`): RX/TX по интерфейсам, пакеты, ошибки/дропы, статус — с фильтром по instance/device (lo/veth/docker скрыты).
- DNS: `mon.ruzzy.dev` → 161.97.93.252. - 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. - Подключён `ru1` (node + cadvisor + openvpn-exporter, напрямую по публичному IP). `de1` выведен — его таргеты удалены. Планируется: экспортёры wireguard/awg и telemt.
## Gitea (`~/gitea`, stack в репо `stacks/gitea/`) ## Gitea (`~/gitea`, stack в репо `stacks/gitea/`)
+50
View File
@@ -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 — прогноз ресурса пока оценочный.
+17
View File
@@ -33,6 +33,22 @@ services:
- '127.0.0.1:9093:9093' - '127.0.0.1:9093:9093'
networks: [monitoring] 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: grafana:
image: grafana/grafana:latest image: grafana/grafana:latest
container_name: grafana container_name: grafana
@@ -93,6 +109,7 @@ services:
networks: [monitoring] networks: [monitoring]
volumes: volumes:
pushgateway_data:
prometheus_data: prometheus_data:
grafana_data: grafana_data:
alertmanager_data: alertmanager_data:
@@ -35,3 +35,9 @@ scrape_configs:
- job_name: openvpn - job_name: openvpn
file_sd_configs: file_sd_configs:
- files: ["/etc/prometheus/targets/openvpn/*.yml"] - 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} labels: {severity: critical}
annotations: annotations:
summary: "КРИТИЧНО: диск почти полон на {{ $labels.instance }} ({{ $labels.mountpoint }}): свободно {{ printf \"%.0f\" $value }}%" 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 }}"
+10
View File
@@ -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"
+16
View File
@@ -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
+7
View File
@@ -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"
+30
View File
@@ -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; }
+24
View File
@@ -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
+36
View File
@@ -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"
+1
View File
@@ -0,0 +1 @@
UUID=6eea3421-da3f-4a0f-b66d-26000b9a1b01 /mnt/backup ext4 defaults,noatime,nofail,x-systemd.device-timeout=30s 0 2
+7
View File
@@ -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