Compare commits
2 Commits
16c254510a
...
604f0c705f
| Author | SHA1 | Date | |
|---|---|---|---|
| 604f0c705f | |||
| 53769e6832 |
@@ -24,6 +24,7 @@
|
||||
| cloud.katykhin.ru | — |
|
||||
| docs.katykhin.ru | — |
|
||||
| git.katykhin.ru | — |
|
||||
| healthchecks.katykhin.ru | Healthchecks (Dead man's switch для бэкапов; на VPS Миран) |
|
||||
| home.katykhin.ru | Homepage |
|
||||
| immich.katykhin.ru | — |
|
||||
| mini-lm.katykhin.ru | — |
|
||||
@@ -94,8 +95,9 @@ pct create 105 local:vztmpl/debian-12-standard_12.2-1_amd64.tar.zst \
|
||||
|
||||
## Дополнительно
|
||||
|
||||
- **Хост Proxmox:** скрипты, таймеры, пути — [host-proxmox.md](../containers/host-proxmox.md).
|
||||
- **Схема сети и зависимости:** полная топология (роутер, Proxmox, контейнеры, VPS), таблица IP/доменов, маршруты NPM, кто от кого зависит, единые точки отказа (SPOF). → [Схема сети и зависимости](../network/network-topology.md).
|
||||
- **Homepage:** на контейнере 103, конфиг сервисов в `/opt/docker/homepage/config/services.yaml` (ссылки на NPM, Invidious, AdGuard, Immich, Galene, Vaultwarden и т.д.).
|
||||
- **Homepage:** на контейнере 103, конфиг сервисов в `/opt/docker/homepage/config/services.yaml` (ссылки на NPM, Invidious, AdGuard, Immich, Galene, Vaultwarden, Healthchecks, Netdata и т.д.).
|
||||
- **VPN (VPS):** отдельный сервер 185.103.253.99, AmneziaWG для обхода блокировок. → [VPN-сервер (VPS, AmneziaWG)](../vps/vpn-vps-amneziawg.md).
|
||||
- **Роутер:** Netcraze Speedster, два WireGuard/AmneziaWG (Германия / США), маршрутизация части трафика через VPN. → [Роутер Netcraze Speedster](../network/router-netcraze-speedster.md).
|
||||
- **VPS Миран (СПБ):** боты (telegram-helper-bot, anonBot), prod-инфраструктура, STUN/TURN для Galene. → [VPS Миран: боты и STUN/TURN](../vps/vps-miran-bots.md).
|
||||
|
||||
@@ -8,6 +8,16 @@
|
||||
|
||||
Все локальные бэкапы лежат на отдельном диске хоста Proxmox: **/dev/sdb1**, смонтирован в **/mnt/backup**.
|
||||
|
||||
### Карта дисков (Proxmox host)
|
||||
|
||||
| Устройство | Тип | Размер | Использование |
|
||||
|--------------|------|--------|----------------------------------|
|
||||
| /dev/nvme0n1 | NVMe | 256 GB | LVM (система, local-lvm) |
|
||||
| /dev/sda | HDD | 2 TB | ZFS |
|
||||
| /dev/sdb | SSD | 2 TB | ext4, /mnt/backup |
|
||||
| /dev/sdc | HDD | 2 TB | ZFS (RAID1 с sda) |
|
||||
| /dev/sdd | HDD | 8 TB | ext4 (внешний) |
|
||||
|
||||
```
|
||||
/mnt/backup/
|
||||
├── proxmox/
|
||||
@@ -36,21 +46,21 @@
|
||||
|
||||
| Что | Откуда | Куда (локально) | Когда | Хранение | Уведомление |
|
||||
|-----|--------|------------------|------|----------|--------------|
|
||||
| **VPS Миран (telegram-helper-bot)** | VPS 185.147.80.190: БД `tg-bot-database.db`, каталог `voice_users`, бакет S3 (Miran) | `/mnt/backup/vps/miran/` (db/, voice_users/, s3/) | **01:00** (cron: `backup-vps-miran.sh`) | БД: 14 дней; voice_users и S3 — перезапись | 🖥️ VPS Миран |
|
||||
| **БД Nextcloud (PostgreSQL)** | CT 101, контейнер `nextcloud-db-1` в `/opt/nextcloud` | `/mnt/backup/databases/ct101-nextcloud/` | **01:15** (cron: `backup-ct101-pgdump.sh`) | 14 дней | 🗄️ Nextcloud (БД) |
|
||||
| **Оригиналы фото Immich** | VM 200, `/mnt/data/library/` | `/mnt/backup/photos/library/` | **01:30** (cron: `backup-immich-photos.sh`, rsync) | Все копии (без автоудаления) | 📷 Фото Immich (rsync) |
|
||||
| **Конфиги MTProto (VPS Германия)** | VPS 185.103.253.99: mtg.service, nginx (katykhin.store), Let's Encrypt, `/var/www/katykhin.store` | `/mnt/backup/vps/mtproto-germany/` (архивы mtproto-config-*.tar.gz) | **01:45** (cron: `backup-vps-mtproto.sh`) | 14 дней | 🌐 VPS MTProto (DE) |
|
||||
| **LXC и VM** | Все выбранные контейнеры (100–109) и VM 200 | `/mnt/backup/proxmox/dump/` | **02:00** (задание в Proxmox UI) | По настройкам задания (7 daily, 4 weekly, 6 monthly) | 💾 Backup local (cron 03:00) |
|
||||
| **Конфиги хоста** | `/etc/pve`, `/etc/network/interfaces`, `/etc/hosts`, `/etc/resolv.conf` | `/mnt/backup/proxmox/etc-pve/` | **02:15** (cron: `backup-etc-pve.sh`) | 30 дней | ⚙️ Конфиги хоста |
|
||||
| **БД Paperless (PostgreSQL)** | CT 104, контейнер `paperless-db-1` в `/opt/paperless` | `/mnt/backup/databases/ct104-paperless/` | **02:30** (cron: `backup-ct104-pgdump.sh`) | 14 дней | 🗄️ Paperless (БД) |
|
||||
| **Данные Vaultwarden (пароли)** | CT 103, `/opt/docker/vaultwarden/data` | `/mnt/backup/other/vaultwarden/`; каталог в restic → Yandex | **02:45** (cron: `backup-vaultwarden-data.sh`) | 14 дней | 🔐 Vaultwarden |
|
||||
| **БД Gitea (PostgreSQL)** | CT 103, контейнер `gitea-db-1` в `/opt/gitea` | `/mnt/backup/databases/ct103-gitea/` | **03:00** (cron: `backup-ct103-gitea-pgdump.sh`) | 14 дней | 🗄️ Gitea (БД) |
|
||||
| **БД Immich (PostgreSQL)** | VM 200, контейнер `database` в `/opt/immich` | `/mnt/backup/databases/vm200-immich/` | **03:15** (cron: `backup-vm200-pgdump.sh`) | 14 дней | 🗄️ Immich (БД) |
|
||||
| **Векторы RAG (CT 105)** | CT 105, `/home/rag-service/data/vectors/` (vectors.npz) | `/mnt/backup/other/ct105-vectors/` | **03:30** (cron: `backup-ct105-vectors.sh`) | 14 дней | 📐 Векторы RAG |
|
||||
| **Выгрузка в Yandex (restic)** | `/mnt/backup` без photos | Yandex Object Storage | **04:00** (cron: `backup-restic-yandex.sh`) | 3 daily, 2 weekly, 2 monthly | ☁️ Restic Yandex |
|
||||
| **Выгрузка в Yandex (restic, фото)** | `/mnt/backup/photos` | Yandex Object Storage (тот же репо) | **04:10** (cron: `backup-restic-yandex-photos.sh`) | 3 daily, 2 weekly, 2 monthly | 📷 Restic Yandex (photos) |
|
||||
| **VPS Миран (telegram-helper-bot)** | VPS 185.147.80.190: БД `tg-bot-database.db`, каталог `voice_users`, бакет S3 (Miran) | `/mnt/backup/vps/miran/` (db/, voice_users/, s3/) | **01:00** (timer: `backup-vps-miran`) | БД: 14 дней; voice_users и S3 — перезапись | 🖥️ VPS Миран |
|
||||
| **БД Nextcloud (PostgreSQL)** | CT 101, контейнер `nextcloud-db-1` в `/opt/nextcloud` | `/mnt/backup/databases/ct101-nextcloud/` | **01:15** (timer: `backup-ct101-pgdump`) | 14 дней | 🗄️ Nextcloud (БД) |
|
||||
| **Оригиналы фото Immich** | VM 200, `/mnt/data/library/` | `/mnt/backup/photos/library/` | **01:30** (timer: `backup-immich-photos`) | Все копии (без автоудаления) | 📷 Фото Immich (rsync) |
|
||||
| **Конфиги MTProto (VPS Германия)** | VPS 185.103.253.99: mtg.service, nginx (katykhin.store), Let's Encrypt, `/var/www/katykhin.store` | `/mnt/backup/vps/mtproto-germany/` (архивы mtproto-config-*.tar.gz) | **01:45** (timer: `backup-vps-mtproto`) | 14 дней | 🌐 VPS MTProto (DE) |
|
||||
| **LXC и VM** | Все выбранные контейнеры (100–109) и VM 200 | `/mnt/backup/proxmox/dump/` | **02:00** (задание в Proxmox UI) | По настройкам задания (7 daily, 4 weekly, 6 monthly) | 💾 Backup local (timer 03:00) |
|
||||
| **Конфиги хоста** | `/etc/pve`, `/etc/network/interfaces`, `/etc/hosts`, `/etc/resolv.conf` | `/mnt/backup/proxmox/etc-pve/` | **02:15** (timer: `backup-etc-pve`) | 30 дней | ⚙️ Конфиги хоста |
|
||||
| **БД Paperless (PostgreSQL)** | CT 104, контейнер `paperless-db-1` в `/opt/paperless` | `/mnt/backup/databases/ct104-paperless/` | **02:30** (timer: `backup-ct104-pgdump`) | 14 дней | 🗄️ Paperless (БД) |
|
||||
| **Данные Vaultwarden (пароли)** | CT 103, `/opt/docker/vaultwarden/data` | `/mnt/backup/other/vaultwarden/`; каталог в restic → Yandex | **02:45** (timer: `backup-vaultwarden-data`) | 14 дней | 🔐 Vaultwarden |
|
||||
| **БД Gitea (PostgreSQL)** | CT 103, контейнер `gitea-db-1` в `/opt/gitea` | `/mnt/backup/databases/ct103-gitea/` | **03:00** (timer: `backup-ct103-gitea-pgdump`) | 14 дней | 🗄️ Gitea (БД) |
|
||||
| **БД Immich (PostgreSQL)** | VM 200, контейнер `database` в `/opt/immich` | `/mnt/backup/databases/vm200-immich/` | **03:15** (timer: `backup-vm200-pgdump`) | 14 дней | 🗄️ Immich (БД) |
|
||||
| **Векторы RAG (CT 105)** | CT 105, `/home/rag-service/data/vectors/` (vectors.npz) | `/mnt/backup/other/ct105-vectors/` | **03:30** (timer: `backup-ct105-vectors`) | 14 дней | 📐 Векторы RAG |
|
||||
| **Выгрузка в Yandex (restic)** | `/mnt/backup` без photos | Yandex Object Storage | **04:00** (timer: `backup-restic-yandex`) | 3 daily, 2 weekly, 2 monthly | ☁️ Restic Yandex |
|
||||
| **Выгрузка в Yandex (restic, фото)** | `/mnt/backup/photos` | Yandex Object Storage (тот же репо) | **04:10** (timer: `backup-restic-yandex-photos`) | 3 daily, 2 weekly, 2 monthly | 📷 Restic Yandex (photos) |
|
||||
|
||||
**Окно бэкапов:** внутренние копии — **01:00–03:30**; выгрузка в облако — **04:00** (основной restic), **04:10** (restic фото). **05:00** зарезервировано под плановую перезагрузку сервера. Задание vzdump — из веб-интерфейса Proxmox (Центр обработки данных → Резервная копия).
|
||||
**Окно бэкапов:** внутренние копии — **01:00–03:30**; выгрузка в облако — **04:00** (основной restic), **04:10** (restic фото). **04:35** — ping Healthchecks (Dead man's switch). **05:00** зарезервировано под плановую перезагрузку сервера. Задание vzdump — из веб-интерфейса Proxmox (Центр обработки данных → Резервная копия).
|
||||
|
||||
---
|
||||
|
||||
@@ -406,26 +416,31 @@ restic restore SNAPSHOT_ID --target /mnt/backup/restore-db --path /mnt/backup/da
|
||||
|
||||
---
|
||||
|
||||
## Скрипты на хосте Proxmox
|
||||
## Скрипты и systemd timers на хосте Proxmox
|
||||
|
||||
Бэкапы запускаются через **systemd timers** (миграция с cron). Unit-файлы: `scripts/systemd/`. Копировать на хост: `cp scripts/systemd/*.service scripts/systemd/*.timer /etc/systemd/system/`, затем `systemctl daemon-reload` и `systemctl enable --now <timer>`.
|
||||
|
||||
| Скрипт | Назначение | Cron |
|
||||
|--------|------------|------|
|
||||
| `/root/scripts/backup-vps-miran.sh` | Бэкап VPS Миран: БД бота, voice_users, S3 (Miran) | 0 1 * * * |
|
||||
| `/root/scripts/backup-ct101-pgdump.sh` | Логический дамп БД Nextcloud из CT 101 | 15 1 * * * |
|
||||
| `/root/scripts/backup-immich-photos.sh` | Копирование библиотеки фото Immich (rsync с VM 200) | 30 1 * * * |
|
||||
| `/root/scripts/backup-vps-mtproto.sh` | Копирование конфигов MTProto + сайт с VPS Германия (185.103.253.99) | 45 1 * * * |
|
||||
| `/root/scripts/backup-etc-pve.sh` | Бэкап /etc/pve и конфигов хоста | 15 2 * * * |
|
||||
| `/root/scripts/backup-ct104-pgdump.sh` | Логический дамп БД Paperless из CT 104 | 30 2 * * * |
|
||||
| `/root/scripts/backup-vaultwarden-data.sh` | Копирование данных Vaultwarden (пароли) из CT 103 | 45 2 * * * |
|
||||
| `/root/scripts/backup-ct103-gitea-pgdump.sh` | Логический дамп БД Gitea из CT 103 | 0 3 * * * |
|
||||
| `/root/scripts/notify-vzdump-success.sh` | Проверка локального vzdump за последние 2 ч, отправка сводки в Telegram | 0 3 * * * |
|
||||
| `/root/scripts/backup-vm200-pgdump.sh` | Логический дамп БД Immich с VM 200 | 15 3 * * * |
|
||||
| `/root/scripts/backup-ct105-vectors.sh` | Копирование векторов RAG (vectors.npz) из CT 105 | 30 3 * * * |
|
||||
| `/root/scripts/backup-restic-yandex.sh` | Выгрузка /mnt/backup (без photos) в Yandex S3 (restic), retention 3/2/2 | 0 4 * * * |
|
||||
| `/root/scripts/backup-restic-yandex-photos.sh` | Выгрузка только /mnt/backup/photos в Yandex S3 (тот же репо), retention 3/2/2 | 10 4 * * * |
|
||||
| `/root/scripts/notify-telegram.sh` | Шлюз отправки уведомлений в Telegram (вызывают скрипты бэкапов) | — |
|
||||
| Скрипт | Timer | Расписание |
|
||||
|--------|-------|------------|
|
||||
| `backup-vps-miran.sh` | backup-vps-miran.timer | 01:00 |
|
||||
| `backup-ct101-pgdump.sh` | backup-ct101-pgdump.timer | 01:15 |
|
||||
| `backup-immich-photos.sh` | backup-immich-photos.timer | 01:30 |
|
||||
| `backup-vps-mtproto.sh` | backup-vps-mtproto.timer | 01:45 |
|
||||
| `backup-etc-pve.sh` | backup-etc-pve.timer | 02:15 |
|
||||
| `backup-ct104-pgdump.sh` | backup-ct104-pgdump.timer | 02:30 |
|
||||
| `backup-vaultwarden-data.sh` | backup-vaultwarden-data.timer | 02:45 |
|
||||
| `backup-ct103-gitea-pgdump.sh` | backup-ct103-gitea-pgdump.timer | 03:00 |
|
||||
| `notify-vzdump-success.sh` | notify-vzdump-success.timer | 03:00 |
|
||||
| `backup-vm200-pgdump.sh` | backup-vm200-pgdump.timer | 03:15 |
|
||||
| `backup-ct105-vectors.sh` | backup-ct105-vectors.timer | 03:30 |
|
||||
| `backup-restic-yandex.sh` | backup-restic-yandex.timer | 04:00 |
|
||||
| `backup-restic-yandex-photos.sh` | backup-restic-yandex-photos.timer | 04:10 |
|
||||
| `healthcheck-ping.sh` | backup-healthcheck-ping.timer | 04:35 (Healthchecks) |
|
||||
| `watchdog-timers.sh` | backup-watchdog-timers.timer | 12:00 (проверка failed timers, .ok) |
|
||||
|
||||
**Healthcheck-файлы:** при успешном завершении каждый скрипт бэкапа пишет `echo $(date -Iseconds) > /var/run/backup-<name>.ok`. Watchdog проверяет раз в день: если файл старше 24 ч — алерт в Telegram.
|
||||
|
||||
**Тест восстановления:** см. [restore-test-manual.md](restore-test-manual.md). Автоматические скрипты: `verify-restore-level1.sh` (restic check, дамп Nextcloud), `verify-vzdump-level2.sh` (vzdump CT 107). Таймеры: `verify-restore-level1-weekly`, `-monthly-check`, `-full-check`, `-monthly-dump`, `verify-vzdump-level2`.
|
||||
|
||||
Задание vzdump (LXC/VM) настраивается в Proxmox UI (расписание 02:00). **05:00** оставлено свободным для плановой перезагрузки сервера.
|
||||
|
||||
@@ -452,7 +467,7 @@ pct exec 103 -- ls -la /opt/docker/vaultwarden/data
|
||||
pct exec 103 -- tar cf - -C /opt/docker/vaultwarden data | wc -c
|
||||
```
|
||||
|
||||
**Запуск из cron и доступ к Vaultwarden (bw):** В cron окружение ограничено: часто `PATH` не содержит `/usr/local/bin`, где обычно установлен `bw`. Скрипты дампов БД (ct101, ct104, ct103) в начале задают `export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"`, поэтому при запуске из cron `bw` и `jq` находятся без правки crontab. Нужно: 1) файл с мастер-паролем, например `/root/.bw-master` (chmod 600), и при необходимости переменная `BW_MASTER_PASSWORD_FILE=/root/.bw-master`; 2) один раз с интерактивной сессии: `bw config server https://vault.katykhin.ru`, `bw login` (сохранит сессию в конфиг); 3) при каждом запуске скрипт делает `bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw` и подставляет пароль БД. Если вручную дамп идёт, а из cron — нет, проверьте: наличие `/root/.bw-master`, права доступа, что `bw` доступен по этому PATH (запустите скрипт через `env -i PATH=/usr/local/bin:/usr/bin:/bin /root/scripts/backup-ct103-gitea-pgdump.sh` для имитации cron).
|
||||
**Запуск из systemd timer и доступ к Vaultwarden (bw):** В окружении systemd timer `PATH` может не содержать `/usr/local/bin`, где обычно установлен `bw`. Скрипты дампов БД задают `export PATH="/usr/local/bin:..."`, поэтому `bw` и `jq` находятся. Нужно: 1) файл `/root/.bw-master` (chmod 600) с мастер-паролем; 2) один раз: `bw config server https://vault.katykhin.ru`, `bw login`; 3) при каждом запуске скрипт делает `bw unlock --passwordfile /root/.bw-master --raw`. Если вручную дамп идёт, а из таймера — нет, проверьте `/root/.bw-master` и PATH.
|
||||
|
||||
### Почему размер дампа меньше размера БД на диске
|
||||
|
||||
@@ -477,7 +492,7 @@ pct exec 101 -- docker exec nextcloud-db-1 psql -U nextcloud -d nextcloud -t -c
|
||||
|
||||
## Уведомления в Telegram
|
||||
|
||||
После **успешного** выполнения каждого бэкапа в Telegram отправляется короткое сообщение (заголовок с эмодзи + краткая сводка). Уведомления приходят по завершении соответствующего скрипта; для локального vzdump — по cron в **03:00** (проверка файлов за последние 2 часа).
|
||||
После **успешного** выполнения каждого бэкапа в Telegram отправляется короткое сообщение (заголовок с эмодзи + краткая сводка). Уведомления приходят по завершении соответствующего скрипта; для локального vzdump — таймер `notify-vzdump-success` в **03:00** (проверка файлов за последние 2 часа).
|
||||
|
||||
|
||||
| Заголовок | Когда | Тело сообщения |
|
||||
@@ -516,8 +531,6 @@ chmod 600 /root/.telegram-notify.env
|
||||
|
||||
Если конфига или кредов нет, шлюз тихо выходит с 0 и не ломает вызывающие скрипты.
|
||||
|
||||
**Позже** тот же шлюз можно вызывать с VM 200 или с VPS (например по SSH на хост Proxmox) — отдельно не реализовано, архитектура это допускает.
|
||||
|
||||
---
|
||||
|
||||
## Связанные документы
|
||||
@@ -525,4 +538,8 @@ chmod 600 /root/.telegram-notify.env
|
||||
- [Vaultwarden и секреты](../vaultwarden-secrets.md) — получение паролей через `bw` для скриптов бэкапов.
|
||||
- [Архитектура](../architecture/architecture.md) — хост, IP, доступ.
|
||||
- [VM 200 (Immich)](../containers/container-200.md) — сервисы, пути, .env.
|
||||
- [Ручной тест восстановления](restore-test-manual.md) — пошаговые команды для полной проверки restore.
|
||||
- [Healthchecks на VPS Миран](../vps/healthchecks-miran-setup.md) — Dead man's switch, ping после бэкапов.
|
||||
- [Netdata на Proxmox](../monitoring/netdata-proxmox-setup.md) — мониторинг CPU, RAM, дисков, алерты в Telegram.
|
||||
- [SMART и smartd](../monitoring/smartd-setup.md) — мониторинг дисков, уведомления при отклонениях.
|
||||
|
||||
|
||||
140
docs/backup/restore-test-manual.md
Normal file
140
docs/backup/restore-test-manual.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# Ручной тест восстановления (уровень 3)
|
||||
|
||||
Пошаговые команды для полной проверки восстановления после потери данных или миграции. Выполнять периодически (раз в 6–12 месяцев) или после значительных изменений инфраструктуры.
|
||||
|
||||
---
|
||||
|
||||
## 1. Полный restore на отдельный диск
|
||||
|
||||
**Когда нужно:** проверка, что все бэкапы доступны и можно восстановить систему на новом диске.
|
||||
|
||||
### Подготовка
|
||||
|
||||
1. Подключить диск с достаточным объёмом (например 2 TB) или использовать временный раздел.
|
||||
2. Смонтировать в `/mnt/restore-test` (или аналогичный путь).
|
||||
3. Убедиться, что есть креды restic: `/root/.restic-yandex.env`, `/root/.restic-password` или Vaultwarden (объект RESTIC).
|
||||
|
||||
### Восстановление из restic (Yandex)
|
||||
|
||||
```bash
|
||||
# Список снимков
|
||||
set -a; source /root/.restic-yandex.env; set +a
|
||||
export RESTIC_PASSWORD_FILE=/root/.restic-password
|
||||
export AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION:-ru-central1}
|
||||
restic snapshots
|
||||
|
||||
# Восстановить основной снимок (без photos) в каталог
|
||||
restic restore latest --target /mnt/restore-test --path /mnt/backup
|
||||
|
||||
# Восстановить фото (отдельный снимок)
|
||||
restic snapshots | grep photos
|
||||
restic restore <SNAPSHOT_ID> --target /mnt/restore-test --path /mnt/backup/photos
|
||||
```
|
||||
|
||||
Файлы появятся в `/mnt/restore-test/mnt/backup/`. Проверить наличие:
|
||||
- `proxmox/dump/dump/` — vzdump
|
||||
- `proxmox/etc-pve/` — конфиги хоста
|
||||
- `databases/` — дампы БД
|
||||
- `other/vaultwarden/` — архив Vaultwarden
|
||||
- `photos/library/` — фото Immich
|
||||
|
||||
---
|
||||
|
||||
## 2. Проверка Immich (веб, загрузка фото)
|
||||
|
||||
**Цель:** убедиться, что Immich работает, веб-интерфейс доступен, загрузка фото проходит.
|
||||
|
||||
### Подготовка
|
||||
|
||||
- Immich доступен по https://immich.katykhin.ru (через NPM).
|
||||
- ВМ 200: `ssh admin@192.168.1.200`.
|
||||
|
||||
### Шаги
|
||||
|
||||
1. **Открыть:** https://immich.katykhin.ru
|
||||
2. **Войти** под учётной записью (логин/пароль из менеджера паролей).
|
||||
3. **Загрузить тестовое фото:**
|
||||
- Нажать «Upload» (или загрузить через drag-and-drop).
|
||||
- Выбрать небольшое изображение (например 1–2 MB).
|
||||
- Дождаться завершения загрузки и появления в библиотеке.
|
||||
4. **Проверить:** фото появилось в галерее, превью отображается, метаданные доступны.
|
||||
|
||||
### Если Immich не загружается
|
||||
|
||||
- Проверить: `ssh admin@192.168.1.200 "cd /opt/immich && docker compose ps"` — все контейнеры running.
|
||||
- Логи: `docker logs immich_server` (или `immich_upload_optimizer`).
|
||||
- NPM: прокси на 192.168.1.200:2283.
|
||||
|
||||
---
|
||||
|
||||
## 3. Проверка Nextcloud (веб, загрузка файла)
|
||||
|
||||
**Цель:** убедиться, что Nextcloud доступен и загрузка файлов работает.
|
||||
|
||||
### Подготовка
|
||||
|
||||
- Nextcloud: https://cloud.katykhin.ru
|
||||
- Контейнер 101: `ssh root@192.168.1.101` или `pct exec 101 -- bash`.
|
||||
|
||||
### Шаги
|
||||
|
||||
1. **Открыть:** https://cloud.katykhin.ru
|
||||
2. **Войти** под учётной записью (логин/пароль из менеджера паролей).
|
||||
3. **Загрузить тестовый файл:**
|
||||
- Перейти в «Files» (или «Файлы»).
|
||||
- Нажать «Upload» или перетащить файл (например .txt или .pdf).
|
||||
- Дождаться завершения загрузки.
|
||||
4. **Проверить:** файл отображается в списке, можно скачать.
|
||||
|
||||
### Если Nextcloud не работает
|
||||
|
||||
- Проверить: `pct exec 101 -- docker ps` — контейнеры nextcloud и nextcloud-db-1 running.
|
||||
- Логи: `docker logs nextcloud-app-1` (или имя контейнера из compose).
|
||||
|
||||
---
|
||||
|
||||
## 4. Проверка GPU passthrough на VM 200
|
||||
|
||||
**Цель:** убедиться, что GPU проброшена в Immich ML и распознавание работает.
|
||||
|
||||
### Подготовка
|
||||
|
||||
- VM 200: `ssh admin@192.168.1.200`
|
||||
- В Immich: включить ML (Settings → Machine Learning).
|
||||
|
||||
### Шаги
|
||||
|
||||
1. **Проверить GPU в контейнере ML:**
|
||||
```bash
|
||||
ssh admin@192.168.1.200
|
||||
cd /opt/immich
|
||||
docker exec immich_machine_learning nvidia-smi
|
||||
```
|
||||
Ожидаемый вывод: информация о GPU (модель, память, драйвер).
|
||||
|
||||
2. **Проверить распознавание в Immich:**
|
||||
- Загрузить фото с лицами или объектами.
|
||||
- Дождаться обработки ML (иконка «Scan» в интерфейсе).
|
||||
- Проверить: объекты/лица распознаны, теги добавлены.
|
||||
|
||||
3. **Если nvidia-smi не работает:**
|
||||
- На хосте Proxmox: проверить `hostpci0` в конфиге VM 200: `cat /etc/pve/qemu-server/200.conf`
|
||||
- Убедиться, что PCI-устройство GPU передано в ВМ (`hostpci0: 0000:xx:00.0` и т.п.).
|
||||
- Перезапустить ВМ при необходимости: `qm stop 200 && qm start 200`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Дополнительные проверки (по желанию)
|
||||
|
||||
- **Vaultwarden:** https://vault.katykhin.ru — вход, синхронизация.
|
||||
- **Gitea:** https://git.katykhin.ru — вход, список репозиториев.
|
||||
- **Paperless:** https://docs.katykhin.ru — вход, поиск документов.
|
||||
- **Galene:** https://call.katykhin.ru — вход в комнату.
|
||||
|
||||
---
|
||||
|
||||
## Связанные документы
|
||||
|
||||
- [backup-howto](backup-howto.md) — восстановление из vzdump, restic, дампов БД, расписание таймеров.
|
||||
- [container-200](../containers/container-200.md) — VM 200 (Immich), GPU, пути.
|
||||
- [architecture](../architecture/architecture.md) — хост, IP, доступ.
|
||||
@@ -14,8 +14,8 @@
|
||||
- **Доступ:** SSH под пользователем **admin** (не root): `ssh admin@192.168.1.200` с хоста 192.168.1.150 или из LAN. Для выполнения команд с правами root: `sudo ...`.
|
||||
|
||||
**Диски:**
|
||||
- **Корневой диск** (sda1): 35 GB, занято **~29 GB (87%)** — система, образы/кэш в пределах корня. **Критично:** мало свободного места; при росте логов или обновлениях возможны сбои. Следить за местом и логированием (см. TODO).
|
||||
- **Данные** (sdb1): 344 GB, смонтирован в **/mnt/data**, занято ~177 GB (55%). Здесь: библиотека Immich, БД PostgreSQL, Docker root, containerd, Ollama, данные deduper.
|
||||
- **Корневой диск** (sda1): 50 GB — система, образы/кэш в пределах корня. Логи Docker ограничены (см. ниже).
|
||||
- **Данные** (sdb1): 350 GB, смонтирован в **/mnt/data**. Здесь: библиотека Immich, БД PostgreSQL, Docker root, containerd, Ollama, данные deduper.
|
||||
|
||||
---
|
||||
|
||||
@@ -184,10 +184,10 @@ sudo resize2fs /dev/sdb1
|
||||
## Логи и ротация
|
||||
|
||||
- **Базовая политика (как в LXC):** на ВМ настроен logrotate `/etc/logrotate.d/homelab-lxc.conf` — 14 дней, 50 MB, 5 архивов, сжатие (системные логи в `/var/log`). На ВМ 200 пакет `logrotate` был установлен вручную (в образе по умолчанию не было); после установки активен таймер `logrotate.timer`. Подробнее: [Logrotate — базовая политика homelab](../maintenance/logrotate/README.md).
|
||||
- **Docker:** данные Docker (образы, контейнеры, overlay) хранятся в **/mnt/data/docker** (Docker Root Dir). Логи контейнеров — драйвер **json-file** без ограничения размера и количества файлов (Config:{} у immich_server и immich_postgres). При активной работе логи могут разрастаться и занимать место на **корневом** разделе (если логи пишутся на корень) или в overlay на /mnt/data — уточнить расположение логов контейнеров (часто в /mnt/data/docker/containers). В любом случае ограничение логов не задано (см. TODO).
|
||||
- **Docker:** данные Docker (образы, контейнеры, overlay) хранятся в **/mnt/data/docker** (Docker Root Dir). Логи контейнеров — драйвер **json-file** с ограничениями в `/etc/docker/daemon.json`: `max-size: "10m"`, `max-file: "3"` (до 30 MB на контейнер). Логи пишутся в `/mnt/data/docker/containers`.
|
||||
- **Системный logrotate:** стандартные правила (apt, dpkg, cloud-init, unattended-upgrades, wtmp) плюс homelab-lxc.conf. Отдельных правил для Immich или Docker нет.
|
||||
|
||||
**Риск:** корневой диск заполнен на 87%. Рост логов, обновления и кэш могут привести к нехватке места. Необходимо ограничить логи Docker и следить за местом на корне (см. TODO).
|
||||
Корневой диск расширен до 50 GB; логи Docker ограничены.
|
||||
|
||||
---
|
||||
|
||||
@@ -208,18 +208,17 @@ sudo resize2fs /dev/sdb1
|
||||
## Уязвимости и риски
|
||||
|
||||
1. **Секреты в .env:** В `/opt/immich/.env` и `/opt/immich-deduper/.env` хранятся пароли БД, API-ключи (IMMICH_API_KEY, GEMINI_API_KEY), креды для deduper (PSQL_*). Файлы не должны попадать в публичный репозиторий. Ограничить права (chmod 600), хранить бэкапы в защищённом месте.
|
||||
2. **Корневой диск 87%:** Критично мало свободного места. При 100% возможны сбои обновлений и работы сервисов. Срочно: освободить место и/или перенести часть данных на /mnt/data, ограничить логи Docker (см. TODO).
|
||||
3. **Логи Docker без лимитов:** Ротация не настроена — возможен рост логов и заполнение диска.
|
||||
4. **Доступ по портам:** Сервисы доступны по 2283, 2284, 8001, 8501, 8086 из LAN. Снаружи доступ только через NPM (https://immich.katykhin.ru → 2283). Не пробрасывать порты напрямую в интернет.
|
||||
5. **GPU и PCI-passthrough:** ВМ использует hostpci0 (VGA). Убедиться, что драйверы NVIDIA и доступ к GPU корректны для immich_machine_learning.
|
||||
2. **Корневой диск:** Расширен до 50 GB; логи Docker ограничены (10m × 3 файла на контейнер). Следить за местом при обновлениях.
|
||||
3. **Доступ по портам:** Сервисы доступны по 2283, 2284, 8001, 8501, 8086 из LAN. Снаружи доступ только через NPM (https://immich.katykhin.ru → 2283). Не пробрасывать порты напрямую в интернет.
|
||||
4. **GPU и PCI-passthrough:** ВМ использует hostpci0 (VGA). Убедиться, что драйверы NVIDIA и доступ к GPU корректны для immich_machine_learning.
|
||||
|
||||
---
|
||||
|
||||
## TODO по ВМ 200
|
||||
|
||||
- [x] **Базовая политика logrotate:** для системных логов настроена (homelab-lxc.conf — 14 дней, 50 MB, 5 архивов, как в LXC). См. [Logrotate — базовая политика homelab](../maintenance/logrotate/README.md).
|
||||
- [ ] **Корневой диск:** Снизить использование корня (87%). Варианты: перенести логи Docker на /mnt/data (если сейчас пишутся на корень), очистить старые образы/кэш (`docker system prune` с осторожностью), увеличить размер корневого диска ВМ в Proxmox. Настроить мониторинг и оповещение при заполнении >90%.
|
||||
- [ ] **Логи Docker:** Включить ограничение размера логов для всех контейнеров Immich и deduper: в `docker-compose.yml` добавить для каждого сервиса `logging: driver: json-file options: max-size: "100m" max-file: "3"` или задать default в `/etc/docker/daemon.json`. Убедиться, что Docker Root Dir остаётся на /mnt/data и логи не пишутся на корень. После изменений перезапустить контейнеры.
|
||||
- [x] **Корневой диск:** Расширен до 50 GB (было 35 GB). Логи Docker ограничены.
|
||||
- [x] **Логи Docker:** В `/etc/docker/daemon.json` заданы `log-driver: json-file`, `max-size: "10m"`, `max-file: "3"`. Логи в /mnt/data/docker/containers.
|
||||
- [ ] **Права на конфиги:** Ограничить доступ к .env (chmod 600), не коммитить в публичные репозитории.
|
||||
- [ ] **Резервное копирование:** Регулярный бэкап критичных данных (оценка размеров на момент документации):
|
||||
- **`/mnt/data/library`** — библиотека Immich (фото, видео, превью). ~148 GB. Основной объём; бэкап обязателен (внешний диск, сетевое хранилище).
|
||||
|
||||
157
docs/containers/host-proxmox.md
Normal file
157
docs/containers/host-proxmox.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# Хост Proxmox (192.168.1.150)
|
||||
|
||||
Описание хоста Proxmox VE: скрипты, systemd-сервисы, пути и демоны. Контейнеры и ВМ описаны в отдельных статьях (container-100.md и т.д.).
|
||||
|
||||
---
|
||||
|
||||
## Общие сведения
|
||||
|
||||
- **IP:** 192.168.1.150/24
|
||||
- **Доступ:** `ssh root@192.168.1.150`
|
||||
- **Роль:** гипервизор (LXC + KVM), точка запуска бэкапов, деплой секретов в контейнеры
|
||||
|
||||
---
|
||||
|
||||
## Диски
|
||||
|
||||
| Устройство | Тип | Размер | Использование |
|
||||
|--------------|------|--------|----------------------------------|
|
||||
| /dev/nvme0n1 | NVMe | 256 GB | LVM (система, local-lvm) |
|
||||
| /dev/sda | HDD | 2 TB | ZFS |
|
||||
| /dev/sdb | SSD | 2 TB | ext4, /mnt/backup |
|
||||
| /dev/sdc | HDD | 2 TB | ZFS (RAID1 с sda) |
|
||||
| /dev/sdd | HDD | 8 TB | ext4 (внешний) |
|
||||
|
||||
---
|
||||
|
||||
## Каталог скриптов: /root/scripts/
|
||||
|
||||
Скрипты копируются из репозитория в `/root/scripts/` на хосте.
|
||||
|
||||
### Бэкапы (backup-*)
|
||||
|
||||
| Скрипт | Таймер | Назначение |
|
||||
|--------|--------|------------|
|
||||
| backup-vps-miran.sh | 01:00 | VPS Миран: БД бота, voice_users, S3 |
|
||||
| backup-ct101-pgdump.sh | 01:15 | Nextcloud PostgreSQL |
|
||||
| backup-immich-photos.sh | 01:30 | rsync фото Immich с VM 200 |
|
||||
| backup-vps-mtproto.sh | 01:45 | Конфиги MTProto (VPS DE) |
|
||||
| backup-etc-pve.sh | 02:15 | /etc/pve, interfaces, hosts, resolv.conf |
|
||||
| backup-ct104-pgdump.sh | 02:30 | Paperless PostgreSQL |
|
||||
| backup-vaultwarden-data.sh | 02:45 | Данные Vaultwarden |
|
||||
| backup-ct103-gitea-pgdump.sh | 03:00 | Gitea PostgreSQL |
|
||||
| backup-vm200-pgdump.sh | 03:15 | Immich PostgreSQL (SSH на VM 200) |
|
||||
| backup-ct105-vectors.sh | 03:30 | Векторы RAG (vectors.npz) |
|
||||
| backup-restic-yandex.sh | 04:00 | restic → Yandex (без photos) |
|
||||
| backup-restic-yandex-photos.sh | 04:10 | restic → Yandex (только photos) |
|
||||
|
||||
### Дашборд мониторинга
|
||||
|
||||
| Компонент | Назначение |
|
||||
|-----------|------------|
|
||||
| homelab-dashboard.service | Дашборд homelab (хост, контейнеры, сервисы) на порту 19998 |
|
||||
| /root/scripts/dashboard/ | Скрипты: dashboard-exporter.py, dashboard-server.py, index.html |
|
||||
| deploy-dashboard.sh | Деплой дашборда на хост |
|
||||
| add-to-homepage.sh | Добавить ссылку в Homepage (CT 103) |
|
||||
|
||||
**URL:** http://192.168.1.150:19998
|
||||
|
||||
### Мониторинг и уведомления
|
||||
|
||||
| Скрипт | Назначение |
|
||||
|--------|------------|
|
||||
| healthcheck-ping.sh | Ping Healthchecks (04:35) — Dead man's switch |
|
||||
| watchdog-timers.sh | Проверка .ok файлов (12:00), алерт в Telegram при отсутствии |
|
||||
| smartd-notify.sh | Вызывается smartd при проблемах с дисками |
|
||||
| notify-telegram.sh | Общий шлюз уведомлений в Telegram |
|
||||
| notify-vzdump-success.sh | Уведомление после успешного vzdump (по systemd path) |
|
||||
|
||||
### Деплой (deploy-*)
|
||||
|
||||
Скрипты разворачивают секреты из Vaultwarden в контейнеры и на VPS:
|
||||
|
||||
| Скрипт | Куда |
|
||||
|--------|------|
|
||||
| deploy-beget-credentials.sh | CT 100 (certbot) |
|
||||
| deploy-nextcloud-credentials.sh | CT 101 |
|
||||
| deploy-gitea-credentials.sh | CT 103 |
|
||||
| deploy-paperless-credentials.sh | CT 104 |
|
||||
| deploy-rag-credentials.sh | CT 105 |
|
||||
| deploy-invidious-credentials.sh | CT 107 |
|
||||
| deploy-galene-credentials.sh | CT 108 |
|
||||
| deploy-wireguard-credentials.sh | CT 109 |
|
||||
| deploy-immich-credentials.sh | VM 200 |
|
||||
| deploy-vpn-route-check.sh | CT 100 (vpn-route-check) |
|
||||
|
||||
### Прочее
|
||||
|
||||
| Скрипт | Назначение |
|
||||
|--------|------------|
|
||||
| immich-pgdump-remote.sh | Копируется на VM 200, вызывается backup-vm200-pgdump.sh по SSH |
|
||||
| restore-one-vzdump-from-restic.sh | Восстановление одного vzdump из Yandex |
|
||||
| verify-restore-level1.sh, verify-vzdump-level2.sh | Проверка восстановления |
|
||||
| backup-setup-sdb1-mount.sh | Монтирование /dev/sdb1 в /mnt/backup |
|
||||
| setup-vps-miran-backup-on-proxmox.sh | Настройка бэкапа VPS Миран на хосте |
|
||||
| npm-add-proxy.sh, npm-add-proxy-vault.sh, npm-cert-cloud.sh | Вспомогательные для NPM |
|
||||
|
||||
---
|
||||
|
||||
## Systemd: таймеры и сервисы
|
||||
|
||||
Unit-файлы лежат в репозитории в `scripts/systemd/`, на хосте — в `/etc/systemd/system/`.
|
||||
|
||||
### Бэкапы (backup-*.timer)
|
||||
|
||||
Все бэкапы запускаются через systemd timers (cron не используется). Расписание: [backup-howto.md](../backup/backup-howto.md).
|
||||
|
||||
После успешного выполнения сервис создаёт файл `/var/run/backup-<name>.ok` с timestamp.
|
||||
|
||||
### Watchdog (backup-watchdog-timers.timer)
|
||||
|
||||
Ежедневно в **12:00** запускается `watchdog-timers.sh`: проверяет, что все `.ok` файлы свежие (не старше 24 ч). При отсутствии или устаревании — уведомление в Telegram.
|
||||
|
||||
### Healthcheck ping (backup-healthcheck-ping.timer)
|
||||
|
||||
Ежедневно в **04:35** — ping в Healthchecks (Dead man's switch). Если бэкапы не прошли и ping не отправился, Healthchecks шлёт алерт.
|
||||
|
||||
### Vzdump (notify-vzdump-success)
|
||||
|
||||
Задание vzdump настраивается в Proxmox UI. После успешного выполнения срабатывает path-юнит `notify-vzdump-success.service` → уведомление в Telegram.
|
||||
|
||||
### Проверка восстановления (verify-*)
|
||||
|
||||
Таймеры для периодической проверки restic и vzdump: `verify-restore-level1-*`, `verify-vzdump-level2`.
|
||||
|
||||
---
|
||||
|
||||
## Ключевые пути
|
||||
|
||||
| Путь | Назначение |
|
||||
|------|------------|
|
||||
| /mnt/backup/ | Локальные бэкапы (см. [backup-howto](../backup/backup-howto.md)) |
|
||||
| /var/run/backup-*.ok | Healthcheck-файлы для watchdog (timestamp последнего успешного запуска) |
|
||||
| /root/.healthchecks.env | URL и UUID для healthcheck-ping |
|
||||
| /root/.bw-master | Мастер-пароль Bitwarden CLI (chmod 600; для restic, pg_dump) |
|
||||
| /root/.restic-yandex.env | Переменные restic (репозиторий, ключи) |
|
||||
| /etc/smartd.conf | Конфигурация smartd |
|
||||
| /etc/pve/ | Конфиги Proxmox (бэкапятся в backup-etc-pve) |
|
||||
|
||||
---
|
||||
|
||||
## Демоны и сервисы
|
||||
|
||||
| Сервис | Назначение |
|
||||
|--------|------------|
|
||||
| smartd | Мониторинг SMART дисков, при проблемах — smartd-notify.sh → Telegram |
|
||||
| pveproxy, pvedaemon | Proxmox API и веб-интерфейс |
|
||||
| corosync, pve-cluster | Кластер Proxmox (при одномузловой установке — локально) |
|
||||
|
||||
---
|
||||
|
||||
## Связанные документы
|
||||
|
||||
- [Архитектура](../architecture/architecture.md) — обзор, контейнеры, поток запросов
|
||||
- [Бэкапы](../backup/backup-howto.md) — что, куда, когда, восстановление
|
||||
- [Схема сети](../network/network-topology.md) — топология, зависимости
|
||||
- [smartd](../monitoring/smartd-setup.md) — мониторинг дисков
|
||||
- [Healthchecks](../vps/healthchecks-miran-setup.md) — Dead man's switch на VPS Миран
|
||||
146
docs/monitoring/dashboard-plan.md
Normal file
146
docs/monitoring/dashboard-plan.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# План реализации дашборда мониторинга homelab
|
||||
|
||||
Дашборд для Netdata (http://192.168.1.150:19999) с блоками: хост, контейнеры, критические сервисы.
|
||||
|
||||
---
|
||||
|
||||
## Текущее состояние (по результатам проверки на сервере)
|
||||
|
||||
### Netdata
|
||||
- **Версия:** v2.9.0
|
||||
- **Режим:** локальный, Cloud отключён
|
||||
- **API:** http://localhost:19999/api/v1/ — доступен
|
||||
|
||||
### Доступные метрики
|
||||
|
||||
| Блок | Метрика | Chart / источник | Статус |
|
||||
|------|---------|------------------|--------|
|
||||
| **Хост** | CPU total | `system.cpu` (user, system, nice, iowait, …) | ✅ |
|
||||
| | RAM total | `system.ram` | ✅ |
|
||||
| | Disk usage | `disk_space./`, `disk_space./mnt/backup`, `disk_space./mnt/nextcloud-hdd`, `disk_space./tank`, … (в API: URL-encode слэши) | ✅ |
|
||||
| | iowait | `system.cpu` dimension `iowait` | ✅ |
|
||||
| | load | `system.load` (load1, load5, load15) | ✅ |
|
||||
| **Контейнеры** | CPU % | `cgroup_<name>.cpu_limit` (used) | ✅ |
|
||||
| | RAM % | `cgroup_<name>.mem_utilization` | ✅ |
|
||||
| | Disk % | скрипт `pct exec ID -- df -P /` | ✅ кастомный экспортер |
|
||||
| | OOM count | `/sys/fs/cgroup/lxc/ID/memory.events` (oom_kill) | ✅ кастомный экспортер |
|
||||
| **Сервисы** | Immich, Nextcloud, nginx, VPN | ссылки на charts Netdata | ✅ без response time/connections |
|
||||
|
||||
### Контейнеры в cgroups (по данным Netdata)
|
||||
- `nginx` (CT 100)
|
||||
- `nextcloud` (CT 101)
|
||||
- `gitea` (CT 103)
|
||||
- `paperless` (CT 104)
|
||||
- `rag-service` (CT 105)
|
||||
- `misc` (CT 107, Invidious)
|
||||
- `galene` (CT 108)
|
||||
- `local-vpn` (CT 109)
|
||||
- `qemu_immich` (VM 200)
|
||||
|
||||
---
|
||||
|
||||
## Решения (по ответам пользователя)
|
||||
|
||||
1. **Disk % по контейнерам** — в приоритете. I/O не нужен. Реализация: скрипт на хосте, `pct exec ID -- df -P /` для каждого LXC, VM 200 — отдельно (`qm guest exec` или аналог).
|
||||
2. **OOM** — `cgroup memory.events` (oom_kill) по каждому контейнеру. Путь: `/sys/fs/cgroup/lxc/ID/memory.events` (LXC), для VM — cgroup QEMU.
|
||||
3. **Response time / open connections** — отложено, не требуется.
|
||||
4. **Размещение** — на хосте (192.168.1.150).
|
||||
5. **Netdata Cloud** — не рассматривается.
|
||||
|
||||
---
|
||||
|
||||
## Варианты реализации дашборда
|
||||
|
||||
### Вариант A: Netdata Cloud — не используется (Cloud отключён)
|
||||
|
||||
### Вариант B: Кастомная HTML-страница (выбран)
|
||||
- Страница на хосте (192.168.1.150), которая:
|
||||
- запрашивает Netdata API (`/api/v1/data?chart=...`)
|
||||
- рендерит блоки: хост, таблица контейнеров, сервисы
|
||||
- Плюсы: полный контроль, работает без Cloud
|
||||
- Минусы: нужна разработка и хостинг страницы
|
||||
|
||||
### Вариант C: Доработка стандартного дашборда Netdata
|
||||
- `dashboard_info.js` — изменение порядка/группировки charts
|
||||
- Плюсы: используем встроенный UI
|
||||
- Минусы: ограниченная кастомизация, в v2 подход мог измениться
|
||||
|
||||
---
|
||||
|
||||
## Рекомендуемый план (поэтапно)
|
||||
|
||||
### Этап 1: Дашборд на базе Netdata API (Вариант B)
|
||||
Создать кастомную HTML-страницу с тремя блоками.
|
||||
|
||||
**Блок 1 — Хост**
|
||||
- CPU total: `system.cpu` (сумма user+system или 100-idle)
|
||||
- RAM total: `system.ram` (used, cached, free)
|
||||
- Disk usage: `disk_space./`, `disk_space./mnt/backup`, `disk_space./mnt/nextcloud-hdd`, `disk_space./tank` (avail/used %)
|
||||
- iowait: `system.cpu` dimension iowait
|
||||
- load: `system.load` (load15)
|
||||
|
||||
**Блок 2 — Контейнеры (таблица)**
|
||||
- Колонки: имя, CPU %, RAM %, Disk %, OOM count
|
||||
- CPU/RAM: `cgroup_<name>.cpu_limit`, `cgroup_<name>.mem_utilization` (Netdata API)
|
||||
- Disk %: кастомный API (скрипт `pct exec ID -- df -P /` + парсинг)
|
||||
- OOM: кастомный API (LXC: `/sys/fs/cgroup/lxc/ID/memory.events`, VM 200: `/sys/fs/cgroup/qemu.slice/200.scope/memory.events` → oom_kill)
|
||||
|
||||
**Блок 3 — Критические сервисы**
|
||||
- Immich, Nextcloud, nginx, VPN — ссылки на charts Netdata (cgroup_*, app.nginx_*)
|
||||
- Response time / open connections — не требуются
|
||||
|
||||
### Этап 2: Кастомный экспортер (скрипт + HTTP API)
|
||||
Скрипт на хосте, запускаемый по таймеру или по запросу:
|
||||
- **Disk %:** для каждого LXC (100–109) — `pct exec ID -- df -P /`; для VM 200 — `qm guest exec` или fallback (lvs/zfs)
|
||||
- **OOM:** чтение `oom_kill` из `/sys/fs/cgroup/lxc/ID/memory.events` (LXC), `/sys/fs/cgroup/qemu.slice/200.scope/memory.events` (VM 200)
|
||||
- Отдача JSON на HTTP (например, порт 19998 или через nginx на хосте)
|
||||
|
||||
### Этап 3: Размещение и интеграция
|
||||
- Дашборд: статика на хосте (nginx или python -m http.server), запросы к Netdata API (localhost:19999) и кастомному API
|
||||
- Добавить ссылку в Homepage (services.yaml)
|
||||
|
||||
---
|
||||
|
||||
## VM 200 (Immich) — RAM после увеличения
|
||||
|
||||
При увеличении RAM с 6 до 10 GB «потребление» может визуально «упасть» по нескольким причинам:
|
||||
|
||||
1. **Процент vs абсолютное значение** — 32% от 10 GB ≈ 3.2 GB. Та же нагрузка при 6 GB давала бы ~53%. Дашборд показывает RAM % (cgroup mem_utilization).
|
||||
2. **Сброс кэша** — при нехватке памяти гость держит кэш; после добавления RAM ядро может освободить кэш, и «used» уменьшается.
|
||||
3. **Balloon** — virtio-balloon мог забирать память при 6 GB; после увеличения лимита balloon отдаёт память гостю, но реальное использование приложений может остаться ~3 GB.
|
||||
|
||||
**Проверка:** `qm guest exec 200 -- free -h` (требует qemu-guest-agent в гостевой ОС) — смотреть `Mem: used` внутри гостя.
|
||||
|
||||
---
|
||||
|
||||
## Реализовано (2026-02-28)
|
||||
|
||||
- **URL дашборда:** http://192.168.1.150:19998
|
||||
- **Ссылка в Homepage:** добавлена (Сервисы → Homelab Dashboard)
|
||||
- **Скрипты:** `scripts/dashboard/` (exporter, server, index.html, deploy, add-to-homepage)
|
||||
- **Systemd:** `homelab-dashboard.service` (порт 19998)
|
||||
|
||||
---
|
||||
|
||||
## Маппинг CT/VM → cgroup name (Netdata)
|
||||
|
||||
| ID | Назначение | cgroup name |
|
||||
|----|------------|-------------|
|
||||
| 100 | nginx | cgroup_nginx |
|
||||
| 101 | nextcloud | cgroup_nextcloud |
|
||||
| 103 | gitea | cgroup_gitea |
|
||||
| 104 | paperless | cgroup_paperless |
|
||||
| 105 | rag-service | cgroup_rag-service |
|
||||
| 107 | misc (Invidious) | cgroup_misc |
|
||||
| 108 | galene | cgroup_galene |
|
||||
| 109 | local-vpn | cgroup_local-vpn |
|
||||
| 200 | immich (VM) | cgroup_qemu_immich |
|
||||
|
||||
---
|
||||
|
||||
## Связанные документы
|
||||
|
||||
- [netdata-proxmox-setup.md](netdata-proxmox-setup.md) — установка и алерты
|
||||
- [smartd-setup.md](smartd-setup.md) — SMART дисков
|
||||
- [container-100](../containers/container-100.md) — NPM, log-dashboard
|
||||
- [architecture](../architecture/architecture.md) — обзор контейнеров
|
||||
211
docs/monitoring/netdata-proxmox-setup.md
Normal file
211
docs/monitoring/netdata-proxmox-setup.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# Netdata на хосте Proxmox
|
||||
|
||||
Мониторинг CPU, RAM, дисков, load average, swap. Алерты в Telegram.
|
||||
|
||||
---
|
||||
|
||||
## Доступ
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| **URL** | http://192.168.1.150:19999 |
|
||||
| **Режим** | Локальный, анонимный (Cloud отключён) |
|
||||
|
||||
---
|
||||
|
||||
## Установка
|
||||
|
||||
На хосте Proxmox (root):
|
||||
|
||||
```bash
|
||||
# Официальный установщик
|
||||
wget -O /tmp/netdata-kickstart.sh https://get.netdata.cloud/kickstart.sh
|
||||
sh /tmp/netdata-kickstart.sh --stable-channel --disable-telemetry
|
||||
```
|
||||
|
||||
Или через пакетный менеджер (если доступен):
|
||||
|
||||
```bash
|
||||
apt update && apt install -y netdata
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Конфигурация Telegram
|
||||
|
||||
Редактировать: `/etc/netdata/health_alarm_notify.conf`
|
||||
|
||||
```bash
|
||||
cd /etc/netdata
|
||||
./edit-config health_alarm_notify.conf
|
||||
```
|
||||
|
||||
Добавить/изменить:
|
||||
|
||||
```
|
||||
SEND_TELEGRAM="YES"
|
||||
TELEGRAM_BOT_TOKEN="<токен из Vaultwarden: HOME_BOT_TOKEN>"
|
||||
DEFAULT_RECIPIENT_TELEGRAM="<chat_id из Vaultwarden: RESTIC.TELEGRAM_SELF_CHAT_ID>"
|
||||
```
|
||||
|
||||
Креды можно взять из Vaultwarden (объекты HOME_BOT_TOKEN, RESTIC) или из `/root/.telegram-notify.env`.
|
||||
|
||||
---
|
||||
|
||||
## Алерты (health.d)
|
||||
|
||||
Файлы в `/etc/netdata/health.d/`. Создать или переопределить:
|
||||
|
||||
### cpu.conf — CPU > 90% более 10 минут
|
||||
|
||||
```conf
|
||||
# Переопределение: CPU > 90% = warning
|
||||
template: cpu_usage
|
||||
on: system.cpu
|
||||
lookup: average -10m percentage of usage
|
||||
warn: $this > 90
|
||||
crit: $this > 95
|
||||
```
|
||||
|
||||
### ram.conf — RAM > 90%
|
||||
|
||||
```conf
|
||||
template: ram_usage
|
||||
on: system.ram
|
||||
lookup: average -10m percentage of used
|
||||
warn: $this > 90
|
||||
crit: $this > 95
|
||||
```
|
||||
|
||||
### load.conf — Load average > cores × 2
|
||||
|
||||
```conf
|
||||
# Load average: warn если load > 2 × число ядер
|
||||
# Число ядер: nproc или lscpu
|
||||
template: load_average
|
||||
on: system.load
|
||||
lookup: average -10m of load15
|
||||
# Порог задаётся вручную под хост (cores × 2). Пример для 8 ядер: 16
|
||||
warn: $this > 16
|
||||
crit: $this > 24
|
||||
```
|
||||
|
||||
**Важно:** заменить `16` и `24` на `cores × 2` и `cores × 3` для вашего хоста. Узнать ядра: `nproc`.
|
||||
|
||||
### swap.conf — Swap > 0 стабильно
|
||||
|
||||
```conf
|
||||
template: swap_usage
|
||||
on: system.swap
|
||||
lookup: average -10m percentage of used
|
||||
warn: $this > 0
|
||||
crit: $this > 10
|
||||
```
|
||||
|
||||
### disk.conf — Диск > 80% (avail < 20%)
|
||||
|
||||
Мониторить: `/` (root, NVMe), `/mnt/backup` (sdb), внешний диск (sdd). Netdata использует `percentage of avail` — warn при avail < 20% (т.е. used > 80%).
|
||||
|
||||
```conf
|
||||
# Шаблон для важных дисков: warn при avail < 20%, crit при avail < 10%
|
||||
template: disk_space_critical
|
||||
on: disk.space
|
||||
lookup: max -1m percentage of avail
|
||||
warn: $this < 20
|
||||
crit: $this < 10
|
||||
```
|
||||
|
||||
Или для конкретных путей (chart ID: disk_space._ с подчёркиваниями вместо слешей):
|
||||
|
||||
```conf
|
||||
# / (root, NVMe)
|
||||
alarm: disk_space_root
|
||||
on: disk_space._
|
||||
lookup: max -1m percentage of avail
|
||||
chart labels: mount_point=/
|
||||
warn: $this < 20
|
||||
crit: $this < 10
|
||||
|
||||
# /mnt/backup (sdb)
|
||||
alarm: disk_space_backup
|
||||
on: disk_space._mnt_backup
|
||||
lookup: max -1m percentage of avail
|
||||
warn: $this < 20
|
||||
crit: $this < 10
|
||||
```
|
||||
|
||||
Узнать точные chart ID: `curl -s "http://localhost:19999/api/v1/charts" | grep disk_space`
|
||||
|
||||
---
|
||||
|
||||
## SMART (smartmontools)
|
||||
|
||||
Для мониторинга SMART через Netdata:
|
||||
|
||||
```bash
|
||||
apt install -y smartmontools
|
||||
```
|
||||
|
||||
Плагин `smartd` в Netdata автоматически обнаруживает диски. Дополнительно см. [smartd-setup.md](smartd-setup.md).
|
||||
|
||||
---
|
||||
|
||||
## Применение изменений
|
||||
|
||||
```bash
|
||||
netdatacli reload-health
|
||||
# или
|
||||
systemctl restart netdata
|
||||
```
|
||||
|
||||
Тест алертов:
|
||||
|
||||
```bash
|
||||
sudo su -s /bin/bash netdata
|
||||
export NETDATA_ALARM_NOTIFY_DEBUG=1
|
||||
/usr/libexec/netdata/plugins.d/alarm-notify.sh test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Мониторинг контейнеров и VM
|
||||
|
||||
Netdata на хосте видит общие метрики (CPU, RAM, диск хоста). Для детального мониторинга каждого LXC/VM:
|
||||
|
||||
- **Вариант 1:** Netdata parent/child — установить агент в каждый CT/VM, связать с родителем.
|
||||
- **Вариант 2:** Один Netdata на хосте — мониторит хост и агрегирует по контейнерам через cgroups (если включено).
|
||||
|
||||
Для homelab обычно достаточно мониторинга хоста. При необходимости — см. [Netdata Parent-Child](https://learn.netdata.cloud/docs/agent/streaming).
|
||||
|
||||
---
|
||||
|
||||
## Отключение Netdata Cloud
|
||||
|
||||
Если не нужен trial/Cloud:
|
||||
|
||||
1. **Полное отключение:** удалить `/var/lib/netdata/cloud.d/` (токены, ключи) и создать заново только `cloud.conf`:
|
||||
```bash
|
||||
rm -rf /var/lib/netdata/cloud.d
|
||||
mkdir -p /var/lib/netdata/cloud.d
|
||||
echo -e "[global]\nenabled = no" > /var/lib/netdata/cloud.d/cloud.conf
|
||||
chown -R netdata:netdata /var/lib/netdata/cloud.d
|
||||
systemctl restart netdata
|
||||
```
|
||||
|
||||
2. **Локальный дашборд** — http://host:19999 (анонимный доступ, без Cloud). Не использовать app.netdata.cloud — иначе снова появится claim/Cloud.
|
||||
|
||||
---
|
||||
|
||||
## Дашборд homelab
|
||||
|
||||
Кастомный дашборд с метриками хоста, контейнеров и сервисов: **http://192.168.1.150:19998**
|
||||
|
||||
Ссылка добавлена в Homepage (Сервисы → Homelab Dashboard). Деплой: `scripts/dashboard/deploy-dashboard.sh`. Подробнее: [dashboard-plan.md](dashboard-plan.md).
|
||||
|
||||
---
|
||||
|
||||
## Связанные документы
|
||||
|
||||
- [dashboard-plan.md](dashboard-plan.md) — план и реализация кастомного дашборда
|
||||
- [smartd-setup.md](smartd-setup.md) — SMART и диски
|
||||
- [backup-howto](../backup/backup-howto.md) — бэкапы
|
||||
109
docs/monitoring/smartd-setup.md
Normal file
109
docs/monitoring/smartd-setup.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# SMART и smartd — мониторинг дисков
|
||||
|
||||
Настройка `smartd` для мониторинга дисков Proxmox: NVMe, HDD, SSD. При отклонениях — уведомление в Telegram через `notify-telegram.sh`.
|
||||
|
||||
---
|
||||
|
||||
## Диски (из контекста homelab)
|
||||
|
||||
| Устройство | Тип | Размер | Использование |
|
||||
|--------------|------|--------|----------------------------------|
|
||||
| /dev/nvme0n1 | NVMe | 256 GB | LVM (система, local-lvm) |
|
||||
| /dev/sda | HDD | 2 TB | ZFS |
|
||||
| /dev/sdb | SSD | 2 TB | ext4, /mnt/backup |
|
||||
| /dev/sdc | HDD | 2 TB | ZFS (RAID1 с sda) |
|
||||
| /dev/sdd | HDD | 8 TB | ext4 (внешний) |
|
||||
|
||||
---
|
||||
|
||||
## Установка
|
||||
|
||||
```bash
|
||||
apt install -y smartmontools
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Конфигурация smartd
|
||||
|
||||
Файл: `/etc/smartd.conf`. Текущая конфигурация на хосте:
|
||||
|
||||
```conf
|
||||
# NVMe (система)
|
||||
/dev/nvme0n1 -a -W 4,45,55 -m root -M exec /root/scripts/smartd-notify.sh
|
||||
|
||||
# HDD sda (ZFS)
|
||||
/dev/sda -a -d ata -R 5 -R 197 -R 198 -W 4,45,55 -m root -M exec /root/scripts/smartd-notify.sh
|
||||
|
||||
# SSD sdb (backup)
|
||||
/dev/sdb -a -d ata -R 5 -R 197 -R 198 -W 4,45,55 -m root -M exec /root/scripts/smartd-notify.sh
|
||||
|
||||
# HDD sdc (ZFS)
|
||||
/dev/sdc -a -d ata -R 5 -R 197 -R 198 -W 4,45,55 -m root -M exec /root/scripts/smartd-notify.sh
|
||||
|
||||
# HDD sdd (внешний, USB/SAT)
|
||||
/dev/sdd -a -d sat -R 5 -R 197 -R 198 -W 4,45,55 -m root -M exec /root/scripts/smartd-notify.sh
|
||||
```
|
||||
|
||||
**Параметры:**
|
||||
- `-a` — мониторить все атрибуты
|
||||
- `-d ata` / `-d sat` — тип устройства (ata для SATA, sat для USB/SAT)
|
||||
- `-R 5` — Reallocated_Sector_Ct
|
||||
- `-R 197` — Current_Pending_Sector
|
||||
- `-R 198` — Offline_Uncorrectable
|
||||
- `-W 4,45,55` — температура: delta 4°C, warn 45°C, crit 55°C
|
||||
- `-M exec` — выполнить скрипт при проблеме
|
||||
|
||||
---
|
||||
|
||||
## Скрипт уведомления в Telegram
|
||||
|
||||
Создать `/root/scripts/smartd-notify.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Вызывается smartd при обнаружении проблемы.
|
||||
# Аргументы: device, type (health/usage/fail), message
|
||||
# См. man smartd.conf (-M exec)
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
DEVICE="${1:-unknown}"
|
||||
# smartd передаёт полный вывод в stdin
|
||||
MSG=$(cat)
|
||||
SUMMARY="${2:-SMART problem}"
|
||||
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
"$NOTIFY_SCRIPT" "⚠️ SMART" "Диск $DEVICE: $SUMMARY
|
||||
|
||||
$MSG" || true
|
||||
fi
|
||||
|
||||
# Передать дальше в mail (если настроен)
|
||||
exit 0
|
||||
```
|
||||
|
||||
Сделать исполняемым: `chmod +x /root/scripts/smartd-notify.sh`
|
||||
|
||||
**Примечание:** smartd при `-M exec` передаёт в скрипт до 3 аргументов и stdin. Точный формат см. в `man smartd.conf` (раздел -M exec).
|
||||
|
||||
---
|
||||
|
||||
## Запуск smartd
|
||||
|
||||
```bash
|
||||
systemctl enable --now smartd
|
||||
systemctl status smartd
|
||||
```
|
||||
|
||||
Проверка вручную:
|
||||
|
||||
```bash
|
||||
smartctl -a /dev/sda
|
||||
smartctl -a /dev/nvme0n1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Интеграция с Netdata
|
||||
|
||||
Netdata имеет плагин smartmontools. После установки smartmontools и настройки smartd Netdata может отображать метрики SMART на дашборде. См. [netdata-proxmox-setup.md](netdata-proxmox-setup.md).
|
||||
@@ -132,7 +132,7 @@ flowchart TB
|
||||
│ VPS DE │ │ VPS US │ │ VPS Миран (СПБ) │
|
||||
│ 185.103.253.99 │ │ 147.45.124.117 │ │ 185.147.80.190 │
|
||||
│ AmneziaWG │ │ AmneziaWG │ │ coTURN (Galene), │
|
||||
│ (обход блок.) │ │ (обход блок.) │ │ боты, Prometheus │
|
||||
│ (обход блок.) │ │ (обход блок.) │ │ Healthchecks, боты │
|
||||
└──────────────────┘ └──────────────────┘ └──────────────────────┘
|
||||
```
|
||||
|
||||
@@ -160,7 +160,7 @@ flowchart TB
|
||||
| **VM 200** | 192.168.1.200 | Immich, PostgreSQL, Redis, ML, deduper, Power Tools, Public Share | immich.katykhin.ru, immich-pt.katykhin.ru, share.katykhin.ru |
|
||||
| **VPS DE** | 185.103.253.99 | AmneziaWG (обход блокировок) | Туннель с роутера (10.8.1.x) |
|
||||
| **VPS US** | 147.45.124.117 | AmneziaWG (второй выход) | Туннель с роутера |
|
||||
| **VPS Миран** | 185.147.80.190 | coTURN (STUN/TURN для Galene), боты, prod | call.katykhin.ru использует STUN/TURN |
|
||||
| **VPS Миран** | 185.147.80.190 | coTURN (STUN/TURN для Galene), Healthchecks, боты, prod | call.katykhin.ru (STUN/TURN), healthchecks.katykhin.ru |
|
||||
| **DNS** | Beget.com | Домен katykhin.ru, поддомены, API для DNS-01 | Все *.katykhin.ru |
|
||||
|
||||
---
|
||||
@@ -241,6 +241,7 @@ flowchart TB
|
||||
## Связь с другими документами
|
||||
|
||||
- [Архитектура и подключение](../architecture/architecture.md) — общее описание, таблица контейнеров, поток запросов.
|
||||
- [Хост Proxmox](../containers/host-proxmox.md) — скрипты, таймеры, пути на 192.168.1.150.
|
||||
- [Контейнер 100](../containers/container-100.md) — NPM, AdGuard, Homepage, порядок запуска.
|
||||
- [Контейнер 109](../containers/container-109.md) — WireGuard VPN (local-vpn), доступ к vault и LAN.
|
||||
- [Генерация .mobileconfig для WireGuard (On-Demand)](vpn-mobileconfig-wireguard.md) — как собрать профиль для iOS/macOS с автоматическим подключением вне дома.
|
||||
|
||||
112
docs/vps/healthchecks-miran-setup.md
Normal file
112
docs/vps/healthchecks-miran-setup.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# Healthchecks на VPS Миран
|
||||
|
||||
Self-hosted [Healthchecks.io](https://healthchecks.io/) на VPS 185.147.80.190 — Dead man's switch для homelab. Если Proxmox не отправляет ping после окна бэкапов, Healthchecks шлёт алерт в Telegram.
|
||||
|
||||
---
|
||||
|
||||
## Доступ
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| **URL** | https://healthchecks.katykhin.ru/healthchecks/ |
|
||||
| **Логин** | admin@katykhin.ru |
|
||||
| **Пароль** | в Vaultwarden (Healthchecks admin) |
|
||||
|
||||
Доступ настроен по домену. Telegram webhook требует валидный SSL — без домена с Let's Encrypt бот не отвечает на `/start`.
|
||||
|
||||
---
|
||||
|
||||
## Развёртывание (для переустановки)
|
||||
|
||||
### 1. Подготовка
|
||||
|
||||
```bash
|
||||
ssh -p 15722 deploy@185.147.80.190
|
||||
mkdir -p /home/prod/healthchecks
|
||||
cd /home/prod/healthchecks
|
||||
```
|
||||
|
||||
Скопировать из репозитория: `scripts/healthchecks-docker/docker-compose.yml`, `scripts/healthchecks-docker/.env.example` → `.env`
|
||||
|
||||
### 2. Конфигурация .env
|
||||
|
||||
```env
|
||||
SITE_ROOT=https://healthchecks.katykhin.ru/healthchecks
|
||||
SECRET_KEY=<openssl rand -hex 32>
|
||||
ALLOWED_HOSTS=healthchecks.katykhin.ru,185.147.80.190,localhost
|
||||
|
||||
DB_HOST=db
|
||||
DB_NAME=hc
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=<надёжный пароль>
|
||||
|
||||
TELEGRAM_TOKEN=<токен из Vaultwarden: HOME_BOT_TOKEN>
|
||||
TELEGRAM_BOT_NAME=<username бота из @BotFather, напр. Katykhinhomebot>
|
||||
|
||||
REGISTRATION_OPEN=False
|
||||
DEFAULT_FROM_EMAIL=healthchecks@katykhin.ru
|
||||
```
|
||||
|
||||
### 3. Запуск
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
docker-compose run web /opt/healthchecks/manage.py createsuperuser --email admin@katykhin.ru --password <password>
|
||||
docker-compose run web python /opt/healthchecks/manage.py settelegramwebhook
|
||||
```
|
||||
|
||||
### 4. Nginx
|
||||
|
||||
Отдельный server block для `healthchecks.katykhin.ru` с Let's Encrypt. Референс: `scripts/healthchecks-nginx-server.conf`. Proxy на 127.0.0.1:8000; нужны location для `/healthchecks/`, `/static/`, `/projects/`, `/accounts/`, `/integrations/`, `/ping/` и др. (Django редиректы без префикса).
|
||||
|
||||
### 5. DNS
|
||||
|
||||
A-запись: `healthchecks.katykhin.ru` → `185.147.80.190`. Сертификат: `certbot --nginx -d healthchecks.katykhin.ru`.
|
||||
|
||||
---
|
||||
|
||||
## Привязка Telegram к check
|
||||
|
||||
1. Войти в Healthchecks → **Integrations** → **Add Integration** → **Telegram**
|
||||
2. Писать **своему** боту (из TELEGRAM_TOKEN), не @HealthchecksBot
|
||||
3. В Telegram: `/start` боту → перейти по ссылке → **Connect** в веб-интерфейсе
|
||||
|
||||
Check **homelab-backups** (UUID: 9451b52b-89f5-4a6c-b922-247a775bbf45).
|
||||
|
||||
---
|
||||
|
||||
## Ping с Proxmox
|
||||
|
||||
Скрипт `/root/scripts/healthcheck-ping.sh`, таймер `backup-healthcheck-ping.timer` — 04:35 ежедневно.
|
||||
|
||||
Конфиг `/root/.healthchecks.env`:
|
||||
|
||||
```env
|
||||
HEALTHCHECKS_URL=https://healthchecks.katykhin.ru/healthchecks
|
||||
HEALTHCHECKS_HOMELAB_UUID=<uuid из Healthchecks>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Смена пароля без SMTP
|
||||
|
||||
Healthchecks требует SMTP для смены пароля через веб. Без SMTP — через Django:
|
||||
|
||||
```bash
|
||||
cd /home/prod/healthchecks
|
||||
docker-compose run web python /opt/healthchecks/manage.py shell -c "
|
||||
from django.contrib.auth import get_user_model
|
||||
User = get_user_model()
|
||||
u = User.objects.get(email='admin@katykhin.ru')
|
||||
u.set_password('NEW_PASSWORD')
|
||||
u.save()
|
||||
print('OK')
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Связанные документы
|
||||
|
||||
- [vps-miran-bots](vps-miran-bots.md) — VPS Миран, порты
|
||||
- [backup-howto](../backup/backup-howto.md) — бэкапы, расписание
|
||||
@@ -81,6 +81,9 @@ VPS в ЦОД Миран (Санкт-Петербург). Развёрнуты
|
||||
| 9100 | node-exporter | TCP |
|
||||
| 3000 | Grafana | TCP |
|
||||
| 3001 | Uptime Kuma | TCP |
|
||||
| 8000 | Healthchecks (внутр.) | TCP |
|
||||
|
||||
**Healthchecks** — self-hosted Dead man's switch для homelab. Развёртывание: [healthchecks-miran-setup.md](healthchecks-miran-setup.md). Доступ через nginx (healthchecks.katykhin.ru).
|
||||
|
||||
---
|
||||
|
||||
@@ -110,7 +113,7 @@ docker compose logs -f telegram-bot
|
||||
|
||||
## Бэкап VPS (telegram-helper-bot)
|
||||
|
||||
Бэкап выполняется **с хоста Proxmox** скриптом `backup-vps-miran.sh` (cron 01:00). Копируются:
|
||||
Бэкап выполняется **с хоста Proxmox** скриптом `backup-vps-miran.sh` (systemd timer 01:00). Копируются:
|
||||
|
||||
1. **БД:** `/home/prod/bots/telegram-helper-bot/database/tg-bot-database.db` → `/mnt/backup/vps/miran/db/` (с датой в имени, хранение 14 дней).
|
||||
2. **Голосовые сообщения:** `/home/prod/bots/telegram-helper-bot/voice_users/` → `/mnt/backup/vps/miran/voice_users/` (rsync).
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
# Запускать на хосте Proxmox под root. Секреты из Vaultwarden (объект RESTIC), файл /root/.bw-master (chmod 600).
|
||||
# Cron: 10 4 * * * (04:10, после основного restic в 04:00).
|
||||
set -e
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||||
export HOME="${HOME:-/root}"
|
||||
|
||||
BACKUP_PATH="/mnt/backup/photos"
|
||||
# Время запуска (для логов и уведомлений)
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
# Перед первым запуском: установить restic, bw (Bitwarden CLI), jq; bw config server https://vault.katykhin.ru; restic init.
|
||||
# Cron: 0 4 * * * (04:00, после окна 01:00–03:30; 05:00 зарезервировано под перезагрузку).
|
||||
set -e
|
||||
# При запуске из systemd PATH и HOME могут быть пустыми
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||||
export HOME="${HOME:-/root}"
|
||||
|
||||
BACKUP_PATH="/mnt/backup"
|
||||
# Время запуска (для логов и уведомлений)
|
||||
|
||||
28
scripts/dashboard/add-to-homepage.sh
Normal file
28
scripts/dashboard/add-to-homepage.sh
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
# Добавить Homelab Dashboard в Homepage (services.yaml на CT 103)
|
||||
# Запуск: с хоста Proxmox — pct exec 103 -- bash -s < /root/scripts/dashboard/add-to-homepage.sh
|
||||
|
||||
set -e
|
||||
|
||||
SERVICES_YAML="${SERVICES_YAML:-/opt/docker/homepage/config/services.yaml}"
|
||||
|
||||
if [ ! -f "$SERVICES_YAML" ]; then
|
||||
echo "ERROR: $SERVICES_YAML not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -q "Homelab Dashboard" "$SERVICES_YAML" 2>/dev/null; then
|
||||
echo "Homelab Dashboard already in services.yaml"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Вставить после блока Netdata (ping: http://192.168.1.150:19999)
|
||||
sed -i '/ping: http:\/\/192.168.1.150:19999$/a\
|
||||
- Homelab Dashboard:\
|
||||
icon: mdi-chart-box\
|
||||
href: http://192.168.1.150:19998\
|
||||
description: Мониторинг хоста, контейнеров, сервисов\
|
||||
target: _blank
|
||||
' "$SERVICES_YAML"
|
||||
|
||||
echo "Added Homelab Dashboard to services.yaml"
|
||||
112
scripts/dashboard/dashboard-exporter.py
Normal file
112
scripts/dashboard/dashboard-exporter.py
Normal file
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Экспортер метрик для дашборда homelab: disk % и OOM по контейнерам/VM.
|
||||
Запуск: python3 dashboard-exporter.py (выводит JSON в stdout)
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Маппинг: (vmid, type) -> (name, cgroup_name для Netdata)
|
||||
CONTAINERS = [
|
||||
(100, "lxc", "nginx", "cgroup_nginx"),
|
||||
(101, "lxc", "nextcloud", "cgroup_nextcloud"),
|
||||
(103, "lxc", "gitea", "cgroup_gitea"),
|
||||
(104, "lxc", "paperless", "cgroup_paperless"),
|
||||
(105, "lxc", "rag-service", "cgroup_rag-service"),
|
||||
(107, "lxc", "misc", "cgroup_misc"),
|
||||
(108, "lxc", "galene", "cgroup_galene"),
|
||||
(109, "lxc", "local-vpn", "cgroup_local-vpn"),
|
||||
(200, "qemu", "immich", "cgroup_qemu_immich"),
|
||||
]
|
||||
|
||||
LXC_CGROUP = Path("/sys/fs/cgroup/lxc")
|
||||
QEMU_CGROUP_200 = Path("/sys/fs/cgroup/qemu.slice/200.scope")
|
||||
|
||||
|
||||
def get_disk_pct_lxc(vmid: int) -> float | None:
|
||||
"""Disk % для LXC через pct exec df."""
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["pct", "exec", str(vmid), "--", "df", "-P", "/"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
return None
|
||||
lines = r.stdout.strip().split("\n")
|
||||
if len(lines) < 2:
|
||||
return None
|
||||
# Формат: Filesystem 1K-blocks Used Available Use% Mounted
|
||||
parts = lines[-1].split()
|
||||
if len(parts) >= 5:
|
||||
use_pct = parts[4].rstrip("%")
|
||||
return float(use_pct)
|
||||
except (subprocess.TimeoutExpired, ValueError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def get_disk_pct_vm200() -> float | None:
|
||||
"""Disk % для VM 200 через lvs (fallback, т.к. qm guest exec часто недоступен)."""
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["lvs", "-o", "data_percent", "--noheadings", "pve/vm-200-disk-0"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
return None
|
||||
val = r.stdout.strip()
|
||||
if val:
|
||||
return float(val)
|
||||
except (subprocess.TimeoutExpired, ValueError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def get_oom_count(vmid: int, vmtype: str) -> int | None:
|
||||
"""OOM count из cgroup memory.events."""
|
||||
if vmtype == "lxc":
|
||||
path = LXC_CGROUP / str(vmid) / "memory.events"
|
||||
elif vmtype == "qemu" and vmid == 200:
|
||||
path = QEMU_CGROUP_200 / "memory.events"
|
||||
else:
|
||||
return None
|
||||
if not path.exists():
|
||||
return None
|
||||
try:
|
||||
text = path.read_text()
|
||||
for line in text.splitlines():
|
||||
if line.startswith("oom_kill "):
|
||||
return int(line.split()[1])
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def main() -> None:
|
||||
result = {"containers": [], "ok": True}
|
||||
for vmid, vmtype, name, cgroup_name in CONTAINERS:
|
||||
disk_pct = None
|
||||
if vmtype == "lxc":
|
||||
disk_pct = get_disk_pct_lxc(vmid)
|
||||
elif vmtype == "qemu" and vmid == 200:
|
||||
disk_pct = get_disk_pct_vm200()
|
||||
oom = get_oom_count(vmid, vmtype)
|
||||
result["containers"].append({
|
||||
"vmid": vmid,
|
||||
"name": name,
|
||||
"cgroup_name": cgroup_name,
|
||||
"disk_pct": disk_pct,
|
||||
"oom_count": oom,
|
||||
})
|
||||
print(json.dumps(result, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
104
scripts/dashboard/dashboard-server.py
Normal file
104
scripts/dashboard/dashboard-server.py
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
HTTP-сервер дашборда homelab: статика, /api/containers, прокси к Netdata.
|
||||
Порт: 19998 (по умолчанию).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from pathlib import Path
|
||||
|
||||
PORT = int(os.environ.get("DASHBOARD_PORT", "19998"))
|
||||
NETDATA_URL = os.environ.get("NETDATA_URL", "http://127.0.0.1:19999")
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
EXPORTER = SCRIPT_DIR / "dashboard-exporter.py"
|
||||
|
||||
|
||||
class DashboardHandler(BaseHTTPRequestHandler):
|
||||
def log_message(self, format, *args):
|
||||
pass # подавить вывод в консоль
|
||||
|
||||
def send_json(self, data: dict, status: int = 200):
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(data, ensure_ascii=False).encode("utf-8"))
|
||||
|
||||
def send_html(self, html: bytes, status: int = 200):
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Cache-Control", "no-cache")
|
||||
self.end_headers()
|
||||
self.wfile.write(html)
|
||||
|
||||
def do_GET(self):
|
||||
path = self.path.split("?")[0].rstrip("/") or "/"
|
||||
if path == "/":
|
||||
self.serve_index()
|
||||
elif path == "/api/containers":
|
||||
self.serve_containers()
|
||||
elif path.startswith("/api/netdata"):
|
||||
self.proxy_netdata()
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
def serve_index(self):
|
||||
html_file = SCRIPT_DIR / "index.html"
|
||||
if html_file.exists():
|
||||
self.send_html(html_file.read_bytes())
|
||||
else:
|
||||
self.send_error(404, "index.html not found")
|
||||
|
||||
def serve_containers(self):
|
||||
try:
|
||||
r = subprocess.run(
|
||||
[sys.executable, str(EXPORTER)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
cwd=str(SCRIPT_DIR),
|
||||
)
|
||||
if r.returncode != 0:
|
||||
self.send_json({"ok": False, "error": r.stderr or "exporter failed"}, 500)
|
||||
return
|
||||
data = json.loads(r.stdout)
|
||||
self.send_json(data)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.send_json({"ok": False, "error": "timeout"}, 504)
|
||||
except json.JSONDecodeError as e:
|
||||
self.send_json({"ok": False, "error": str(e)}, 500)
|
||||
except Exception as e:
|
||||
self.send_json({"ok": False, "error": str(e)}, 500)
|
||||
|
||||
def proxy_netdata(self):
|
||||
qs = self.path.split("?", 1)[1] if "?" in self.path else ""
|
||||
url = f"{NETDATA_URL}/api/v1/data?{qs}" if qs else f"{NETDATA_URL}/api/v1/data"
|
||||
try:
|
||||
req = urllib.request.Request(url)
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
data = resp.read()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", resp.headers.get("Content-Type", "application/json"))
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
except Exception as e:
|
||||
self.send_json({"error": str(e)}, 502)
|
||||
|
||||
|
||||
def main():
|
||||
server = HTTPServer(("0.0.0.0", PORT), DashboardHandler)
|
||||
print(f"Dashboard server on http://0.0.0.0:{PORT}", file=sys.stderr)
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
35
scripts/dashboard/deploy-dashboard.sh
Normal file
35
scripts/dashboard/deploy-dashboard.sh
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
# Деплой дашборда homelab на хост Proxmox
|
||||
# Запуск: с хоста Proxmox или ssh root@192.168.1.150 'bash -s' < scripts/dashboard/deploy-dashboard.sh
|
||||
# Или из репозитория: ./scripts/dashboard/deploy-dashboard.sh (копирует из текущей директории)
|
||||
|
||||
set -e
|
||||
|
||||
# REPO_ROOT: корень репозитория (содержит scripts/dashboard/)
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="${REPO_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
||||
DASHBOARD_SRC="${REPO_ROOT}/scripts/dashboard"
|
||||
DEST="/root/scripts/dashboard"
|
||||
SYSTEMD_DEST="/etc/systemd/system"
|
||||
|
||||
log() { echo "[$(date -Iseconds)] $*"; }
|
||||
|
||||
log "Deploying homelab dashboard..."
|
||||
|
||||
mkdir -p "$DEST"
|
||||
if [ "$(realpath "$DASHBOARD_SRC")" != "$(realpath "$DEST")" ]; then
|
||||
cp -v "${DASHBOARD_SRC}/dashboard-exporter.py" "$DEST/"
|
||||
cp -v "${DASHBOARD_SRC}/dashboard-server.py" "$DEST/"
|
||||
cp -v "${DASHBOARD_SRC}/index.html" "$DEST/"
|
||||
fi
|
||||
chmod +x "${DEST}/dashboard-exporter.py" "${DEST}/dashboard-server.py"
|
||||
|
||||
if [ -f "${REPO_ROOT}/scripts/systemd/homelab-dashboard.service" ]; then
|
||||
cp -v "${REPO_ROOT}/scripts/systemd/homelab-dashboard.service" "$SYSTEMD_DEST/"
|
||||
fi
|
||||
systemctl daemon-reload
|
||||
systemctl enable homelab-dashboard.service
|
||||
systemctl restart homelab-dashboard.service
|
||||
|
||||
log "Dashboard deployed. URL: http://192.168.1.150:19998"
|
||||
log "Status: $(systemctl is-active homelab-dashboard.service)"
|
||||
210
scripts/dashboard/index.html
Normal file
210
scripts/dashboard/index.html
Normal file
@@ -0,0 +1,210 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Homelab Dashboard</title>
|
||||
<style>
|
||||
:root { --bg: #0d1117; --card: #161b22; --text: #e6edf3; --muted: #8b949e; --accent: #58a6ff; --ok: #3fb950; --warn: #d29922; --err: #f85149; }
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 1rem; line-height: 1.5; }
|
||||
h1 { font-size: 1.25rem; margin: 0 0 1rem; }
|
||||
h2 { font-size: 1rem; margin: 0 0 0.5rem; color: var(--muted); font-weight: 500; }
|
||||
.card { background: var(--card); border-radius: 8px; padding: 1rem; margin-bottom: 1rem; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 0.75rem; }
|
||||
.metric { text-align: center; }
|
||||
.metric-value { font-size: 1.5rem; font-weight: 600; }
|
||||
.metric-label { font-size: 0.75rem; color: var(--muted); }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { padding: 0.5rem; text-align: left; border-bottom: 1px solid #30363d; }
|
||||
th { color: var(--muted); font-weight: 500; font-size: 0.85rem; }
|
||||
.pct-ok { color: var(--ok); }
|
||||
.pct-warn { color: var(--warn); }
|
||||
.pct-err { color: var(--err); }
|
||||
.links { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem; }
|
||||
.links a { color: var(--accent); text-decoration: none; font-size: 0.9rem; }
|
||||
.links a:hover { text-decoration: underline; }
|
||||
.loading { color: var(--muted); }
|
||||
.error { color: var(--err); }
|
||||
.updated { font-size: 0.75rem; color: var(--muted); margin-top: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Homelab Dashboard</h1>
|
||||
|
||||
<div class="card">
|
||||
<h2>Блок 1 — Хост</h2>
|
||||
<div class="grid" id="host-metrics">
|
||||
<div class="metric"><span class="metric-value loading">—</span><span class="metric-label">CPU %</span></div>
|
||||
<div class="metric"><span class="metric-value loading">—</span><span class="metric-label">RAM</span></div>
|
||||
<div class="metric"><span class="metric-value loading">—</span><span class="metric-label">Load</span></div>
|
||||
<div class="metric"><span class="metric-value loading">—</span><span class="metric-label">iowait %</span></div>
|
||||
<div class="metric"><span class="metric-value loading">—</span><span class="metric-label">Disk /</span></div>
|
||||
<div class="metric"><span class="metric-value loading">—</span><span class="metric-label">Disk backup</span></div>
|
||||
<div class="metric"><span class="metric-value loading">—</span><span class="metric-label">Disk nextcloud-hdd</span></div>
|
||||
<div class="metric"><span class="metric-value loading">—</span><span class="metric-label">Disk tank</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Блок 2 — Контейнеры</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Контейнер</th><th>CPU %</th><th>RAM %</th><th>Disk %</th><th>OOM</th></tr>
|
||||
</thead>
|
||||
<tbody id="containers-table"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Блок 3 — Критические сервисы</h2>
|
||||
<div class="links">
|
||||
<a href="http://192.168.1.150:19999/#menu_system_submenu_cpu;netdata" target="_blank">Netdata (CPU)</a>
|
||||
<a href="http://192.168.1.150:19999/#menu_cgroup_nginx;netdata" target="_blank">nginx (CT 100)</a>
|
||||
<a href="http://192.168.1.150:19999/#menu_cgroup_nextcloud;netdata" target="_blank">Nextcloud (CT 101)</a>
|
||||
<a href="http://192.168.1.150:19999/#menu_cgroup_qemu_immich;netdata" target="_blank">Immich (VM 200)</a>
|
||||
<a href="http://192.168.1.150:19999/#menu_cgroup_local-vpn;netdata" target="_blank">VPN (CT 109)</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="updated" id="updated"></div>
|
||||
<div id="status" class="updated" style="color:var(--muted)"></div>
|
||||
|
||||
<script>
|
||||
const API = window.location.origin; // явно использовать текущий origin
|
||||
|
||||
function pctClass(v) {
|
||||
if (v == null) return '';
|
||||
if (v >= 90) return 'pct-err';
|
||||
if (v >= 75) return 'pct-warn';
|
||||
return 'pct-ok';
|
||||
}
|
||||
|
||||
function fmt(v, suffix = '') {
|
||||
if (v == null || v === undefined) return '—';
|
||||
if (typeof v === 'number') return v.toFixed(1) + suffix;
|
||||
return String(v) + suffix;
|
||||
}
|
||||
|
||||
async function fetchNetdata(chart, points = 1) {
|
||||
const url = `${API}/api/netdata?chart=${encodeURIComponent(chart)}&points=${points}&format=json`;
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) throw new Error(`${chart}: ${r.status}`);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
async function loadHost() {
|
||||
try {
|
||||
const results = await Promise.allSettled([
|
||||
fetchNetdata('system.cpu'),
|
||||
fetchNetdata('system.ram'),
|
||||
fetchNetdata('system.load'),
|
||||
fetchNetdata('disk_space./'),
|
||||
fetchNetdata('disk_space./mnt/backup'),
|
||||
fetchNetdata('disk_space./mnt/nextcloud-hdd'),
|
||||
fetchNetdata('disk_space./tank'),
|
||||
]);
|
||||
const [cpu, ram, load, diskRoot, diskBackup, diskNextcloud, diskTank] = results.map(r => r.status === 'fulfilled' ? r.value : null);
|
||||
const cpuData = cpu?.data?.[0];
|
||||
const ramData = ram?.data?.[0];
|
||||
const loadData = load?.data?.[0];
|
||||
const li = cpu?.labels || [];
|
||||
const cpuTotal = cpuData ? (li.indexOf('user') >= 0 ? (cpuData[li.indexOf('user')] || 0) + (cpuData[li.indexOf('system')] || 0) + (cpuData[li.indexOf('nice')] || 0) + (cpuData[li.indexOf('iowait')] || 0) + (cpuData[li.indexOf('irq')] || 0) + (cpuData[li.indexOf('softirq')] || 0) + (cpuData[li.indexOf('steal')] || 0) + (cpuData[li.indexOf('guest')] || 0) + (cpuData[li.indexOf('guest_nice')] || 0) : 0) : null;
|
||||
const iowait = cpuData && li.indexOf('iowait') >= 0 ? cpuData[li.indexOf('iowait')] : null;
|
||||
const ramUsed = ramData && ram?.labels ? ramData[ram.labels.indexOf('used')] : null;
|
||||
const load15 = loadData && load?.labels ? loadData[load.labels.indexOf('load15')] : null;
|
||||
// disk_space возвращает avail/used в GiB, считаем %: used/(used+avail)*100
|
||||
const diskPct = (d) => {
|
||||
if (!d?.data?.[0] || !d?.labels) return null;
|
||||
const idxU = d.labels.indexOf('used'), idxA = d.labels.indexOf('avail');
|
||||
if (idxU < 0 || idxA < 0) return null;
|
||||
const used = d.data[0][idxU], avail = d.data[0][idxA];
|
||||
const total = used + avail;
|
||||
return total > 0 ? (used / total * 100) : null;
|
||||
};
|
||||
const diskRootUsed = diskPct(diskRoot);
|
||||
const diskBackupUsed = diskPct(diskBackup);
|
||||
const diskNextcloudUsed = diskPct(diskNextcloud);
|
||||
const diskTankUsed = diskPct(diskTank);
|
||||
|
||||
document.getElementById('host-metrics').innerHTML = `
|
||||
<div class="metric"><span class="metric-value">${fmt(cpuTotal, '%')}</span><span class="metric-label">CPU %</span></div>
|
||||
<div class="metric"><span class="metric-value">${fmt(ramUsed, ' MiB')}</span><span class="metric-label">RAM used</span></div>
|
||||
<div class="metric"><span class="metric-value">${fmt(load15)}</span><span class="metric-label">Load 15</span></div>
|
||||
<div class="metric"><span class="metric-value">${fmt(iowait, '%')}</span><span class="metric-label">iowait %</span></div>
|
||||
<div class="metric"><span class="metric-value ${pctClass(diskRootUsed)}">${fmt(diskRootUsed, '%')}</span><span class="metric-label">Disk /</span></div>
|
||||
<div class="metric"><span class="metric-value ${pctClass(diskBackupUsed)}">${fmt(diskBackupUsed, '%')}</span><span class="metric-label">Disk backup</span></div>
|
||||
<div class="metric"><span class="metric-value ${pctClass(diskNextcloudUsed)}">${fmt(diskNextcloudUsed, '%')}</span><span class="metric-label">Disk nextcloud-hdd</span></div>
|
||||
<div class="metric"><span class="metric-value ${pctClass(diskTankUsed)}">${fmt(diskTankUsed, '%')}</span><span class="metric-label">Disk tank</span></div>
|
||||
`;
|
||||
} catch (e) {
|
||||
document.getElementById('host-metrics').innerHTML = `<div class="error">Ошибка: ${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
const CGROUP_CHARTS = {
|
||||
'cgroup_nginx': { cpu: 'cgroup_nginx.cpu_limit', mem: 'cgroup_nginx.mem_utilization' },
|
||||
'cgroup_nextcloud': { cpu: 'cgroup_nextcloud.cpu_limit', mem: 'cgroup_nextcloud.mem_utilization' },
|
||||
'cgroup_gitea': { cpu: 'cgroup_gitea.cpu_limit', mem: 'cgroup_gitea.mem_utilization' },
|
||||
'cgroup_paperless': { cpu: 'cgroup_paperless.cpu_limit', mem: 'cgroup_paperless.mem_utilization' },
|
||||
'cgroup_rag-service': { cpu: 'cgroup_rag-service.cpu_limit', mem: 'cgroup_rag-service.mem_utilization' },
|
||||
'cgroup_misc': { cpu: 'cgroup_misc.cpu_limit', mem: 'cgroup_misc.mem_utilization' },
|
||||
'cgroup_galene': { cpu: 'cgroup_galene.cpu_limit', mem: 'cgroup_galene.mem_utilization' },
|
||||
'cgroup_local-vpn': { cpu: 'cgroup_local-vpn.cpu_limit', mem: 'cgroup_local-vpn.mem_utilization' },
|
||||
'cgroup_qemu_immich': { cpu: 'cgroup_qemu_immich.cpu_limit', mem: 'cgroup_qemu_immich.mem_utilization' },
|
||||
};
|
||||
|
||||
async function loadContainers() {
|
||||
try {
|
||||
const containersRes = await fetch(`${API}/api/containers`);
|
||||
if (!containersRes.ok) throw new Error(`API ${containersRes.status}`);
|
||||
const containersData = await containersRes.json();
|
||||
if (!containersData.ok || !containersData.containers) {
|
||||
document.getElementById('containers-table').innerHTML = `<tr><td colspan="5" class="error">Ошибка загрузки</td></tr>`;
|
||||
return;
|
||||
}
|
||||
const containers = containersData.containers;
|
||||
const cpuPromises = containers.map(c => {
|
||||
const charts = CGROUP_CHARTS[c.cgroup_name];
|
||||
if (!charts) return [null, null];
|
||||
return Promise.all([
|
||||
fetchNetdata(charts.cpu).then(d => d.data?.[0]?.[d.labels?.indexOf('used') ?? 0] != null ? d.data[0][d.labels.indexOf('used')] * 100 : null),
|
||||
fetchNetdata(charts.mem).then(d => d.data?.[0]?.[d.labels?.indexOf('utilization') ?? 0] != null ? d.data[0][d.labels.indexOf('utilization')] : null),
|
||||
]);
|
||||
});
|
||||
const netdataRows = await Promise.all(cpuPromises);
|
||||
const rows = containers.map((c, i) => {
|
||||
const [cpuPct, ramPct] = netdataRows[i] || [null, null];
|
||||
return `<tr>
|
||||
<td>${c.name} (${c.vmid})</td>
|
||||
<td class="${pctClass(cpuPct)}">${fmt(cpuPct, '%')}</td>
|
||||
<td class="${pctClass(ramPct)}">${fmt(ramPct, '%')}</td>
|
||||
<td class="${pctClass(c.disk_pct)}">${fmt(c.disk_pct, '%')}</td>
|
||||
<td>${c.oom_count != null ? c.oom_count : '—'}</td>
|
||||
</tr>`;
|
||||
});
|
||||
document.getElementById('containers-table').innerHTML = rows.join('');
|
||||
} catch (e) {
|
||||
document.getElementById('containers-table').innerHTML = `<tr><td colspan="5" class="error">Ошибка: ${e.message}. Проверьте доступ к ${API}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
const statusEl = document.getElementById('status');
|
||||
statusEl.textContent = 'Загрузка...';
|
||||
try {
|
||||
await Promise.all([loadHost(), loadContainers()]);
|
||||
document.getElementById('updated').textContent = 'Обновлено: ' + new Date().toLocaleString('ru');
|
||||
statusEl.textContent = '';
|
||||
} catch (e) {
|
||||
document.getElementById('updated').textContent = '';
|
||||
statusEl.textContent = 'Ошибка: ' + e.message;
|
||||
statusEl.style.color = 'var(--err)';
|
||||
}
|
||||
}
|
||||
|
||||
refresh();
|
||||
setInterval(refresh, 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
20
scripts/healthcheck-ping.sh
Executable file
20
scripts/healthcheck-ping.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# Ping Healthchecks после успешного окна бэкапов (Dead man's switch).
|
||||
# Если ping не пришёл — Healthchecks шлёт алерт в Telegram.
|
||||
# Конфиг: /root/.healthchecks.env (HEALTHCHECKS_URL, HEALTHCHECKS_HOMELAB_UUID)
|
||||
|
||||
CONFIG="${HEALTHCHECKS_CONFIG:-/root/.healthchecks.env}"
|
||||
if [ -f "$CONFIG" ]; then
|
||||
set -a
|
||||
# shellcheck source=/dev/null
|
||||
source "$CONFIG"
|
||||
set +a
|
||||
fi
|
||||
|
||||
HC_URL="${HEALTHCHECKS_URL:-https://healthchecks.katykhin.ru}"
|
||||
HC_UUID="${HEALTHCHECKS_HOMELAB_UUID:-}"
|
||||
|
||||
[ -z "$HC_UUID" ] && exit 0
|
||||
|
||||
curl -fsS --retry 3 --max-time 10 "${HC_URL}/ping/${HC_UUID}" >/dev/null 2>&1 || true
|
||||
exit 0
|
||||
23
scripts/healthchecks-docker/.env.example
Normal file
23
scripts/healthchecks-docker/.env.example
Normal file
@@ -0,0 +1,23 @@
|
||||
# Healthchecks на VPS Миран
|
||||
# Копировать: cp .env.example .env
|
||||
|
||||
SITE_ROOT=https://healthchecks.katykhin.ru/healthchecks
|
||||
SECRET_KEY=CHANGE_ME_openssl_rand_hex_32
|
||||
ALLOWED_HOSTS=healthchecks.katykhin.ru,185.147.80.190,localhost
|
||||
|
||||
DB=postgres
|
||||
DB_HOST=db
|
||||
DB_NAME=hc
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=CHANGE_ME_secure_password
|
||||
|
||||
# Свой бот (не @HealthchecksBot!) — создать через @BotFather, username бота
|
||||
TELEGRAM_TOKEN=
|
||||
TELEGRAM_BOT_NAME=YourBotUsername
|
||||
|
||||
REGISTRATION_OPEN=False
|
||||
|
||||
EMAIL_HOST=
|
||||
EMAIL_HOST_USER=
|
||||
EMAIL_HOST_PASSWORD=
|
||||
DEFAULT_FROM_EMAIL=healthchecks@katykhin.ru
|
||||
31
scripts/healthchecks-docker/docker-compose.yml
Normal file
31
scripts/healthchecks-docker/docker-compose.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
# Healthchecks на VPS Миран
|
||||
# Копировать: cp -r scripts/healthchecks-docker /home/prod/healthchecks
|
||||
# cd /home/prod/healthchecks && cp .env.example .env && редактировать .env
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:16
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_DB=${DB_NAME:-hc}
|
||||
- POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
restart: unless-stopped
|
||||
|
||||
web:
|
||||
image: healthchecks/healthchecks:latest
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- DB_HOST=db
|
||||
- DB_NAME=${DB_NAME:-hc}
|
||||
- DB_USER=postgres
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
ports:
|
||||
- "127.0.0.1:8000:8000"
|
||||
depends_on:
|
||||
- db
|
||||
restart: unless-stopped
|
||||
25
scripts/healthchecks-nginx-server.conf
Normal file
25
scripts/healthchecks-nginx-server.conf
Normal file
@@ -0,0 +1,25 @@
|
||||
# Референс: server block для healthchecks.katykhin.ru (Let's Encrypt, Telegram webhook)
|
||||
# Вставить в nginx.conf после HTTP redirect server block
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name healthchecks.katykhin.ru;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/healthchecks.katykhin.ru/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/healthchecks.katykhin.ru/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
|
||||
location = / { return 302 /healthchecks/; }
|
||||
location /static/ { proxy_pass http://127.0.0.1:8000/static/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location /projects/ { proxy_pass http://127.0.0.1:8000/projects/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location /accounts/ { proxy_pass http://127.0.0.1:8000/accounts/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location /integrations/ { proxy_pass http://127.0.0.1:8000/integrations/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location /ping/ { proxy_pass http://127.0.0.1:8000/ping/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location /admin/ { proxy_pass http://127.0.0.1:8000/admin/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location /badge/ { proxy_pass http://127.0.0.1:8000/badge/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location /checks/ { proxy_pass http://127.0.0.1:8000/checks/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location /docs/ { proxy_pass http://127.0.0.1:8000/docs/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location /tv/ { proxy_pass http://127.0.0.1:8000/tv/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
location = /healthchecks/ { return 302 /healthchecks/accounts/login/; }
|
||||
location = /healthchecks { return 302 /healthchecks/accounts/login/; }
|
||||
location /healthchecks/ { proxy_pass http://127.0.0.1:8000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }
|
||||
}
|
||||
6
scripts/healthchecks.env.example
Normal file
6
scripts/healthchecks.env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
# Конфиг для healthcheck-ping.sh (Proxmox)
|
||||
# Копировать: cp healthchecks.env.example /root/.healthchecks.env
|
||||
# UUID — из веб-интерфейса Healthchecks после создания check "homelab-backups"
|
||||
|
||||
HEALTHCHECKS_URL=https://healthchecks.katykhin.ru
|
||||
HEALTHCHECKS_HOMELAB_UUID=
|
||||
30
scripts/smartd-notify.sh
Executable file
30
scripts/smartd-notify.sh
Executable file
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
# Вызывается smartd при обнаружении проблемы (-M exec).
|
||||
# Аргументы: $1 = device, $2 = type (1=health, 2=usage, 3=fail), $3 = message
|
||||
# См. man smartd.conf
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
DEVICE="${1:-unknown}"
|
||||
TYPE="${2:-}"
|
||||
MSG="${3:-}"
|
||||
# Дополнительный вывод smartd может быть в stdin
|
||||
EXTRA=$(cat 2>/dev/null || true)
|
||||
|
||||
case "$TYPE" in
|
||||
1) SUMMARY="Health check failed" ;;
|
||||
2) SUMMARY="Usage attribute warning" ;;
|
||||
3) SUMMARY="Usage attribute failure" ;;
|
||||
*) SUMMARY="SMART problem" ;;
|
||||
esac
|
||||
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
BODY="Диск $DEVICE: $SUMMARY"
|
||||
[ -n "$MSG" ] && BODY="${BODY}
|
||||
$MSG"
|
||||
[ -n "$EXTRA" ] && BODY="${BODY}
|
||||
|
||||
$EXTRA"
|
||||
"$NOTIFY_SCRIPT" "⚠️ SMART" "$BODY" || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
24
scripts/systemd/README.md
Normal file
24
scripts/systemd/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Systemd unit-файлы для бэкапов и мониторинга
|
||||
|
||||
Копировать на хост Proxmox в `/etc/systemd/system/`:
|
||||
|
||||
```bash
|
||||
cp *.service *.timer /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
```
|
||||
|
||||
Включить все таймеры:
|
||||
|
||||
```bash
|
||||
for t in backup-*.timer notify-vzdump-success.timer verify-*.timer backup-watchdog-timers.timer backup-healthcheck-ping.timer; do
|
||||
systemctl enable --now "$t" 2>/dev/null || true
|
||||
done
|
||||
```
|
||||
|
||||
Проверка:
|
||||
|
||||
```bash
|
||||
systemctl list-timers --all | grep backup
|
||||
```
|
||||
|
||||
Перед миграцией с cron — отключить задания в crontab (`crontab -e`).
|
||||
14
scripts/systemd/backup-ct101-pgdump.service
Normal file
14
scripts/systemd/backup-ct101-pgdump.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Бэкап БД Nextcloud (CT 101)
|
||||
|
||||
[Unit]
|
||||
Description=Backup Nextcloud PostgreSQL (CT 101)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-ct101-pgdump.sh && echo $(date -Iseconds) > /var/run/backup-ct101-pgdump.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-ct101-pgdump.timer
Normal file
9
scripts/systemd/backup-ct101-pgdump.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Backup Nextcloud DB daily at 01:15
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 01:15:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/backup-ct103-gitea-pgdump.service
Normal file
14
scripts/systemd/backup-ct103-gitea-pgdump.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Бэкап БД Gitea (CT 103)
|
||||
|
||||
[Unit]
|
||||
Description=Backup Gitea PostgreSQL (CT 103)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-ct103-gitea-pgdump.sh && echo $(date -Iseconds) > /var/run/backup-ct103-gitea-pgdump.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-ct103-gitea-pgdump.timer
Normal file
9
scripts/systemd/backup-ct103-gitea-pgdump.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Backup Gitea DB daily at 03:00
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 03:00:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/backup-ct104-pgdump.service
Normal file
14
scripts/systemd/backup-ct104-pgdump.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Бэкап БД Paperless (CT 104)
|
||||
|
||||
[Unit]
|
||||
Description=Backup Paperless PostgreSQL (CT 104)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-ct104-pgdump.sh && echo $(date -Iseconds) > /var/run/backup-ct104-pgdump.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-ct104-pgdump.timer
Normal file
9
scripts/systemd/backup-ct104-pgdump.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Backup Paperless DB daily at 02:30
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 02:30:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/backup-ct105-vectors.service
Normal file
14
scripts/systemd/backup-ct105-vectors.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Бэкап векторов RAG (CT 105)
|
||||
|
||||
[Unit]
|
||||
Description=Backup RAG vectors (CT 105)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-ct105-vectors.sh && echo $(date -Iseconds) > /var/run/backup-ct105-vectors.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-ct105-vectors.timer
Normal file
9
scripts/systemd/backup-ct105-vectors.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Backup RAG vectors daily at 03:30
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 03:30:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/backup-etc-pve.service
Normal file
14
scripts/systemd/backup-etc-pve.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Бэкап /etc/pve и конфигов хоста
|
||||
|
||||
[Unit]
|
||||
Description=Backup Proxmox host config (/etc/pve, interfaces, hosts)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-etc-pve.sh && echo $(date -Iseconds) > /var/run/backup-etc-pve.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-etc-pve.timer
Normal file
9
scripts/systemd/backup-etc-pve.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Backup etc-pve daily at 02:15
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 02:15:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/backup-healthcheck-ping.service
Normal file
14
scripts/systemd/backup-healthcheck-ping.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Ping Healthchecks после окна бэкапов (Dead man's switch)
|
||||
|
||||
[Unit]
|
||||
Description=Ping Healthchecks (homelab backups)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/root/scripts/healthcheck-ping.sh
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-healthcheck-ping.timer
Normal file
9
scripts/systemd/backup-healthcheck-ping.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Ping Healthchecks daily at 04:35 (after backup window)
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 04:35:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/backup-immich-photos.service
Normal file
14
scripts/systemd/backup-immich-photos.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Бэкап библиотеки фото Immich (rsync с VM 200)
|
||||
|
||||
[Unit]
|
||||
Description=Backup Immich photos (rsync from VM 200)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-immich-photos.sh && echo $(date -Iseconds) > /var/run/backup-immich-photos.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-immich-photos.timer
Normal file
9
scripts/systemd/backup-immich-photos.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Backup Immich photos daily at 01:30
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 01:30:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
16
scripts/systemd/backup-restic-yandex-photos.service
Normal file
16
scripts/systemd/backup-restic-yandex-photos.service
Normal file
@@ -0,0 +1,16 @@
|
||||
# Выгрузка /mnt/backup/photos в Yandex S3 через restic
|
||||
|
||||
[Unit]
|
||||
Description=Backup photos to Yandex S3 (restic)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Environment=HOME=/root
|
||||
Environment=PATH=/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-restic-yandex-photos.sh && echo $(date -Iseconds) > /var/run/backup-restic-yandex-photos.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-restic-yandex-photos.timer
Normal file
9
scripts/systemd/backup-restic-yandex-photos.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Restic backup photos to Yandex daily at 04:10
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 04:10:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
16
scripts/systemd/backup-restic-yandex.service
Normal file
16
scripts/systemd/backup-restic-yandex.service
Normal file
@@ -0,0 +1,16 @@
|
||||
# Выгрузка /mnt/backup (без photos) в Yandex S3 через restic
|
||||
|
||||
[Unit]
|
||||
Description=Backup to Yandex S3 (restic, main)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Environment=HOME=/root
|
||||
Environment=PATH=/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-restic-yandex.sh && echo $(date -Iseconds) > /var/run/backup-restic-yandex.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-restic-yandex.timer
Normal file
9
scripts/systemd/backup-restic-yandex.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Restic backup to Yandex daily at 04:00
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 04:00:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/backup-vaultwarden-data.service
Normal file
14
scripts/systemd/backup-vaultwarden-data.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Бэкап данных Vaultwarden (CT 103)
|
||||
|
||||
[Unit]
|
||||
Description=Backup Vaultwarden data (CT 103)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-vaultwarden-data.sh && echo $(date -Iseconds) > /var/run/backup-vaultwarden-data.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-vaultwarden-data.timer
Normal file
9
scripts/systemd/backup-vaultwarden-data.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Backup Vaultwarden daily at 02:45
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 02:45:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/backup-vm200-pgdump.service
Normal file
14
scripts/systemd/backup-vm200-pgdump.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Бэкап БД Immich (VM 200)
|
||||
|
||||
[Unit]
|
||||
Description=Backup Immich PostgreSQL (VM 200)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-vm200-pgdump.sh && echo $(date -Iseconds) > /var/run/backup-vm200-pgdump.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-vm200-pgdump.timer
Normal file
9
scripts/systemd/backup-vm200-pgdump.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Backup Immich DB daily at 03:15
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 03:15:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
17
scripts/systemd/backup-vps-miran.service
Normal file
17
scripts/systemd/backup-vps-miran.service
Normal file
@@ -0,0 +1,17 @@
|
||||
# Копировать на Proxmox: /etc/systemd/system/
|
||||
# systemctl daemon-reload && systemctl enable --now backup-vps-miran.timer
|
||||
# Удалить из cron: 0 1 * * *
|
||||
|
||||
[Unit]
|
||||
Description=Backup VPS Miran (БД бота, voice_users, S3)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
# Запись .ok только при успехе (для watchdog)
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-vps-miran.sh && echo $(date -Iseconds) > /var/run/backup-vps-miran.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-vps-miran.timer
Normal file
9
scripts/systemd/backup-vps-miran.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Backup VPS Miran daily at 01:00
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 01:00:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/backup-vps-mtproto.service
Normal file
14
scripts/systemd/backup-vps-mtproto.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Бэкап конфигов MTProto + сайт (VPS Германия)
|
||||
|
||||
[Unit]
|
||||
Description=Backup VPS MTProto (Germany)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/sh -c '/root/scripts/backup-vps-mtproto.sh && echo $(date -Iseconds) > /var/run/backup-vps-mtproto.ok'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-vps-mtproto.timer
Normal file
9
scripts/systemd/backup-vps-mtproto.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Backup VPS MTProto daily at 01:45
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 01:45:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/backup-watchdog-timers.service
Normal file
14
scripts/systemd/backup-watchdog-timers.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Watchdog: проверка failed timers и устаревших healthcheck-файлов
|
||||
|
||||
[Unit]
|
||||
Description=Backup watchdog (failed timers, stale .ok files)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/root/scripts/watchdog-timers.sh
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/backup-watchdog-timers.timer
Normal file
9
scripts/systemd/backup-watchdog-timers.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Backup watchdog daily at 12:00
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 12:00:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
19
scripts/systemd/homelab-dashboard.service
Normal file
19
scripts/systemd/homelab-dashboard.service
Normal file
@@ -0,0 +1,19 @@
|
||||
# Дашборд мониторинга homelab (хост, контейнеры, сервисы)
|
||||
# Порт 19998, статика + API + прокси к Netdata
|
||||
|
||||
[Unit]
|
||||
Description=Homelab Dashboard (monitoring)
|
||||
After=network-online.target netdata.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/python3 /root/scripts/dashboard/dashboard-server.py
|
||||
WorkingDirectory=/root/scripts/dashboard
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
15
scripts/systemd/notify-vzdump-success.service
Normal file
15
scripts/systemd/notify-vzdump-success.service
Normal file
@@ -0,0 +1,15 @@
|
||||
# Проверка локального vzdump за последние 2 ч, отправка сводки в Telegram
|
||||
# Задание vzdump в Proxmox UI выполняется в 02:00
|
||||
|
||||
[Unit]
|
||||
Description=Notify vzdump success (check dump dir, send Telegram)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/root/scripts/notify-vzdump-success.sh
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/notify-vzdump-success.timer
Normal file
9
scripts/systemd/notify-vzdump-success.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Notify vzdump success daily at 03:00
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 03:00:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/verify-restore-level1-full-check.service
Normal file
14
scripts/systemd/verify-restore-level1-full-check.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Restic check --read-data (раз в 6 мес: 1 янв и 1 июля)
|
||||
|
||||
[Unit]
|
||||
Description=Verify restic repository (full read-data, semiannual)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/root/scripts/verify-restore-level1.sh full-check
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
10
scripts/systemd/verify-restore-level1-full-check.timer
Normal file
10
scripts/systemd/verify-restore-level1-full-check.timer
Normal file
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Restic full check semiannual (Jan 1, Jul 1 at 10:00)
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-01-01 10:00:00
|
||||
OnCalendar=*-07-01 10:00:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/verify-restore-level1-monthly-check.service
Normal file
14
scripts/systemd/verify-restore-level1-monthly-check.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Restic check --read-data-subset=10% (ежемесячно, 1-е число)
|
||||
|
||||
[Unit]
|
||||
Description=Verify restic repository (monthly read-data-subset)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/root/scripts/verify-restore-level1.sh monthly-check
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Restic check read-data-subset monthly (1st at 10:00)
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-01 10:00:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/verify-restore-level1-monthly-dump.service
Normal file
14
scripts/systemd/verify-restore-level1-monthly-dump.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Тест restore дампа Nextcloud из restic (ежемесячно)
|
||||
|
||||
[Unit]
|
||||
Description=Verify Nextcloud dump restore from restic (monthly)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/root/scripts/verify-restore-level1.sh monthly-dump
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/verify-restore-level1-monthly-dump.timer
Normal file
9
scripts/systemd/verify-restore-level1-monthly-dump.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Verify Nextcloud dump restore monthly (1st at 11:00)
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-01 11:00:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/verify-restore-level1-weekly.service
Normal file
14
scripts/systemd/verify-restore-level1-weekly.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Restic check (еженедельно)
|
||||
|
||||
[Unit]
|
||||
Description=Verify restic repository (weekly check)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/root/scripts/verify-restore-level1.sh weekly
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/verify-restore-level1-weekly.timer
Normal file
9
scripts/systemd/verify-restore-level1-weekly.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Restic check weekly (Sunday 03:00)
|
||||
|
||||
[Timer]
|
||||
OnCalendar=Sun *-*-* 03:00:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
14
scripts/systemd/verify-vzdump-level2.service
Normal file
14
scripts/systemd/verify-vzdump-level2.service
Normal file
@@ -0,0 +1,14 @@
|
||||
# Автотест vzdump CT 107 (ежемесячно)
|
||||
|
||||
[Unit]
|
||||
Description=Verify vzdump restore (CT 107, monthly)
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/root/scripts/verify-vzdump-level2.sh
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
9
scripts/systemd/verify-vzdump-level2.timer
Normal file
9
scripts/systemd/verify-vzdump-level2.timer
Normal file
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Verify vzdump restore monthly (1st at 12:00)
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-01 12:00:00
|
||||
Persistent=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
147
scripts/verify-restore-level1.sh
Executable file
147
scripts/verify-restore-level1.sh
Executable file
@@ -0,0 +1,147 @@
|
||||
#!/bin/bash
|
||||
# Тест восстановления уровня 1: restic check и проверка дампа Nextcloud из restic.
|
||||
# Запускать на хосте Proxmox под root.
|
||||
# Режимы (аргумент): weekly | monthly-check | full-check | monthly-dump
|
||||
# weekly — restic check (еженедельно)
|
||||
# monthly-check — restic check --read-data-subset=10% (ежемесячно, 1-е число)
|
||||
# full-check — restic check --read-data (раз в 6–12 мес, 1 янв и 1 июля)
|
||||
# monthly-dump — restore дампа Nextcloud из restic, проверка целостности (ежемесячно)
|
||||
# Секреты: из Vaultwarden (объект RESTIC), как в backup-restic-yandex.sh.
|
||||
# Cron/Timer: отдельные таймеры для каждого режима.
|
||||
set -e
|
||||
|
||||
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||||
|
||||
MODE="${1:-weekly}"
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
RESTORE_TARGET="/tmp/restore-test"
|
||||
RESTIC_PATH_NEXTCLOUD="/mnt/backup/databases/ct101-nextcloud"
|
||||
MIN_DUMP_SIZE_MB=1
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Запускайте под root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Загрузка кредов restic из Vaultwarden (как в backup-restic-yandex.sh)
|
||||
setup_restic_env() {
|
||||
BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}"
|
||||
if [ ! -f "$BW_MASTER_PASSWORD_FILE" ]; then
|
||||
echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE"
|
||||
return 1
|
||||
fi
|
||||
if ! command -v bw >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then
|
||||
echo "Установите bw (Bitwarden CLI) и jq."
|
||||
return 1
|
||||
fi
|
||||
export BW_SESSION
|
||||
BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw 2>/dev/null) || return 1
|
||||
RESTIC_ITEM=$(bw get item "RESTIC" 2>/dev/null) || return 1
|
||||
export RESTIC_REPOSITORY
|
||||
RESTIC_REPOSITORY=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="RESTIC_REPOSITORY") | .value')
|
||||
export AWS_ACCESS_KEY_ID
|
||||
AWS_ACCESS_KEY_ID=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="AWS_ACCESS_KEY_ID") | .value')
|
||||
export AWS_SECRET_ACCESS_KEY
|
||||
AWS_SECRET_ACCESS_KEY=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="AWS_SECRET_ACCESS_KEY") | .value')
|
||||
export AWS_DEFAULT_REGION
|
||||
AWS_DEFAULT_REGION=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="AWS_DEFAULT_REGION") | .value')
|
||||
[ -z "$AWS_DEFAULT_REGION" ] && AWS_DEFAULT_REGION="ru-central1"
|
||||
RESTIC_PASS=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="RESTIC_BACKUP_KEY") | .value')
|
||||
for var in RESTIC_REPOSITORY AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY; do
|
||||
[ -z "${!var}" ] && return 1
|
||||
done
|
||||
RESTIC_PASSWORD_FILE=$(mktemp -u)
|
||||
echo -n "$RESTIC_PASS" > "$RESTIC_PASSWORD_FILE"
|
||||
chmod 600 "$RESTIC_PASSWORD_FILE"
|
||||
trap 'rm -f "$RESTIC_PASSWORD_FILE"' EXIT INT TERM
|
||||
export RESTIC_PASSWORD_FILE
|
||||
return 0
|
||||
}
|
||||
|
||||
notify_ok() {
|
||||
[ -x "$NOTIFY_SCRIPT" ] && "$NOTIFY_SCRIPT" "$1" "$2" || true
|
||||
}
|
||||
|
||||
notify_err() {
|
||||
[ -x "$NOTIFY_SCRIPT" ] && "$NOTIFY_SCRIPT" "⚠️ $1" "$2" || true
|
||||
}
|
||||
|
||||
case "$MODE" in
|
||||
weekly)
|
||||
echo "[verify-restore-level1] Режим: weekly (restic check)"
|
||||
setup_restic_env || { notify_err "Restic check" "Не удалось загрузить креды restic."; exit 1; }
|
||||
if restic check 2>&1; then
|
||||
echo "[verify-restore-level1] restic check OK"
|
||||
# При еженедельном успехе — не спамим (только при ошибке)
|
||||
else
|
||||
notify_err "Restic check" "Ошибка проверки репозитория restic."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
monthly-check)
|
||||
echo "[verify-restore-level1] Режим: monthly-check (restic check --read-data-subset=10%)"
|
||||
setup_restic_env || { notify_err "Restic check (read-data-subset)" "Не удалось загрузить креды restic."; exit 1; }
|
||||
if restic check --read-data-subset=10% 2>&1; then
|
||||
echo "[verify-restore-level1] restic check --read-data-subset=10% OK"
|
||||
notify_ok "Тест restic (read-data-subset)" "OK, 10% данных проверено."
|
||||
else
|
||||
notify_err "Restic check (read-data-subset)" "Ошибка проверки 10% данных репозитория."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
full-check)
|
||||
echo "[verify-restore-level1] Режим: full-check (restic check --read-data)"
|
||||
setup_restic_env || { notify_err "Restic check (read-data)" "Не удалось загрузить креды restic."; exit 1; }
|
||||
if restic check --read-data 2>&1; then
|
||||
echo "[verify-restore-level1] restic check --read-data OK"
|
||||
notify_ok "Тест restic (full read-data)" "OK, полная проверка данных завершена."
|
||||
else
|
||||
notify_err "Restic check (read-data)" "Ошибка полной проверки данных репозитория."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
monthly-dump)
|
||||
echo "[verify-restore-level1] Режим: monthly-dump (restore и проверка дампа Nextcloud)"
|
||||
setup_restic_env || { notify_err "Тест дампа Nextcloud" "Не удалось загрузить креды restic."; exit 1; }
|
||||
rm -rf "$RESTORE_TARGET"
|
||||
mkdir -p "$RESTORE_TARGET"
|
||||
trap 'rm -f "${RESTIC_PASSWORD_FILE:-}" 2>/dev/null; rm -rf "$RESTORE_TARGET"' EXIT INT TERM
|
||||
if ! restic restore latest --target "$RESTORE_TARGET" --path "$RESTIC_PATH_NEXTCLOUD" 2>&1; then
|
||||
notify_err "Тест дампа Nextcloud" "Ошибка restic restore: не удалось восстановить $RESTIC_PATH_NEXTCLOUD"
|
||||
exit 1
|
||||
fi
|
||||
# Путь после restore: RESTORE_TARGET/mnt/backup/databases/ct101-nextcloud/
|
||||
RESTORED_DIR="$RESTORE_TARGET/mnt/backup/databases/ct101-nextcloud"
|
||||
if [ ! -d "$RESTORED_DIR" ]; then
|
||||
notify_err "Тест дампа Nextcloud" "Ошибка: каталог $RESTORED_DIR не найден после restore."
|
||||
exit 1
|
||||
fi
|
||||
LATEST_SQL=$(ls -t "$RESTORED_DIR"/nextcloud-db-*.sql.gz 2>/dev/null | head -1)
|
||||
if [ -z "$LATEST_SQL" ] || [ ! -f "$LATEST_SQL" ]; then
|
||||
notify_err "Тест дампа Nextcloud" "Ошибка: не найден .sql.gz в $RESTORED_DIR"
|
||||
exit 1
|
||||
fi
|
||||
SIZE_BYTES=$(stat -c%s "$LATEST_SQL" 2>/dev/null || echo 0)
|
||||
SIZE_MB=$(( SIZE_BYTES / 1024 / 1024 ))
|
||||
if [ "$SIZE_MB" -lt "$MIN_DUMP_SIZE_MB" ]; then
|
||||
notify_err "Тест дампа Nextcloud" "Ошибка: размер дампа ${SIZE_MB} MB < ${MIN_DUMP_SIZE_MB} MB (файл: $LATEST_SQL)"
|
||||
exit 1
|
||||
fi
|
||||
if ! gunzip -t "$LATEST_SQL" 2>/dev/null; then
|
||||
notify_err "Тест дампа Nextcloud" "Ошибка: gunzip -t не прошёл для $LATEST_SQL"
|
||||
exit 1
|
||||
fi
|
||||
if ! gunzip -c "$LATEST_SQL" 2>/dev/null | grep -q 'CREATE TABLE'; then
|
||||
notify_err "Тест дампа Nextcloud" "Ошибка: в распакованном дампе нет CREATE TABLE (возможно не SQL дамп)"
|
||||
exit 1
|
||||
fi
|
||||
echo "[verify-restore-level1] Дамп Nextcloud OK: $LATEST_SQL, размер ${SIZE_MB} MB"
|
||||
notify_ok "Тест дампа Nextcloud" "OK, размер ${SIZE_MB} MB."
|
||||
;;
|
||||
*)
|
||||
echo "Использование: $0 {weekly|monthly-check|full-check|monthly-dump}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
88
scripts/verify-vzdump-level2.sh
Executable file
88
scripts/verify-vzdump-level2.sh
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/bin/bash
|
||||
# Тест восстановления уровня 2: автотест vzdump CT 107.
|
||||
# Восстанавливает последний vzdump-lxc-107 в временный CT 999, проверяет запуск, удаляет.
|
||||
# Запускать на хосте Proxmox под root. Ежемесячно (systemd timer).
|
||||
# При успехе/ошибке — уведомление в Telegram.
|
||||
set -e
|
||||
|
||||
DUMP_DIR="/mnt/backup/proxmox/dump/dump"
|
||||
TEST_VMID=999
|
||||
TEST_IP="192.168.1.199/24"
|
||||
TEST_GW="192.168.1.1"
|
||||
STORAGE="local-lvm"
|
||||
WAIT_START_SEC=60
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Запускайте под root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Очистка при выходе (успех или ошибка)
|
||||
cleanup() {
|
||||
if pct status "$TEST_VMID" &>/dev/null; then
|
||||
echo "[verify-vzdump] Останавливаем и удаляем CT $TEST_VMID..."
|
||||
pct stop "$TEST_VMID" --skiplock 2>/dev/null || true
|
||||
sleep 2
|
||||
pct destroy "$TEST_VMID" --purge 1 --force 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
notify_ok() {
|
||||
[ -x "$NOTIFY_SCRIPT" ] && "$NOTIFY_SCRIPT" "✅ Тест vzdump CT 107" "$1" || true
|
||||
}
|
||||
|
||||
notify_err() {
|
||||
[ -x "$NOTIFY_SCRIPT" ] && "$NOTIFY_SCRIPT" "⚠️ Тест vzdump CT 107" "Ошибка: $1" || true
|
||||
}
|
||||
|
||||
if [ ! -d "$DUMP_DIR" ]; then
|
||||
notify_err "Каталог $DUMP_DIR не найден."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Последний vzdump-lxc-107
|
||||
ARCHIVE=$(ls -t "$DUMP_DIR"/vzdump-lxc-107-*.tar.zst 2>/dev/null | head -1)
|
||||
if [ -z "$ARCHIVE" ] || [ ! -f "$ARCHIVE" ]; then
|
||||
notify_err "Не найден vzdump-lxc-107-*.tar.zst в $DUMP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[verify-vzdump] Архив: $ARCHIVE"
|
||||
|
||||
# Убедиться, что CT 999 не существует (остаток от прошлого запуска)
|
||||
if pct status "$TEST_VMID" &>/dev/null; then
|
||||
pct destroy "$TEST_VMID" --purge 1 --force 2>/dev/null || true
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
echo "[verify-vzdump] Создаём CT $TEST_VMID из архива..."
|
||||
if ! pct create "$TEST_VMID" "$ARCHIVE" --restore 1 --storage "$STORAGE" 2>&1; then
|
||||
notify_err "pct create не удался."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Другой IP, чтобы не конфликтовать с оригиналом 107
|
||||
echo "[verify-vzdump] Настраиваем сеть (IP $TEST_IP)..."
|
||||
pct set "$TEST_VMID" --net0 "name=eth0,bridge=vmbr0,gw=$TEST_GW,ip=$TEST_IP,type=veth"
|
||||
|
||||
echo "[verify-vzdump] Запускаем CT $TEST_VMID..."
|
||||
if ! pct start "$TEST_VMID" 2>&1; then
|
||||
notify_err "pct start не удался."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[verify-vzdump] Ожидание $WAIT_START_SEC сек..."
|
||||
sleep "$WAIT_START_SEC"
|
||||
|
||||
STATUS=$(pct exec "$TEST_VMID" -- systemctl is-system-running 2>/dev/null || echo "unknown")
|
||||
if [ "$STATUS" != "running" ]; then
|
||||
notify_err "systemctl is-system-running вернул: $STATUS (ожидалось running)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[verify-vzdump] CT 999 запущен, system running. Тест пройден."
|
||||
notify_ok "OK"
|
||||
|
||||
exit 0
|
||||
58
scripts/watchdog-timers.sh
Executable file
58
scripts/watchdog-timers.sh
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
# Watchdog: проверка провалившихся systemd timers.
|
||||
# Запускать раз в день (например 12:00). При наличии failed → notify в Telegram.
|
||||
# Timer: backup-watchdog-timers.timer
|
||||
|
||||
NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}"
|
||||
MAX_AGE_HOURS=24
|
||||
BACKUP_OK_DIR="/var/run"
|
||||
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "Запускайте под root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1. Проверка systemctl list-timers --failed
|
||||
FAILED=$(systemctl list-timers --failed --no-legend --no-pager 2>/dev/null | grep -v '^$' || true)
|
||||
if [ -n "$FAILED" ]; then
|
||||
MSG="Провалившиеся таймеры:
|
||||
$FAILED"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
"$NOTIFY_SCRIPT" "⚠️ Systemd timers" "$MSG" || true
|
||||
fi
|
||||
echo "[watchdog] Найдены провалившиеся таймеры"
|
||||
echo "$FAILED"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. Проверка healthcheck-файлов (если файл старше 24 ч — алерт)
|
||||
BACKUP_NAMES="vps-miran ct101-pgdump immich-photos vps-mtproto etc-pve ct104-pgdump vaultwarden-data ct103-gitea-pgdump vm200-pgdump ct105-vectors restic-yandex restic-yandex-photos"
|
||||
STALE=""
|
||||
for name in $BACKUP_NAMES; do
|
||||
OK_FILE="$BACKUP_OK_DIR/backup-$name.ok"
|
||||
if [ -f "$OK_FILE" ]; then
|
||||
AGE_SEC=$(( $(date +%s) - $(stat -c %Y "$OK_FILE" 2>/dev/null || echo 0) ))
|
||||
AGE_HOURS=$(( AGE_SEC / 3600 ))
|
||||
if [ "$AGE_HOURS" -ge "$MAX_AGE_HOURS" ]; then
|
||||
STALE="${STALE}backup-$name.ok (${AGE_HOURS}h)
|
||||
"
|
||||
fi
|
||||
else
|
||||
STALE="${STALE}backup-$name.ok (отсутствует)
|
||||
"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$STALE" ]; then
|
||||
MSG="Файлы .ok старше ${MAX_AGE_HOURS} ч или отсутствуют (последний успешный бэкап):
|
||||
$STALE"
|
||||
if [ -x "$NOTIFY_SCRIPT" ]; then
|
||||
"$NOTIFY_SCRIPT" "⚠️ Backup watchdog" "$MSG" || true
|
||||
fi
|
||||
echo "[watchdog] Устаревшие healthcheck-файлы"
|
||||
echo "$STALE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[watchdog] OK: таймеры и healthcheck-файлы в порядке"
|
||||
exit 0
|
||||
Reference in New Issue
Block a user