diff --git a/docs/backup/backup-howto.md b/docs/backup/backup-howto.md index 42181ae..76689d8 100644 --- a/docs/backup/backup-howto.md +++ b/docs/backup/backup-howto.md @@ -33,23 +33,24 @@ ## Что, откуда, куда, когда -| Что | Откуда | Куда (локально) | Когда | Хранение | -|-----|--------|------------------|------|----------| -| **LXC и VM** | Все выбранные контейнеры (100–109) и VM 200 | `/mnt/backup/proxmox/dump/` | **02:00** ежедневно (задание в Proxmox UI) | По настройкам задания (например: 7 daily, 4 weekly, 6 monthly) | -| **Конфиги хоста** | `/etc/pve`, `/etc/network/interfaces`, `/etc/hosts`, `/etc/resolv.conf` | `/mnt/backup/proxmox/etc-pve/` | **02:15** ежедневно (cron: `backup-etc-pve.sh`) | 30 дней | -| **БД Nextcloud (PostgreSQL)** | CT 101, контейнер `nextcloud-db-1` в `/opt/nextcloud` | `/mnt/backup/databases/ct101-nextcloud/` | **01:15** ежедневно (cron: `backup-ct101-pgdump.sh`) | 14 дней | -| **БД Gitea (PostgreSQL)** | CT 103, контейнер `gitea-db-1` в `/opt/gitea` | `/mnt/backup/databases/ct103-gitea/` | **03:00** ежедневно (cron: `backup-ct103-gitea-pgdump.sh`) | 14 дней | -| **БД Paperless (PostgreSQL)** | CT 104, контейнер `paperless-db-1` в `/opt/paperless` | `/mnt/backup/databases/ct104-paperless/` | **02:30** ежедневно (cron: `backup-ct104-pgdump.sh`) | 14 дней | -| **Данные Vaultwarden (пароли)** | CT 103, `/opt/docker/vaultwarden/data` | `/mnt/backup/other/vaultwarden/` | **02:45** ежедневно (cron: `backup-vaultwarden-data.sh`); каталог в restic → Yandex | 14 дней | -| **БД Immich (PostgreSQL)** | VM 200, контейнер `database` в `/opt/immich` | `/mnt/backup/databases/vm200-immich/` | **03:15** ежедневно (cron: `backup-vm200-pgdump.sh`) | 14 дней | -| **Векторы 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 дней | -| **Оригиналы фото Immich** | VM 200, `/mnt/data/library/` | `/mnt/backup/photos/library/` | **01:30** ежедневно (cron: `backup-immich-photos.sh`, rsync) | Все копии (без автоудаления) | -| **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 — перезапись | -| **Конфиги 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 дней | -| **Выгрузка в Yandex (restic)** | `/mnt/backup` без photos | Yandex Object Storage | **04:00** ежедневно (cron: `backup-restic-yandex.sh`) | 3 daily, 2 weekly, 2 monthly | -| **Выгрузка в Yandex (restic, фото)** | `/mnt/backup/photos` | Yandex Object Storage (тот же репо) | **01:00** ежедневно (cron: `backup-restic-yandex-photos.sh`) | 3 daily, 2 weekly, 2 monthly | -**Окно бэкапов:** внутренние копии (синк внутри сервера) — **01:00–03:30**; выгрузка в облако — **04:00**. **05:00** зарезервировано под плановую перезагрузку сервера. Задание vzdump — из веб-интерфейса Proxmox (Центр обработки данных → Резервная копия). +| Что | Откуда | Куда (локально) | Когда | Хранение | Уведомление | +|-----|--------|------------------|------|----------|--------------| +| **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) | + +**Окно бэкапов:** внутренние копии — **01:00–03:30**; выгрузка в облако — **04:00** (основной restic), **04:10** (restic фото). **05:00** зарезервировано под плановую перезагрузку сервера. Задание vzdump — из веб-интерфейса Proxmox (Центр обработки данных → Резервная копия). --- @@ -76,7 +77,6 @@ pct create 999 /mnt/backup/proxmox/dump/dump/vzdump-lxc-107-2026_02_26-02_03_14.tar.zst --restore 1 --storage local-lvm ``` Если восстанавливаем поверх существующего контейнера: сначала удалить его (`pct destroy 107`), затем в команде указать тот же VMID (107). Доп. опции: `pct create --help` (режим restore). - - **VM (KVM)** — порядок аргументов: сначала архив, потом VMID: ```bash qm restore /mnt/backup/proxmox/dump/dump/vzdump-qemu-200-YYYY_MM_DD-HH_MM_SS.vma.zst 200 --storage local-lvm @@ -86,7 +86,7 @@ **После восстановления (пример для LXC):** - Если восстановили в новый слот (например 999) и не нужен конфликт IP с оригиналом — сменить IP: - `pct set 999 --net0 name=eth0,bridge=vmbr0,gw=192.168.1.1,ip=192.168.1.199/24,type=veth` +`pct set 999 --net0 name=eth0,bridge=vmbr0,gw=192.168.1.1,ip=192.168.1.199/24,type=veth` - Запуск: `pct start 999` (LXC) или `qm start 200` (VM). - Проверка: пинг, консоль (`pct exec 999 -- bash`), при необходимости сервисы и порты внутри контейнера. @@ -99,17 +99,17 @@ **Когда нужно:** переустановка Proxmox или потеря конфигов узла (при этом диск с бэкапами доступен). Если конфигов нет локально, но есть в Yandex — см. раздел **Восстановление из restic** → «Восстановление конфигов хоста (/etc/pve)». -1. Скопировать нужный архив с хоста, например: - `etc-pve-YYYYMMDD-HHMM.tar.gz` и/или `etc-host-configs-YYYYMMDD-HHMM.tar.gz` из `/mnt/backup/proxmox/etc-pve/`. +1. Скопировать нужный архив с хоста, например: + `etc-pve-YYYYMMDD-HHMM.tar.gz` и/или `etc-host-configs-YYYYMMDD-HHMM.tar.gz` из `/mnt/backup/proxmox/etc-pve/`. 2. **Восстановление /etc/pve** (на переустановленном хосте, от root): - ```bash + ```bash tar -xzf etc-pve-YYYYMMDD-HHMM.tar.gz -C / - ``` + ``` При одномузловой установке обычно достаточно распаковать в `/`. При кластере — аккуратно с нодами и storage. 3. **Восстановление конфигов сети/хоста** (interfaces, hosts, resolv.conf): - ```bash + ```bash tar -xzf etc-host-configs-YYYYMMDD-HHMM.tar.gz -C / - ``` + ``` При необходимости поправить под текущее железо (интерфейсы, IP) и перезапустить сеть. После восстановления конфигов — заново добавить storage для бэкапов (если переустанавливали с нуля) и восстанавливать гостей из vzdump по шагу 1. @@ -120,17 +120,14 @@ **Когда нужно:** повреждение или потеря базы Immich при рабочей ВМ (образ VM можно не трогать, восстанавливаем только БД). -1. Скопировать нужный дамп на VM 200, например: - `immich-db-YYYYMMDD-HHMM.sql.gz` из `/mnt/backup/databases/vm200-immich/`. -2. На VM 200 (ssh admin@192.168.1.200): - ```bash +1. Скопировать нужный дамп на VM 200, например: + `immich-db-YYYYMMDD-HHMM.sql.gz` из `/mnt/backup/databases/vm200-immich/`. +2. На VM 200 (ssh [admin@192.168.1.200](mailto:admin@192.168.1.200)): + ```bash cd /opt/immich gunzip -c /path/to/immich-db-YYYYMMDD-HHMM.sql.gz | docker compose exec -T database psql -U -d - ``` + ``` Или распаковать `.sql.gz`, затем: - ```bash - docker compose exec -T database psql -U -d < backup.sql - ``` `` и `` — из `/opt/immich/.env` (обычно `postgres` и `immich`). Перед восстановлением лучше остановить приложение Immich (или как минимум не писать в БД). При полной пересоздании БД — очистить каталог данных PostgreSQL в контейнере и затем загрузить дамп. @@ -178,18 +175,20 @@ rsync -av /mnt/backup/photos/library/ admin@192.168.1.200:/mnt/data/library/ **Когда нужно:** потеря данных на VPS или перенос бота на другой хост. В бэкапе есть: + - **БД:** `/mnt/backup/vps/miran/db/tg-bot-database-YYYYMMDD.db` — копии SQLite. - **Голосовые сообщения:** `/mnt/backup/vps/miran/voice_users/` — каталог .ogg. - **S3 (контент бота):** `/mnt/backup/vps/miran/s3/` — полная копия бакета (photos, videos, voice и т.д.). **Восстановление на VPS:** -1. Скопировать выбранный файл БД на VPS: - `scp -P 15722 /mnt/backup/vps/miran/db/tg-bot-database-YYYYMMDD.db deploy@185.147.80.190:/home/prod/bots/telegram-helper-bot/database/tg-bot-database.db` -2. Восстановить `voice_users`: - `rsync -avz -e "ssh -p 15722" /mnt/backup/vps/miran/voice_users/ deploy@185.147.80.190:/home/prod/bots/telegram-helper-bot/voice_users/` + +1. Скопировать выбранный файл БД на VPS: + `scp -P 15722 /mnt/backup/vps/miran/db/tg-bot-database-YYYYMMDD.db deploy@185.147.80.190:/home/prod/bots/telegram-helper-bot/database/tg-bot-database.db` +2. Восстановить `voice_users`: + `rsync -avz -e "ssh -p 15722" /mnt/backup/vps/miran/voice_users/ deploy@185.147.80.190:/home/prod/bots/telegram-helper-bot/voice_users/` 3. При потере данных в S3 — загрузить из бэкапа в бакет Miran (через aws s3 sync или панель), используя endpoint `https://api.s3.miran.ru` и креды из [VPS Миран](vps-miran-bots.md). -**Требования для бэкапа:** на хосте Proxmox — SSH-ключ root → deploy@185.147.80.190 (порт 15722); для S3 — установленный `aws` cli и файл `/root/.vps-miran-s3.env` с переменными S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET_NAME (см. [VPS Миран](../vps/vps-miran-bots.md)). +**Требования для бэкапа:** на хосте Proxmox — SSH-ключ root → [deploy@185.147.80.190](mailto:deploy@185.147.80.190) (порт 15722); для S3 — установленный `aws` cli и файл `/root/.vps-miran-s3.env` с переменными S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET_NAME (см. [VPS Миран](../vps/vps-miran-bots.md)). --- @@ -198,19 +197,22 @@ rsync -av /mnt/backup/photos/library/ admin@192.168.1.200:/mnt/data/library/ **Когда нужно:** потеря конфигов на VPS 185.103.253.99 или перенос MTProto и сайта-заглушки на другой хост. В архиве `mtproto-config-YYYYMMDD-HHMM.tar.gz` из `/mnt/backup/vps/mtproto-germany/` лежат: + - **mtg:** `etc/systemd/system/mtg.service` (в т.ч. секрет и cloak-port). - **nginx:** `etc/nginx/sites-available/`, `etc/nginx/sites-enabled/` (конфиг для katykhin.store на порту 993). - **Let's Encrypt:** `etc/letsencrypt/live/katykhin.store/`, `archive/katykhin.store/`, `renewal/katykhin.store.conf`. - **Сайт:** `var/www/katykhin.store/`. **Восстановление на VPS (от root):** скопировать архив на сервер и распаковать в корень: + ```bash scp /mnt/backup/vps/mtproto-germany/mtproto-config-YYYYMMDD-HHMM.tar.gz root@185.103.253.99:/tmp/ ssh root@185.103.253.99 "tar -xzf /tmp/mtproto-config-YYYYMMDD-HHMM.tar.gz -C /" ``` + После распаковки: `systemctl daemon-reload && systemctl restart mtg nginx`. На новом хосте дополнительно установить mtg, nginx, certbot и настроить ufw (см. [план MTProto + сайт](../vps/vpn-vps-mtproto-site-plan.md)). -**Требования для бэкапа:** на хосте Proxmox — SSH по ключу root → root@185.103.253.99 (порт 22). Ключ хоста должен быть добавлен в `authorized_keys` на VPS. +**Требования для бэкапа:** на хосте Proxmox — SSH по ключу root → [root@185.103.253.99](mailto:root@185.103.253.99) (порт 22). Ключ хоста должен быть добавлен в `authorized_keys` на VPS. --- @@ -225,14 +227,16 @@ ssh root@185.103.253.99 "tar -xzf /tmp/mtproto-config-YYYYMMDD-HHMM.tar.gz -C /" VM 200 **не входит** в задание vzdump (образ ~380 ГБ, не помещается в политику 7 копий). В бэкапе есть: **конфиг ВМ** (в архивах `/etc/pve`), **БД** (pg_dump), **фото** (rsync в `photos/library`). Восстановление — создание новой ВМ с теми же параметрами и перенос данных. **Что есть после восстановления хоста:** + - Из бэкапа `etc-pve`: файл `/etc/pve/qemu-server/200.conf` — полное описание ВМ (CPU, память, диски, **hostpci для GPU**, сеть). Его можно использовать как образец при создании новой ВМ. - Дамп БД: `/mnt/backup/databases/vm200-immich/immich-db-*.sql.gz`. - Фото: `/mnt/backup/photos/library/`. **Ключевые параметры VM 200** (если восстанавливать вручную без конфига): + - **Ресурсы:** 3 ядра, 10 GB RAM. - **GPU:** проброс видеокарты (hostpci) — в Proxmox: Hardware → Add → PCI Device → выбрать VGA/NVIDIA, поставить «All Functions» и «ROM-Bar» при необходимости. В конфиге это выглядит как `hostpci0: 0000:xx:00.0` и т.п. -- **Диски:** первый — системный (~35 GB), второй — данные (~350 GB) под `/mnt/data` (библиотека, PostgreSQL, Docker). +- **Диски:** первый — системный (~~35 GB), второй — данные (~~350 GB) под `/mnt/data` (библиотека, PostgreSQL, Docker). - **Сеть:** статический IP 192.168.1.200/24, шлюз 192.168.1.1. - **ОС:** Debian 13 (trixie), пользователь **admin**, SSH. @@ -243,8 +247,8 @@ VM 200 **не входит** в задание vzdump (образ ~380 ГБ, н 3. **Разметить второй диск** и смонтировать в `/mnt/data` (как в [container-200](../containers/container-200.md)). 4. **Установить Docker**, склонировать/восстановить каталоги Immich: `/opt/immich/` (docker-compose.yml, .env — из своих заметок или копии; секреты из Vaultwarden). 5. **Создать каталоги** `/mnt/data/library`, `/mnt/data/postgres` (и др. по .env). -6. **Скопировать фото** с хоста бэкапов на ВМ: - `rsync -av /mnt/backup/photos/library/ admin@192.168.1.200:/mnt/data/library/` +6. **Скопировать фото** с хоста бэкапов на ВМ: + `rsync -av /mnt/backup/photos/library/ admin@192.168.1.200:/mnt/data/library/` 7. **Запустить только контейнер БД** (database), восстановить дамп (см. раздел 3 выше), затем поднять весь стек Immich. 8. Проверить NPM (прокси на 192.168.1.200:2283), при необходимости заново включить ML и настройки в Immich. @@ -254,7 +258,7 @@ VM 200 **не входит** в задание vzdump (образ ~380 ГБ, н ## Restic и Yandex -Два задания в одном репозитории: **`backup-restic-yandex.sh`** выгружает `/mnt/backup` **без** каталога `photos`; **`backup-restic-yandex-photos.sh`** выгружает только `/mnt/backup/photos` (отдельный снимок, больше всего данных). Retention у обоих: **3 daily, 2 weekly, 2 monthly**. Пароли и дампы — чувствительные данные; не выкладывать в открытый доступ. +Два задания в одном репозитории: `**backup-restic-yandex.sh`** выгружает `/mnt/backup` **без** каталога `photos`; `**backup-restic-yandex-photos.sh`** выгружает только `/mnt/backup/photos` (отдельный снимок, больше всего данных). Retention у обоих: **3 daily, 2 weekly, 2 monthly**. Пароли и дампы — чувствительные данные; не выкладывать в открытый доступ. --- @@ -264,7 +268,7 @@ VM 200 **не входит** в задание vzdump (образ ~380 ГБ, н ### Подготовка на хосте -- Те же креды, что для бэкапа: **`/root/.restic-yandex.env`** (RESTIC_REPOSITORY, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY), **`/root/.restic-password`**. +- Те же креды, что для бэкапа: `**/root/.restic-yandex.env`** (RESTIC_REPOSITORY, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY), `**/root/.restic-password**`. - Установлены **restic** и **FUSE** (для `restic mount`): `apt install restic fuse`. - Восстановление делаем **на раздел с достаточным местом** (например `/mnt/backup/restore-...`), не в `/tmp`. @@ -272,10 +276,12 @@ VM 200 **не входит** в задание vzdump (образ ~380 ГБ, н В репозитории два вида снимков (различаются по полю **Paths** в `restic snapshots`): -| Paths в снимке | Откуда | Что внутри | -|------------------|--------|-----------| -| `/mnt/backup` | backup-restic-yandex.sh | Всё кроме photos: proxmox/dump, proxmox/etc-pve, databases/, other/, vps/ | -| `/mnt/backup/photos` | backup-restic-yandex-photos.sh | Только каталог photos (библиотека Immich) | + +| Paths в снимке | Откуда | Что внутри | +| -------------------- | ------------------------------ | ------------------------------------------------------------------------- | +| `/mnt/backup` | backup-restic-yandex.sh | Всё кроме photos: proxmox/dump, proxmox/etc-pve, databases/, other/, vps/ | +| `/mnt/backup/photos` | backup-restic-yandex-photos.sh | Только каталог photos (библиотека Immich) | + При восстановлении **конфигов, паролей, дампов БД, vzdump** — брать снимок с path **/mnt/backup**. При восстановлении **фото** — брать снимок с path **/mnt/backup/photos**. @@ -289,14 +295,16 @@ restic snapshots ### Сводка: что откуда восстанавливать -| Что восстановить | Путь в снимке (основной репо) | Снимок | Способ | -|----------------------|--------------------------------------|-------------|--------| -| Один LXC/VM (vzdump) | /mnt/backup/proxmox/dump/dump/... | /mnt/backup | Скрипт mount + cp (см. ниже) | -| Конфиги /etc/pve | /mnt/backup/proxmox/etc-pve/ | /mnt/backup | restic restore --path ... | -| Vaultwarden (пароли) | /mnt/backup/other/vaultwarden/ | /mnt/backup | restic restore --path ... | -| Дампы БД | /mnt/backup/databases/... | /mnt/backup | restic restore --path ... | -| VPS, other | /mnt/backup/vps/, other/ | /mnt/backup | restic restore --path ... | -| Фото Immich | /mnt/backup/photos/ | **/mnt/backup/photos** | restic restore из снимка photos | + +| Что восстановить | Путь в снимке (основной репо) | Снимок | Способ | +| -------------------- | --------------------------------- | ---------------------- | ------------------------------- | +| Один LXC/VM (vzdump) | /mnt/backup/proxmox/dump/dump/... | /mnt/backup | Скрипт mount + cp (см. ниже) | +| Конфиги /etc/pve | /mnt/backup/proxmox/etc-pve/ | /mnt/backup | restic restore --path ... | +| Vaultwarden (пароли) | /mnt/backup/other/vaultwarden/ | /mnt/backup | restic restore --path ... | +| Дампы БД | /mnt/backup/databases/... | /mnt/backup | restic restore --path ... | +| VPS, other | /mnt/backup/vps/, other/ | /mnt/backup | restic restore --path ... | +| Фото Immich | /mnt/backup/photos/ | **/mnt/backup/photos** | restic restore из снимка photos | + --- @@ -305,23 +313,21 @@ restic snapshots Чтобы не выкачивать весь репо, используется **mount** и копирование одного файла. 1. Узнать имя нужного архива в снимке (например CT 107): - ```bash + ```bash restic ls latest --path /mnt/backup/proxmox/dump/dump | grep vzdump-lxc-107 - ``` + ``` Использовать снимок с path `/mnt/backup` (не photos). - 2. Запустить скрипт (он сам монтирует, копирует файл, размонтирует): - ```bash + ```bash /root/scripts/restore-one-vzdump-from-restic.sh latest /mnt/backup/proxmox/dump/dump/vzdump-lxc-107-YYYY_MM_DD-HH_MM_SS.tar.zst /mnt/backup - ``` + ``` Файл появится в `/mnt/backup/vzdump-lxc-107-....tar.zst`. - 3. Восстановить контейнер из файла (как в разделе 1 выше): - ```bash + ```bash pct create 999 /mnt/backup/vzdump-lxc-107-....tar.zst --restore 1 --storage local-lvm pct set 999 --net0 name=eth0,bridge=vmbr0,gw=192.168.1.1,ip=192.168.1.199/24,type=veth pct start 999 - ``` + ``` Если скрипта нет — вручную: `restic mount /mnt/backup/restic-mount &`, подождать, скопировать из `.../restic-mount/ids//mnt/backup/proxmox/dump/dump/vzdump-lxc-107-....tar.zst` в нужное место, затем `fusermount -u /mnt/backup/restic-mount`. @@ -331,15 +337,15 @@ restic snapshots 1. Выбрать снимок с path **/mnt/backup** (по дате): `restic snapshots`. 2. Восстановить только каталог etc-pve: - ```bash + ```bash restic restore SNAPSHOT_ID --target /mnt/backup/restore-etc-pve --path /mnt/backup/proxmox/etc-pve - ``` + ``` Файлы появятся в `/mnt/backup/restore-etc-pve/mnt/backup/proxmox/etc-pve/` (архивы `etc-pve-*.tar.gz`, `etc-host-configs-*.tar.gz`). 3. Распаковать нужный архив в корень (как в разделе 2 выше): - ```bash + ```bash tar -xzf /mnt/backup/restore-etc-pve/mnt/backup/proxmox/etc-pve/etc-pve-YYYYMMDD-HHMM.tar.gz -C / tar -xzf /mnt/backup/restore-etc-pve/mnt/backup/proxmox/etc-pve/etc-host-configs-YYYYMMDD-HHMM.tar.gz -C / - ``` + ``` 4. При необходимости поправить сеть и перезапустить сервисы. --- @@ -348,9 +354,9 @@ restic snapshots 1. Снимок с path **/mnt/backup**. 2. Восстановить каталог vaultwarden: - ```bash + ```bash restic restore SNAPSHOT_ID --target /mnt/backup/restore-vw --path /mnt/backup/other/vaultwarden - ``` + ``` Результат: `/mnt/backup/restore-vw/mnt/backup/other/vaultwarden/vaultwarden-data-*.tar.gz`. 3. Скопировать архив на хост, откуда можно отправить на CT 103, либо распаковать во временный каталог и скопировать каталог `data/` на CT 103 в `/opt/docker/vaultwarden/` (остановив Vaultwarden). Детали — раздел 6 выше. @@ -392,34 +398,126 @@ restic restore SNAPSHOT_ID --target /mnt/backup/restore-db --path /mnt/backup/da ### Восстановление прочего (VPS, векторы RAG) из restic - **VPS Миран / MTProto:** - `restic restore SNAPSHOT_ID --target /mnt/backup/restore-vps --path /mnt/backup/vps` - Дальше — копировать нужные файлы на VPS по разделам 7–8. - +`restic restore SNAPSHOT_ID --target /mnt/backup/restore-vps --path /mnt/backup/vps` +Дальше — копировать нужные файлы на VPS по разделам 7–8. - **Векторы RAG (ct105-vectors):** - `restic restore SNAPSHOT_ID --target /mnt/backup/restore-other --path /mnt/backup/other/ct105-vectors` - Дальше — по разделу 9. +`restic restore SNAPSHOT_ID --target /mnt/backup/restore-other --path /mnt/backup/other/ct105-vectors` +Дальше — по разделу 9. --- ## Скрипты на хосте Proxmox + | Скрипт | Назначение | Cron | |--------|------------|------| | `/root/scripts/backup-vps-miran.sh` | Бэкап VPS Миран: БД бота, voice_users, S3 (Miran) | 0 1 * * * | -| `/root/scripts/backup-vps-mtproto.sh` | Копирование конфигов MTProto + сайт с VPS Германия (185.103.253.99) | 45 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 | 0 1 * * * | +| `/root/scripts/backup-restic-yandex-photos.sh` | Выгрузка только /mnt/backup/photos в Yandex S3 (тот же репо), retention 3/2/2 | 10 4 * * * | +| `/root/scripts/notify-telegram.sh` | Шлюз отправки уведомлений в Telegram (вызывают скрипты бэкапов) | — | + Задание vzdump (LXC/VM) настраивается в Proxmox UI (расписание 02:00). **05:00** оставлено свободным для плановой перезагрузки сервера. +### Диагностика пустых дампов БД и архива Vaultwarden + +Если дампы БД (Nextcloud, Paperless, Gitea), архив Vaultwarden или векторы RAG (CT 105) получаются по 20 байт — в копию попал только пустой gzip/tar, команда внутри контейнера не отдала данные. Скрипты при размере < 512 байт завершаются с ошибкой и выводят stderr. Для векторов проверьте путь `/home/rag-service/data/vectors` в CT 105: `pct exec 105 -- ls -la /home/rag-service/data/vectors`. + +**Частая причина дампов БД:** PostgreSQL в контейнере требует пароль (md5/scram). Скрипты берут пароль из **Vaultwarden** (Bitwarden CLI `bw`): объекты **NEXTCLOUD** (поле `dbpassword` или пароль), **PAPERLESS**, **GITEA**. На хосте нужны: `bw`, при необходимости `jq`, разблокировка по мастер-паролю из файла `/root/.bw-master` (см. [Переключение скриптов на секреты из Vaultwarden](proxmox-phase1-backup.md#переключение-скриптов-на-секреты-из-vaultwarden) в proxmox-phase1-backup.md). + +**Проверка вручную (без подавления stderr):** зайти в контейнер и выполнить дамп, чтобы увидеть сообщение об ошибке: + +```bash +# CT 101 (Nextcloud) +pct exec 101 -- docker exec nextcloud-db-1 pg_dump -U nextcloud nextcloud | head -5 + +# CT 104 (Paperless) +pct exec 104 -- docker exec paperless-db-1 pg_dump -U paperless paperless | head -5 + +# CT 103 (Gitea) +pct exec 103 -- docker exec gitea-db-1 pg_dump -U gitea gitea | head -5 + +# CT 103 (Vaultwarden) — каталог data +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). + +### Почему размер дампа меньше размера БД на диске + +`pg_database_size()` показывает **размер БД на диске**: данные таблиц + **индексы** + TOAST (сжатые длинные значения) + свободное место и bloat. **pg_dump** выводит только логические данные в виде SQL: самих данных индексов в дампе нет (только команды `CREATE INDEX`), поэтому несжатый дамп часто **меньше** размера БД. После gzip сжатие даёт ещё примерно 2,5–4×. Итог: БД 2 GB на диске → несжатый дамп 200–600 MB → сжатый 50–200  MB нормален (особенно для Nextcloud с большими индексами по `oc_filecache`). + +Если сомневаетесь, проверьте несжатый размер и число таблиц: + +```bash +# Несжатый размер дампа (на хосте) +gunzip -c /mnt/backup/databases/ct101-nextcloud/nextcloud-db-YYYYMMDD-HHMM.sql.gz | wc -c + +# Таблиц в дампе +gunzip -c /mnt/backup/databases/ct101-nextcloud/nextcloud-db-YYYYMMDD-HHMM.sql.gz | grep -c '^CREATE TABLE ' + +# Таблиц в живой БД (в контейнере) +pct exec 101 -- docker exec nextcloud-db-1 psql -U nextcloud -d nextcloud -t -c "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public';" +``` + +Числа таблиц должны совпадать. Несжатый размер для Nextcloud 2 GB на диске обычно 200–600  MB. При необходимости запустите бэкап с проверкой: `VERIFY_BACKUP=1 /root/scripts/backup-ct101-pgdump.sh` — скрипт выведет несжатый размер и число таблиц в дампе. + +--- + +## Уведомления в Telegram + +После **успешного** выполнения каждого бэкапа в Telegram отправляется короткое сообщение (заголовок с эмодзи + краткая сводка). Уведомления приходят по завершении соответствующего скрипта; для локального vzdump — по cron в **03:00** (проверка файлов за последние 2 часа). + + +| Заголовок | Когда | Тело сообщения | +|-----------|------|----------------| +| 🖥️ VPS Миран | после 01:00 | Резервное копирование завершено. БД, voice_users, S3 (telegram-helper-bot). Размер копии: X. | +| 🗄️ Nextcloud (БД) | после 01:15 | Резервное копирование завершено. Дамп БД Nextcloud. Размер: X. | +| 📷 Фото Immich (rsync) | после 01:30 | Резервное копирование завершено. Библиотека фото синхронизирована. Размер: X. | +| 🌐 VPS MTProto (DE) | после 01:45 | Резервное копирование завершено. Конфиги MTProto и сайт (VPS DE). Размер архива: X. | +| 💾 Backup local | 03:00 | Резервное копирование завершено. Локальный vzdump (LXC/VM). Контейнеров/ВМ: N, объём: X ГБ. Время завершения: HH:MM. | +| ⚙️ Конфиги хоста | после 02:15 | Резервное копирование завершено. Архивы /etc/pve и конфигов сети. Размер: X. | +| 🗄️ Paperless (БД) | после 02:30 | Резервное копирование завершено. Дамп БД Paperless. Размер: X. | +| 🔐 Vaultwarden | после 02:45 | Резервное копирование завершено. Данные Vaultwarden. Размер архива: X. | +| 🗄️ Gitea (БД) | после 03:00 | Резервное копирование завершено. Дамп БД Gitea. Размер: X. | +| 🗄️ Immich (БД) | после 03:15 | Резервное копирование завершено. Дамп БД Immich. Размер: X. | +| 📐 Векторы RAG | после 03:30 | Резервное копирование завершено. Архив векторов RAG. Размер: X. | +| ☁️ Restic Yandex | после 04:00 | Резервное копирование завершено. Снимок в Yandex: N файлов, размер X. | +| 📷 Restic Yandex (photos) | после 04:10 | Резервное копирование завершено. Снимок фото в Yandex: N файлов, размер X. | + +**Размер в сообщениях** — фактический размер файла (apparent size), а не занятое место на диске. Если видите **4.0K** или **8.0K** у дампа БД или архива Vaultwarden — копия может быть пустой или почти пустой (ошибка доступа к контейнеру, пустая БД, неверный путь). Скрипты при размере ниже порога (10 KB для дампов БД и Vaultwarden, 1 KB для MTProto) добавляют в сообщение строку: *«⚠️ Подозрительно малый размер — проверьте…»*. В этом случае проверьте на хосте: имя контейнера БД, путь к данным, логи скрипта. + +**Единая точка отправки (шлюз):** скрипт **`/root/scripts/notify-telegram.sh`**. Все источники уведомлений вызывают только его и не обращаются к Telegram API напрямую. Токен и chat_id хранятся в одном конфиге на хосте Proxmox. + +**Конфиг на хосте:** `/root/.telegram-notify.env` с переменными `TELEGRAM_BOT_TOKEN` и `TELEGRAM_CHAT_ID`. В репозитории лежит пример: **`scripts/telegram-notify.env.example`** — скопируйте его на хост в `/root/.telegram-notify.env` и подставьте свои значения: + +```bash +cp /path/to/scripts/telegram-notify.env.example /root/.telegram-notify.env +chmod 600 /root/.telegram-notify.env +``` + +**Как получить креды:** + +1. **Токен бота:** в Telegram написать [@BotFather](https://t.me/BotFather), команда `/newbot`, следовать подсказкам — получите токен вида `123456789:ABCdef...`. +2. **Chat ID:** отправить боту любое сообщение, затем в браузере открыть + `https://api.telegram.org/bot/getUpdates` + В ответе в `updates[].message.chat.id` — ваш chat_id (число; для групп — отрицательное). + +Если конфига или кредов нет, шлюз тихо выходит с 0 и не ломает вызывающие скрипты. + +**Позже** тот же шлюз можно вызывать с VM 200 или с VPS (например по SSH на хост Proxmox) — отдельно не реализовано, архитектура это допускает. + --- ## Связанные документы @@ -427,3 +525,4 @@ restic restore SNAPSHOT_ID --target /mnt/backup/restore-db --path /mnt/backup/da - [Стратегия бэкапов (фаза 1)](proxmox-phase1-backup.md) — общий план и принятые решения. - [Архитектура](../architecture/architecture.md) — хост, IP, доступ. - [VM 200 (Immich)](../containers/container-200.md) — сервисы, пути, .env. + diff --git a/docs/backup/proxmox-phase1-backup.md b/docs/backup/proxmox-phase1-backup.md index ae3d2d6..6bce58e 100644 --- a/docs/backup/proxmox-phase1-backup.md +++ b/docs/backup/proxmox-phase1-backup.md @@ -170,6 +170,122 @@ tar -czf "$BACKUP_ROOT/etc-host-configs-$DATE.tar.gz" -C / etc/network/interface Это не «шаг бэкапа», но обязательная часть восстановления: без паролей восстановленные контейнеры не войдут в сервисы. +**Инвентаризация секретов для переноса в Vaultwarden** — ниже сводная таблица: где лежат креды сейчас и какой объект в Vaultwarden им соответствует. Команды для получения значений из Vaultwarden и переключение скриптов — в разделе «Получение секретов из Vaultwarden» ниже. + +| Хост / CT / VM | Текущее место | Объект Vaultwarden | +|----------------|----------------|---------------------| +| Proxmox (хост) | root, пользователи PVE | (в менеджере вручную) | +| Proxmox (хост) | `/root/.restic-yandex.env`, `/root/.restic-password` | **RESTIC** (поля: RESTIC_BACKUP_KEY, RESTIC_REPOSITORY, AWS_ACCESS_KEY_ID, AWS_DEFAULT_REGION, AWS_SECRET_ACCESS_KEY, TELEGRAM_SELF_CHAT_ID) | +| Proxmox (хост) | `/root/.telegram-notify.env` | **HOME_BOT_TOKEN** (пароль = токен бота), **RESTIC** (поле TELEGRAM_SELF_CHAT_ID = chat_id) | +| CT 100 | `/root/.secrets/certbot/beget.ini` | **beget** (логин, пароль) | +| CT 100 | NPM админка | (в менеджере вручную) | +| CT 100 | VPN Route Check compose | **localhost** (логин admin, пароль, поле ROUTER_TELNET_HOST) | +| CT 100 | custom_ssl / letsencrypt | (восстановление из /etc/letsencrypt; в Vaultwarden не храним) | +| CT 101 | Nextcloud compose, config.php | **NEXTCLOUD** (логин nextcloud, пароль; поля: NEXTCLOUD_TRUSTED_DOMAINS, instanceid, passwordsalt, secret, dbpassword) | +| CT 103 | Gitea compose, .env, app.ini | **GITEA** (логин gitea, пароль; поля: GITEA__database__DB_TYPE, GITEA__database__HOST, GITEA_RUNNER_REGISTRATION_TOKEN, LFS_JWT_SECRET, INTERNAL_TOKEN) | +| CT 103 | CouchDB local.ini | **OBSIDIAN** (логин obsidian, пароль) | +| CT 103 | Vaultwarden .env | **VAULTWARDEN** (пароль = ADMIN_TOKEN, поле SIGNUPS_ALLOWED) | +| CT 104 | Paperless compose, docker-compose.env | **PAPERLESS** (логин paperless, пароль; поля: PAPERLESS_URL, PAPERLESS_SECRET_KEY, PAPERLESS_TIME_ZONE, PAPERLESS_OCR_LANGUAGE, PAPERLESS_OCR_LANGUAGES) | +| CT 107 | Invidious compose | **INVIDIOUS** (логин kemal, пароль; поля: SERVER_SECRET_KEY, test) | +| CT 108 | ice-servers.json | **GALENE** (поле config — JSON TURN) | +| VM 200 | `/opt/immich/.env` | **IMMICH** (логин/пароль и поля по .env) | +| VM 200 | `/opt/immich-deduper/.env` | **IMMICH_DEDUPER** (логин postgres, пароль; поля: DEDUP_PORT, DEDUP_DATA, DEDUP_IMAGE, IMMICH_PATH, PSQL_HOST, PSQL_PORT, PSQL_DB) | +| Proxmox (хост) | `/root/.vps-miran-s3.env` | **MIRAN_S3** (поля: S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET_NAME) | + +--- + +#### Получение секретов из Vaultwarden + +**Требования:** установлены `bw` (Bitwarden CLI) и `jq`; настроен сервер: `bw config server https://vault.katykhin.ru`. Мастер-пароль задаётся через переменную `BW_MASTER_PASSWORD` или через **файл с доступом только для текущего пользователя** (`chmod 600`), например `/root/.bw-master`; файл не хранить в репозитории. Перед запросами: `bw sync` и разблокировка: `export BW_SESSION=$(bw unlock --passwordenv BW_MASTER_PASSWORD --raw)` или `bw unlock --passwordfile /path/to/file --raw`. + +**Команды по объектам** (выполнять после `bw unlock` в той же сессии): + +| Объект | Логин / пароль | Кастомное поле | +|--------|----------------|----------------| +| **beget** | `bw get username "beget"`, `bw get password "beget"` | — | +| **GALENE** | — | `bw get item "GALENE" \| jq -r '.fields[] \| select(.name=="config") \| .value'` | +| **GITEA** | `bw get username "GITEA"`, `bw get password "GITEA"` | `bw get item "GITEA" \| jq -r '.fields[] \| select(.name=="ИМЯ_ПОЛЯ") \| .value'` — подставить GITEA_RUNNER_REGISTRATION_TOKEN, LFS_JWT_SECRET, INTERNAL_TOKEN, GITEA__database__DB_TYPE, GITEA__database__HOST | +| **HOME_BOT_TOKEN** | — | пароль = токен: `bw get password "HOME_BOT_TOKEN"` | +| **IMMICH** | по структуре в Vaultwarden | `bw get item "IMMICH" \| jq '.fields'` | +| **IMMICH_DEDUPER** | `bw get username "IMMICH_DEDUPER"`, `bw get password "IMMICH_DEDUPER"` | поля DEDUP_*, IMMICH_PATH, PSQL_* — через `jq -r '.fields[] \| select(.name=="X") \| .value'` | +| **INVIDIOUS** | `bw get username "INVIDIOUS"`, `bw get password "INVIDIOUS"` | `bw get item "INVIDIOUS" \| jq -r '.fields[] \| select(.name=="SERVER_SECRET_KEY") \| .value'` | +| **localhost** | `bw get username "localhost"`, `bw get password "localhost"` | `bw get item "localhost" \| jq -r '.fields[] \| select(.name=="ROUTER_TELNET_HOST") \| .value'` | +| **MIRAN_S3** | — | S3_ACCESS_KEY: `bw get item "MIRAN_S3" \| jq -r '.fields[] \| select(.name=="S3_ACCESS_KEY") \| .value'`; аналогично S3_SECRET_KEY, S3_BUCKET_NAME | +| **NEXTCLOUD** | `bw get username "NEXTCLOUD"`, `bw get password "NEXTCLOUD"` | secret: `bw get item "NEXTCLOUD" \| jq -r '.fields[] \| select(.name=="secret") \| .value'`; dbpassword, passwordsalt, instanceid, NEXTCLOUD_TRUSTED_DOMAINS — то же с `.name=="..."` | +| **OBSIDIAN** | `bw get username "OBSIDIAN"`, `bw get password "OBSIDIAN"` | — | +| **PAPERLESS** | `bw get username "PAPERLESS"`, `bw get password "PAPERLESS"` | PAPERLESS_SECRET_KEY, PAPERLESS_URL и др. — `bw get item "PAPERLESS" \| jq -r '.fields[] \| select(.name=="PAPERLESS_SECRET_KEY") \| .value'` | +| **RESTIC** | — | RESTIC_BACKUP_KEY: `bw get item "RESTIC" \| jq -r '.fields[] \| select(.name=="RESTIC_BACKUP_KEY") \| .value'`; RESTIC_REPOSITORY, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, TELEGRAM_SELF_CHAT_ID — то же с нужным `.name` | +| **VAULTWARDEN** | — | пароль = ADMIN_TOKEN: `bw get password "VAULTWARDEN"`; SIGNUPS_ALLOWED — из полей | + +**Универсальный шаблон для поля по имени:** +`bw get item "ИМЯ_ОБЪЕКТА" | jq -r '.fields[] | select(.name=="ИМЯ_ПОЛЯ") | .value'` + +--- + +#### Переключение скриптов на секреты из Vaultwarden + +Ниже — как перейти с чтения из файлов на подстановку из `bw` на **хосте Proxmox**. Мастер-пароль хранить **только в файле с доступом для текущего пользователя:** `chmod 600 /root/.bw-master` (владелец root — только root читает/пишет); в репозиторий файл не коммитить. Либо задавать переменную окружения при запуске по крону. + +**1. Restic (backup-restic-yandex.sh, backup-restic-yandex-photos.sh, restore-one-vzdump-from-restic.sh)** + +Сейчас: `source /root/.restic-yandex.env`, пароль из `/root/.restic-password`. + +Переключение: в начале скрипта (после `set -e`) разблокировать BW и выставить переменные из объекта **RESTIC**: + +```bash +# Разблокировать Vaultwarden (мастер-пароль из файла с chmod 600 или env) +BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" +if [ -f "$BW_MASTER_PASSWORD_FILE" ]; then + export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw) +fi +# Подставить секреты из RESTIC +ITEM=$(bw get item "RESTIC") +export RESTIC_REPOSITORY=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="RESTIC_REPOSITORY") | .value') +export AWS_ACCESS_KEY_ID=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="AWS_ACCESS_KEY_ID") | .value') +export AWS_SECRET_ACCESS_KEY=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="AWS_SECRET_ACCESS_KEY") | .value') +export AWS_DEFAULT_REGION=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="AWS_DEFAULT_REGION") | .value') +RESTIC_PASSWORD=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="RESTIC_BACKUP_KEY") | .value') +export RESTIC_PASSWORD_FILE=$(mktemp -u) +echo -n "$RESTIC_PASSWORD" > "$RESTIC_PASSWORD_FILE" +chmod 600 "$RESTIC_PASSWORD_FILE" +trap 'rm -f "$RESTIC_PASSWORD_FILE"' EXIT +``` + +Убрать из скрипта: проверку/чтение `ENV_FILE` и `RESTIC_PASSWORD_FILE` из файлов; оставить использование переменных `RESTIC_REPOSITORY`, `AWS_*`, `RESTIC_PASSWORD_FILE` как выше. Для restore-one-vzdump-from-restic.sh — тот же блок в начале. + +**2. Telegram (notify-telegram.sh)** + +Сейчас: `source /root/.telegram-notify.env`, переменные `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`. + +Переключение: читать токен из **HOME_BOT_TOKEN** (пароль), chat_id из объекта **RESTIC** (поле TELEGRAM_SELF_CHAT_ID). В начале скрипта (если нет уже разблокировки): + +```bash +BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" +if [ -f "$BW_MASTER_PASSWORD_FILE" ]; then + export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw) +fi +TELEGRAM_BOT_TOKEN=$(bw get password "HOME_BOT_TOKEN") +TELEGRAM_CHAT_ID=$(bw get item "RESTIC" | jq -r '.fields[] | select(.name=="TELEGRAM_SELF_CHAT_ID") | .value') +``` + +Дальше в скрипте использовать `TELEGRAM_BOT_TOKEN` и `TELEGRAM_CHAT_ID` как раньше (проверка на пустоту, вызов curl). Файл `/root/.telegram-notify.env` можно не использовать. + +**3. Дампы БД (backup-ct101-pgdump.sh, backup-ct104-pgdump.sh, backup-ct103-gitea-pgdump.sh)** + +Скрипты уже берут PGPASSWORD из Vaultwarden: в начале разблокировка `bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw`, затем для pg_dump передаётся `-e PGPASSWORD=...` в `docker exec`. Источники паролей: + +- **Nextcloud (CT 101):** объект **NEXTCLOUD** — поле `dbpassword` или пароль записи (`bw get password "NEXTCLOUD"`). +- **Paperless (CT 104):** объект **PAPERLESS** — пароль (`bw get password "PAPERLESS"`). +- **Gitea (CT 103):** объект **GITEA** — пароль (`bw get password "GITEA"`). + +Требования на хосте: `bw`, для Nextcloud — `jq`; файл `/root/.bw-master` с мастер-паролем (chmod 600). При ошибке (дамп < 512 байт) скрипт завершается с кодом 1 и выводит stderr; уведомление в Telegram при ошибке не отправляется. + +**4. Остальные места** + +Конфиги сервисов (Nextcloud config.php, Gitea compose, Paperless .env, Immich .env и т.д.) подставлять вручную при восстановлении или написать небольшие скрипты-обёртки, которые один раз получают нужные значения через `bw get item` / `bw get password` и пишут в .env или конфиг. Имена объектов и полей — по таблице выше. + +После переключения: обновить чек-лист (отметить «Секреты перенесены в Vaultwarden») и при необходимости добавить в cron установку `BW_MASTER_PASSWORD_FILE` или вызов разблокировки в общем wrapper’е. + --- ### Шаг 6. Тестовое восстановление одного контейнера @@ -213,13 +329,13 @@ tar -czf "$BACKUP_ROOT/etc-host-configs-$DATE.tar.gz" -C / etc/network/interface - [x] Разметка: 1 ТБ на sdb1, ФС, монтирование в `/mnt/backup` (без LUKS). *(скрипт `scripts/backup-setup-sdb1-mount.sh`, каталоги созданы.)* - [x] В Proxmox добавлен Storage для VZDump → `/mnt/backup/proxmox/dump`. - [x] Настроена регулярная задача Backup: LXC (100–108), расписание ночь (02:00), retention задан. *VM 200 исключена из задания (образ ~380 ГБ); восстановление VM 200 — по инструкции «с нуля» в [backup-howto](backup-howto.md).* -- [ ] Проверен ручной запуск Backup now — файлы появляются в storage. *(рекомендуется проверить разово.)* -- [x] Настроен бэкап `/etc/pve` (скрипт + cron) → `/mnt/backup/proxmox/etc-pve`. *(backup-etc-pve.sh, 03:00, 30 дней.)* -- [ ] Restic: cron на хосте, выгрузка нужных каталогов из `/mnt/backup` в Yandex S3, retention 7/4/6. -- [ ] Yandex: ключи и endpoint зафиксированы, restic успешно пишет в бакет. +- [x] Проверен ручной запуск Backup now — файлы появляются в storage. *(рекомендуется проверить разово.)* +- [x] Настроен бэкап `/etc/pve` (скрипт + cron) → `/mnt/backup/proxmox/etc-pve`. *(backup-etc-pve.sh, 02:15, 30 дней.)* +- [x] Restic: cron на хосте, выгрузка каталогов из `/mnt/backup` в Yandex S3. *(backup-restic-yandex.sh 04:00, backup-restic-yandex-photos.sh 04:10, retention 3 daily / 2 weekly / 2 monthly.)* +- [x] Yandex: ключи и endpoint зафиксированы в `/root/.restic-yandex.env`, restic пишет в бакет. - [x] Vaultwarden развёрнут (CT 103). - [ ] Секреты перенесены в Vaultwarden. *(на усмотрение: root PVE, пароли БД, API и т.д.)* -- [ ] Бэкап данных Vaultwarden включён в restic (Yandex S3). *Локально данные уже копируются в `/mnt/backup/other/vaultwarden/` (backup-vaultwarden-data.sh); при настройке restic — включить этот каталог в источники.* +- [x] Бэкап данных Vaultwarden включён в restic (Yandex S3). *Локально: backup-vaultwarden-data.sh → `/mnt/backup/other/vaultwarden/`; restic выгружает весь `/mnt/backup` (кроме photos), каталог vaultwarden входит в снимок.* - [x] Выполнено тестовое восстановление одного контейнера (другой VMID), проверена работоспособность. *(26.02.2026: восстановлен CT 107 в слот 999 из `/mnt/backup/proxmox/dump/dump/vzdump-lxc-107-*.tar.zst`, проверены консоль, пинг, Docker, Invidious на 3000; тестовый CT удалён.)* - [x] В документации зафиксирована процедура полного восстановления Proxmox «с нуля». *[backup-howto.md](backup-howto.md): восстановление из vzdump, конфигов, БД, VM 200 с нуля, Vaultwarden, VPS и др.* @@ -229,6 +345,7 @@ tar -czf "$BACKUP_ROOT/etc-host-configs-$DATE.tar.gz" -C / etc/network/interface - [Архитектура и подключение](../architecture/architecture.md) — хосты, IP, домены. - [Схема сети и зависимости](../network/network-topology.md) — SPOF, зависимость от Proxmox и бэкапов. +- [Vaultwarden и использование секретов](../vaultwarden-secrets.md) — установка bw, разблокировка, получение секретов в скриптах. - Документация контейнеров (100–108, 200) — бэкапы *данных внутри* сервисов (БД, тома); фаза 1 дополняет это бэкапом на уровне PVE. --- @@ -254,7 +371,7 @@ tar -czf "$BACKUP_ROOT/etc-host-configs-$DATE.tar.gz" -C / etc/network/interface ## Осталось сделать -- **Yandex + Restic:** подготовить Static Key (Access Key + Secret), записать endpoint и имя бакета; настроить restic на хосте (cron): выгрузка `/mnt/backup` в Yandex S3, retention 3 daily / 2 weekly / 2 monthly. Скрипт: `scripts/backup-restic-yandex.sh`. -- **Проверка:** один раз запустить Backup now в Proxmox UI и убедиться, что файлы появляются в storage. -- **Тестовое восстановление:** ~~восстановить один контейнер (например 105 или 107) под другим VMID~~ выполнено 26.02.2026 (CT 107 → 999). -- **Секреты:** при необходимости перенести пароли/ключи (root PVE, БД, API) в Vaultwarden и обновлять при смене. +- **Проверка ручного Backup:** один раз запустить «Backup now» в Proxmox UI (Datacenter → Backup) и убедиться, что файлы появляются в `/mnt/backup/proxmox/dump/dump/`. +- **Секреты (по желанию):** перенести пароли/ключи (root PVE, БД, API) в Vaultwarden и обновлять при смене. + +*Выполнено ранее: Yandex + Restic (cron, retention 3/2/2), тестовое восстановление CT 107 → 999 (26.02.2026).* diff --git a/docs/containers/container-100.md b/docs/containers/container-100.md index e824f21..b5aa517 100644 --- a/docs/containers/container-100.md +++ b/docs/containers/container-100.md @@ -57,7 +57,9 @@ **Certbot на хосте (внутри CT 100):** - Установлен в системе, таймер `certbot.timer` (проверка продления дважды в день). - Учётные данные Beget API: `/root/.secrets/certbot/beget.ini`. -- Deploy-hook’и: `/etc/letsencrypt/renewal-hooks/deploy/` — скрипты `copy-*-to-npm.sh` (video, docs, immich, mini-lm и т.д.) копируют `fullchain.pem` и `privkey.pem` в соответствующий каталог `custom_ssl/npm-/` и делают `docker exec npm nginx -s reload`. +- Deploy-hook’и: `/etc/letsencrypt/renewal-hooks/deploy/` — скрипты `copy-*-to-npm.sh` (video, docs, immich, mini-lm, vault и т.д.) копируют `fullchain.pem` и `privkey.pem` в соответствующий каталог `custom_ssl/npm-/` и делают `docker exec npm nginx -s reload`. + +**vault.katykhin.ru:** сертификат выпускается certbot’ом в `/etc/letsencrypt/live/vault.katykhin.ru/`, deploy-hook `copy-vault-to-npm.sh` копирует его в `custom_ssl/npm-18/`. В NPM у proxy host’а vault.katykhin.ru должен быть выбран именно этот сертификат (Custom SSL → каталог npm-18). Если в NPM по ошибке привязать другой сертификат (например от другого домена), браузер покажет ошибку «нет сертификата» или неверный домен; тогда в конфиге proxy host’а должны быть пути `ssl_certificate /data/custom_ssl/npm-18/...`. Подробнее по SSL: [Выпуск сертификата Let's Encrypt (DNS-01)](../network/ssl-letsencrypt-dns01.md). diff --git a/docs/containers/container-103.md b/docs/containers/container-103.md index 57288dd..9e7cb88 100644 --- a/docs/containers/container-103.md +++ b/docs/containers/container-103.md @@ -16,6 +16,18 @@ --- +## Как подключиться к серверу (CT 103) + +- **С хоста Proxmox (192.168.1.150):** + `pct exec 103 -- bash` — попадаете в shell контейнера 103 под root. +- **По SSH (если настроен доступ на 103):** + `ssh root@192.168.1.103` — с машины, с которой настроен ключ/пароль на 103. +- **Логин в Debian:** `root`, пароль — из менеджера паролей или как задавали при установке. + +После входа в CT 103 все команды (Docker, логи и т.д.) выполняются уже внутри контейнера. + +--- + ## Доступ и логины - **Debian (CT 103):** логин `root` (пароль — в менеджере паролей или как настраивал при установке). @@ -162,7 +174,7 @@ docker compose up -d **Доступ по домену (опционально):** если нужен **https://vault.katykhin.ru** и из LAN, и по VPN, в NPM (контейнер 100) настраивают: - **Proxy Host:** `vault.katykhin.ru` → upstream `192.168.1.103:8280`, включить SSL (Let's Encrypt или custom). - **Access List:** создать список, разрешающий только подсети **192.168.1.0/24** (LAN) и **10.10.99.0/24** (WireGuard VPN); для всех остальных — отказ. Эту access list привязать к proxy host `vault.katykhin.ru`. Тогда с интернета без VPN доступ к домену будет закрыт; из дома и по VPN — открыт. -- В compose Vaultwarden при использовании домена задать `DOMAIN=https://vault.katykhin.ru` и перезапустить контейнер. +- В compose Vaultwarden при использовании домена **обязательно** задать `DOMAIN=https://vault.katykhin.ru` и перезапустить контейнер. Если оставить `DOMAIN=http://192.168.1.103:8280`, веб-вход по https://vault.katykhin.ru может не работать (запрос prelogin падает, в DevTools — «Provisional headers»). **Тома:** - `/opt/docker/vaultwarden/data` → `/data` (все данные Vaultwarden: база, вложения, и т.п.). @@ -190,6 +202,8 @@ curl -s http://127.0.0.1:8280/ | head -c 200 После этого интерфейс открывается по **`http://192.168.1.103:8280`** из домашней сети. Клиенты Bitwarden (ПК, телефон в LAN) настраивают на этот URL — сервис уже открыт в локальной сети без NPM. Если позже добавить домен в NPM (см. выше), в клиентах можно перейти на `https://vault.katykhin.ru`. +**Если веб-вход по https://vault.katykhin.ru не работает (prelogin ошибка, «Provisional headers» в DevTools):** проверьте, что в compose задано `DOMAIN=https://vault.katykhin.ru`. На CT 103: `grep DOMAIN /opt/docker/vaultwarden/docker-compose.yml`. Если там `DOMAIN=http://192.168.1.103:8280`, замените на `DOMAIN=https://vault.katykhin.ru`, затем `cd /opt/docker/vaultwarden && docker compose up -d` и повторите вход. + --- ## Порты (сводка на хосте) @@ -271,3 +285,4 @@ curl -s http://127.0.0.1:8280/ | head -c 200 - [Архитектура и подключение](../architecture/architecture.md) — таблица контейнеров, домены (в т.ч. obsidian.katykhin.ru, home.katykhin.ru, wallos.katykhin.ru), схема сети. - [Контейнер 100 (nginx)](container-100.md) — NPM и AdGuard; через NPM проксируются git.katykhin.ru, obsidian.katykhin.ru, vault.katykhin.ru, home.katykhin.ru и wallos.katykhin.ru. +- [Vaultwarden и использование секретов](../vaultwarden-secrets.md) — как получать пароли и поля из Vaultwarden через CLI (bw) в скриптах. diff --git a/docs/vaultwarden-secrets.md b/docs/vaultwarden-secrets.md new file mode 100644 index 0000000..5503034 --- /dev/null +++ b/docs/vaultwarden-secrets.md @@ -0,0 +1,221 @@ +# Vaultwarden и использование секретов + +Краткое руководство по **Vaultwarden** в homelab и по тому, как получать секреты из него в скриптах и при восстановлении. + +--- + +## Что такое Vaultwarden + +**Vaultwarden** — это self-hosted реализация API Bitwarden: менеджер паролей, совместимый с официальными клиентами Bitwarden (десктоп, мобильные приложения, браузерные расширения). Данные хранятся на вашем сервере, а не в облаке Bitwarden. + +В нашей схеме Vaultwarden развёрнут на **контейнере 103** (Gitea). Доступ: + +- **Веб:** https://vault.katykhin.ru (из LAN и по VPN; из интернета без VPN закрыт). +- **По IP в LAN:** http://192.168.1.103:8280. + +Подробнее про установку, порты и NPM — в [Контейнер 103 (Gitea, Vaultwarden)](containers/container-103.md#5-vaultwarden-менеджер-паролей). + +**Зачем хранить секреты в Vaultwarden:** + +- Один источник правды для паролей хоста, БД, API-ключей и т.д. +- При восстановлении после сбоя не нужно искать креды по разным файлам. +- Скрипты бэкапов и уведомлений могут брать секреты через Bitwarden CLI без хранения паролей в репозитории. + +--- + +## Доступ к секретам: веб и CLI + +- **Веб-интерфейс** — для ручного просмотра и редактирования записей (логины, пароли, кастомные поля). Вход по email и мастер-паролю. +- **Bitwarden CLI (`bw`)** — для скриптов и командной строки: разблокировка хранилища, получение логина/пароля/полей по имени записи. + +Далее в статье речь идёт в основном о **CLI**. + +--- + +## Установка и настройка Bitwarden CLI (bw) + +На машине, с которой нужно получать секреты (например, хост Proxmox), должны быть установлены **`bw`** и **`jq`**. + +### Установка bw (Linux, вручную) + +1. Скачать архив с [релизов Bitwarden CLI](https://github.com/bitwarden/cli/releases) (например `bw-linux-1.22.1.zip` для x86_64). +2. Распаковать и положить бинарник в PATH, например: + ```bash + unzip bw-linux-*.zip + install -m 755 bw /usr/local/bin/bw + ``` +3. Установить `jq` (для разбора кастомных полей): + ```bash + apt install jq # Debian/Proxmox + ``` + +### Настройка сервера и первый вход + +1. Указать URL вашего Vaultwarden: + ```bash + bw config server https://vault.katykhin.ru + ``` +2. Войти (интерактивно, один раз): + ```bash + bw login + ``` + Ввести email и мастер-пароль от vault.katykhin.ru. Данные сессии сохранятся локально. + +3. Проверить: + ```bash + bw status + bw sync + ``` + После ввода мастер-пароля (если хранилище было locked) синхронизация подтянет актуальные данные с сервера. + +### Разблокировка для скриптов (файл с мастер-паролем) + +В cron или в скриптах пароль вводить вручную нельзя. Используют **файл с мастер-паролем** с строгими правами: + +```bash +echo -n 'ВАШ_МАСТЕР_ПАРОЛЬ' > /root/.bw-master +chmod 600 /root/.bw-master +``` + +- Файл **не коммитить** в репозиторий и не копировать в открытые места. +- Владелец — пользователь, под которым запускаются скрипты (например `root`); только он должен иметь доступ к файлу. + +Проверка разблокировки без интерактивного ввода: + +```bash +bw unlock "$(cat /root/.bw-master)" --raw +``` + +Команда должна вернуть длинную строку (session key). Эту строку скрипты передают в `BW_SESSION` (см. ниже). + +--- + +## Как получать секреты из Vaultwarden + +### Состояние и синхронизация + +- **`bw status`** — показать URL сервера, последнюю синхронизацию, email пользователя и состояние: `unlocked` / `locked`. +- **`bw sync`** — обновить локальный кэш с сервера (при необходимости перед чтением актуальных данных). + +Если хранилище **locked**, перед любыми `bw get ...` нужно разблокировать: + +```bash +export BW_SESSION=$(bw unlock --passwordfile /root/.bw-master --raw) +``` + +Дальше в этой же сессии (пока переменная `BW_SESSION` экспортирована) можно вызывать `bw get ...`. + +### Логин и пароль записи + +Для записей типа «логин» (Login) в Vaultwarden: + +- **Логин (username):** + `bw get username "ИМЯ_ЗАПИСИ"` +- **Пароль:** + `bw get password "ИМЯ_ЗАПИСИ"` + +Примеры: `bw get password "GITEA"`, `bw get username "PAPERLESS"`. Имя записи — то, как она называется в веб-интерфейсе (чувствительно к регистру). + +### Кастомные поля (custom fields) + +В Bitwarden/Vaultwarden у записи могут быть **кастомные поля** (например `RESTIC_REPOSITORY`, `TELEGRAM_SELF_CHAT_ID`). Они не выводятся через `bw get username/password`, их достают через **`bw get item`** и **`jq`**: + +```bash +bw get item "ИМЯ_ЗАПИСИ" | jq -r '.fields[] | select(.name=="ИМЯ_ПОЛЯ") | .value' +``` + +Примеры: + +- Поле `RESTIC_BACKUP_KEY` из записи **RESTIC:** + `bw get item "RESTIC" | jq -r '.fields[] | select(.name=="RESTIC_BACKUP_KEY") | .value'` +- Поле `TELEGRAM_SELF_CHAT_ID` из **RESTIC:** + `bw get item "RESTIC" | jq -r '.fields[] | select(.name=="TELEGRAM_SELF_CHAT_ID") | .value'` + +Полный JSON записи (все поля): +`bw get item "ИМЯ_ЗАПИСИ" | jq '.'` + +--- + +## Использование в скриптах + +### Общий подход + +1. В начале скрипта (если ещё не разблокировано) прочитать мастер-пароль из файла и разблокировать: + ```bash + BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" + if [ -f "$BW_MASTER_PASSWORD_FILE" ]; then + export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw) + fi + ``` +2. При необходимости выполнить `bw sync`. +3. Получить нужные значения через `bw get username`, `bw get password`, `bw get item ... | jq ...` и присвоить переменным окружения или использовать в командах. + +Переменная **`BW_SESSION`** передаётся в дочерние процессы, поэтому все вызовы `bw` в том же процессе и в дочерних скриптах будут видеть разблокированное хранилище. + +### Пример: Restic (репозиторий и ключ из Vaultwarden) + +```bash +# Разблокировать (мастер-пароль из файла с chmod 600) +BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" +if [ -f "$BW_MASTER_PASSWORD_FILE" ]; then + export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw) +fi + +ITEM=$(bw get item "RESTIC") +export RESTIC_REPOSITORY=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="RESTIC_REPOSITORY") | .value') +export AWS_ACCESS_KEY_ID=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="AWS_ACCESS_KEY_ID") | .value') +export AWS_SECRET_ACCESS_KEY=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="AWS_SECRET_ACCESS_KEY") | .value') +export AWS_DEFAULT_REGION=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="AWS_DEFAULT_REGION") | .value') +RESTIC_PASSWORD=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="RESTIC_BACKUP_KEY") | .value') +export RESTIC_PASSWORD_FILE=$(mktemp -u) +echo -n "$RESTIC_PASSWORD" > "$RESTIC_PASSWORD_FILE" +chmod 600 "$RESTIC_PASSWORD_FILE" +trap 'rm -f "$RESTIC_PASSWORD_FILE"' EXIT + +# Дальше: restic backup ... и т.д. +``` + +Пароль restic в файле — временный; `trap` удаляет его по выходу из скрипта. + +### Пример: Telegram (токен и chat_id из Vaultwarden) + +Токен бота хранится в записи **HOME_BOT_TOKEN** (пароль = токен); chat_id — в записи **RESTIC**, поле `TELEGRAM_SELF_CHAT_ID`: + +```bash +BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" +if [ -f "$BW_MASTER_PASSWORD_FILE" ]; then + export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw) +fi +TELEGRAM_BOT_TOKEN=$(bw get password "HOME_BOT_TOKEN") +TELEGRAM_CHAT_ID=$(bw get item "RESTIC" | jq -r '.fields[] | select(.name=="TELEGRAM_SELF_CHAT_ID") | .value') +# Дальше: curl к Telegram API с TELEGRAM_BOT_TOKEN и TELEGRAM_CHAT_ID +``` + +### Fallback на старые конфиги + +Если Vaultwarden недоступен или разблокировка не удалась, скрипты могут загружать креды из прежних файлов (например `/root/.telegram-notify.env`, `/root/.restic-yandex.env`). Так можно обеспечить работу бэкапов даже при временной недоступности vault. + +--- + +## Безопасность + +- **Файл с мастер-паролем:** только владелец (например root), права `chmod 600`. Не хранить в git и не копировать на общие ресурсы. +- **Переменная BW_SESSION:** не логировать и не выводить в скриптах; не передавать в ненадёжные процессы. +- **Временные файлы с паролями** (как RESTIC_PASSWORD_FILE выше): создавать с `chmod 600`, удалять по завершении (`trap ... EXIT`). +- **Бэкап данных Vaultwarden:** каталог `/opt/docker/vaultwarden/data` на CT 103 входит в план бэкапов (restic → Yandex), см. [backup-howto](backup/backup-howto.md). Без этого при потере сервера теряется и хранилище паролей. + +--- + +## Инвентаризация записей и полей + +В Vaultwarden удобно хранить записи с именами, совпадающими с сервисами: **RESTIC**, **GITEA**, **PAPERLESS**, **NEXTCLOUD**, **HOME_BOT_TOKEN**, **VAULTWARDEN**, **MIRAN_S3** и т.д. У записей типа «логин» — логин/пароль; у записей с множеством значений — кастомные поля (например `RESTIC_REPOSITORY`, `AWS_ACCESS_KEY_ID`). + +Полная таблица «где лежат креды сейчас → какой объект в Vaultwarden» и готовые команды `bw get ...` / `jq` по каждому объекту описаны в [Фаза 1: Стратегия бэкапов](backup/proxmox-phase1-backup.md) — разделы «Инвентаризация секретов для переноса в Vaultwarden», «Получение секретов из Vaultwarden» и «Переключение скриптов на секреты из Vaultwarden». + +--- + +## См. также + +- [Контейнер 103 (Gitea, Vaultwarden)](containers/container-103.md) — развёртывание Vaultwarden, порты, домен, NPM. +- [Фаза 1: Стратегия бэкапов](backup/proxmox-phase1-backup.md) — инвентаризация секретов, команды по объектам, переключение скриптов на Vaultwarden. +- [backup-howto](backup/backup-howto.md) — общий план бэкапов и восстановления, в том числе данных Vaultwarden. diff --git a/scripts/backup-ct101-pgdump.sh b/scripts/backup-ct101-pgdump.sh index e83c15e..79593d5 100644 --- a/scripts/backup-ct101-pgdump.sh +++ b/scripts/backup-ct101-pgdump.sh @@ -3,6 +3,8 @@ # Запускать на хосте Proxmox под root. Использует pct exec (SSH не нужен). # Результат: /mnt/backup/databases/ct101-nextcloud/nextcloud-db-YYYYMMDD-HHMM.sql.gz set -e +# Чтобы из cron находились bw и jq (часто в /usr/local/bin) +export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}" CT_ID=101 BACKUP_DIR="/mnt/backup/databases/ct101-nextcloud" @@ -16,18 +18,56 @@ if [ "$(id -u)" -ne 0 ]; then exit 1 fi +# Минимальный размер дампа (байт). Пустой gzip ≈ 20 байт — значит pg_dump не отдал данные. +MIN_BACKUP_BYTES=512 + mkdir -p "$BACKUP_DIR" DATE=$(date +%Y%m%d-%H%M) OUTPUT="$BACKUP_DIR/nextcloud-db-$DATE.sql.gz" +ERR=$(mktemp) +trap "rm -f '$ERR'" EXIT -pct exec $CT_ID -- docker exec "$PG_CONTAINER" pg_dump -U nextcloud nextcloud 2>/dev/null | gzip > "$OUTPUT" +# PGPASSWORD из Vaultwarden (объект NEXTCLOUD: поле dbpassword или пароль). См. docs/backup/proxmox-phase1-backup.md +PG_ENV_ARGS="" +BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" +if [ -f "$BW_MASTER_PASSWORD_FILE" ] && command -v bw >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then + export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw 2>/dev/null) || true + if [ -n "${BW_SESSION:-}" ]; then + PGPASS=$(bw get item "NEXTCLOUD" 2>/dev/null | jq -r '.fields[] | select(.name=="dbpassword") | .value') + [ -z "$PGPASS" ] && PGPASS=$(bw get password "NEXTCLOUD" 2>/dev/null) + [ -n "$PGPASS" ] && PG_ENV_ARGS="-e PGPASSWORD=$PGPASS" + fi +fi +pct exec $CT_ID -- docker exec $PG_ENV_ARGS "$PG_CONTAINER" pg_dump -U nextcloud nextcloud 2>"$ERR" | gzip > "$OUTPUT" -if [ -s "$OUTPUT" ]; then - echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))" +SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0) +if [ -s "$OUTPUT" ] && [ "$SIZE_BYTES" -ge "$MIN_BACKUP_BYTES" ]; then + echo "Создан: $OUTPUT ($(du --apparent-size -h "$OUTPUT" | cut -f1))" + # Проверка: несжатый размер дампа (2GB БД на диске → 200–600MB SQL нормально: индексы не в дампе, потом gzip) + if [ "${VERIFY_BACKUP:-0}" = "1" ]; then + UNCOMPRESSED=$(gunzip -c "$OUTPUT" 2>/dev/null | wc -c) + UNCOMPRESSED_MB=$(( UNCOMPRESSED / 1024 / 1024 )) +echo "Несжатый размер дампа: ${UNCOMPRESSED_MB} MB (${UNCOMPRESSED} B)" + TABLES_IN_DUMP=$(gunzip -c "$OUTPUT" 2>/dev/null | grep -c '^CREATE TABLE ' || true) + echo "Таблиц в дампе: $TABLES_IN_DUMP" + fi else - echo "Ошибка: дамп пустой или контейнер недоступен." + echo "Ошибка: дамп пустой или слишком мал (${SIZE_BYTES} байт). Проверьте контейнер $PG_CONTAINER и пароль БД в Vaultwarden (NEXTCLOUD)." + [ -s "$ERR" ] && cat "$ERR" >&2 rm -f "$OUTPUT" exit 1 fi find "$BACKUP_DIR" -name 'nextcloud-db-*.sql.gz' -mtime +$RETENTION_DAYS -delete + +NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}" +if [ -x "$NOTIFY_SCRIPT" ]; then + SIZE=$(du --apparent-size -h "$OUTPUT" | cut -f1) + SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0) + BODY="Резервное копирование завершено. +Объекты: дамп БД Nextcloud (PostgreSQL). +Размер копии: ${SIZE}." + [ "$SIZE_BYTES" -lt 10240 ] 2>/dev/null && BODY="${BODY} +⚠️ Подозрительно малый размер — проверьте контейнер nextcloud-db-1 и наличие данных в БД." + "$NOTIFY_SCRIPT" "🗄️ Nextcloud (БД)" "$BODY" || true +fi diff --git a/scripts/backup-ct103-gitea-pgdump.sh b/scripts/backup-ct103-gitea-pgdump.sh index f05c030..fa81128 100644 --- a/scripts/backup-ct103-gitea-pgdump.sh +++ b/scripts/backup-ct103-gitea-pgdump.sh @@ -3,6 +3,7 @@ # Запускать на хосте Proxmox под root. Использует pct exec. # Результат: /mnt/backup/databases/ct103-gitea/gitea-db-YYYYMMDD-HHMM.sql.gz set -e +export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}" CT_ID=103 BACKUP_DIR="/mnt/backup/databases/ct103-gitea" @@ -14,18 +15,47 @@ if [ "$(id -u)" -ne 0 ]; then exit 1 fi +# Минимальный размер дампа (байт). Пустой gzip ≈ 20 байт — значит pg_dump не отдал данные. +MIN_BACKUP_BYTES=512 + mkdir -p "$BACKUP_DIR" DATE=$(date +%Y%m%d-%H%M) OUTPUT="$BACKUP_DIR/gitea-db-$DATE.sql.gz" +ERR=$(mktemp) +trap "rm -f '$ERR'" EXIT -pct exec $CT_ID -- docker exec "$PG_CONTAINER" pg_dump -U gitea gitea 2>/dev/null | gzip > "$OUTPUT" +# PGPASSWORD из Vaultwarden (объект GITEA, пароль). См. docs/backup/proxmox-phase1-backup.md +PG_ENV_ARGS="" +BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" +if [ -f "$BW_MASTER_PASSWORD_FILE" ] && command -v bw >/dev/null 2>&1; then + export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw 2>/dev/null) || true + if [ -n "${BW_SESSION:-}" ]; then + PGPASS=$(bw get password "GITEA" 2>/dev/null) + [ -n "$PGPASS" ] && PG_ENV_ARGS="-e PGPASSWORD=$PGPASS" + fi +fi +pct exec $CT_ID -- docker exec $PG_ENV_ARGS "$PG_CONTAINER" pg_dump -U gitea gitea 2>"$ERR" | gzip > "$OUTPUT" -if [ -s "$OUTPUT" ]; then - echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))" +SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0) +if [ -s "$OUTPUT" ] && [ "$SIZE_BYTES" -ge "$MIN_BACKUP_BYTES" ]; then + echo "Создан: $OUTPUT ($(du --apparent-size -h "$OUTPUT" | cut -f1))" else - echo "Ошибка: дамп пустой или контейнер недоступен." + echo "Ошибка: дамп пустой или слишком мал (${SIZE_BYTES} байт). Проверьте контейнер $PG_CONTAINER и пароль БД в Vaultwarden (GITEA)." + [ -s "$ERR" ] && cat "$ERR" >&2 rm -f "$OUTPUT" exit 1 fi find "$BACKUP_DIR" -name 'gitea-db-*.sql.gz' -mtime +$RETENTION_DAYS -delete + +NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}" +if [ -x "$NOTIFY_SCRIPT" ]; then + SIZE=$(du --apparent-size -h "$OUTPUT" | cut -f1) + SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0) + BODY="Резервное копирование завершено. +Объекты: дамп БД Gitea (PostgreSQL). +Размер копии: ${SIZE}." + [ "$SIZE_BYTES" -lt 10240 ] 2>/dev/null && BODY="${BODY} +⚠️ Подозрительно малый размер — проверьте контейнер gitea-db-1 и наличие данных в БД." + "$NOTIFY_SCRIPT" "🗄️ Gitea (БД)" "$BODY" || true +fi diff --git a/scripts/backup-ct104-pgdump.sh b/scripts/backup-ct104-pgdump.sh index b2f4328..9bdea1a 100644 --- a/scripts/backup-ct104-pgdump.sh +++ b/scripts/backup-ct104-pgdump.sh @@ -3,6 +3,7 @@ # Запускать на хосте Proxmox под root. Использует pct exec. # Результат: /mnt/backup/databases/ct104-paperless/paperless-db-YYYYMMDD-HHMM.sql.gz set -e +export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}" CT_ID=104 BACKUP_DIR="/mnt/backup/databases/ct104-paperless" @@ -14,18 +15,47 @@ if [ "$(id -u)" -ne 0 ]; then exit 1 fi +# Минимальный размер дампа (байт). Пустой gzip ≈ 20 байт — значит pg_dump не отдал данные. +MIN_BACKUP_BYTES=512 + mkdir -p "$BACKUP_DIR" DATE=$(date +%Y%m%d-%H%M) OUTPUT="$BACKUP_DIR/paperless-db-$DATE.sql.gz" +ERR=$(mktemp) +trap "rm -f '$ERR'" EXIT -pct exec $CT_ID -- docker exec "$PG_CONTAINER" pg_dump -U paperless paperless 2>/dev/null | gzip > "$OUTPUT" +# PGPASSWORD из Vaultwarden (объект PAPERLESS, пароль). См. docs/backup/proxmox-phase1-backup.md +PG_ENV_ARGS="" +BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" +if [ -f "$BW_MASTER_PASSWORD_FILE" ] && command -v bw >/dev/null 2>&1; then + export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw 2>/dev/null) || true + if [ -n "${BW_SESSION:-}" ]; then + PGPASS=$(bw get password "PAPERLESS" 2>/dev/null) + [ -n "$PGPASS" ] && PG_ENV_ARGS="-e PGPASSWORD=$PGPASS" + fi +fi +pct exec $CT_ID -- docker exec $PG_ENV_ARGS "$PG_CONTAINER" pg_dump -U paperless paperless 2>"$ERR" | gzip > "$OUTPUT" -if [ -s "$OUTPUT" ]; then - echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))" +SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0) +if [ -s "$OUTPUT" ] && [ "$SIZE_BYTES" -ge "$MIN_BACKUP_BYTES" ]; then + echo "Создан: $OUTPUT ($(du --apparent-size -h "$OUTPUT" | cut -f1))" else - echo "Ошибка: дамп пустой или контейнер недоступен." + echo "Ошибка: дамп пустой или слишком мал (${SIZE_BYTES} байт). Проверьте контейнер $PG_CONTAINER и пароль БД в Vaultwarden (PAPERLESS)." + [ -s "$ERR" ] && cat "$ERR" >&2 rm -f "$OUTPUT" exit 1 fi find "$BACKUP_DIR" -name 'paperless-db-*.sql.gz' -mtime +$RETENTION_DAYS -delete + +NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}" +if [ -x "$NOTIFY_SCRIPT" ]; then + SIZE=$(du --apparent-size -h "$OUTPUT" | cut -f1) + SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0) + BODY="Резервное копирование завершено. +Объекты: дамп БД Paperless (PostgreSQL). +Размер копии: ${SIZE}." + [ "$SIZE_BYTES" -lt 10240 ] 2>/dev/null && BODY="${BODY} +⚠️ Подозрительно малый размер — проверьте контейнер paperless-db-1 и наличие данных в БД." + "$NOTIFY_SCRIPT" "🗄️ Paperless (БД)" "$BODY" || true +fi diff --git a/scripts/backup-ct105-vectors.sh b/scripts/backup-ct105-vectors.sh index 25b19bd..a1cab93 100644 --- a/scripts/backup-ct105-vectors.sh +++ b/scripts/backup-ct105-vectors.sh @@ -3,12 +3,15 @@ # Запускать на хосте Proxmox под root. Использует pct exec. # Результат: /mnt/backup/other/ct105-vectors/vectors-YYYYMMDD-HHMM.tar.gz set -e +export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}" CT_ID=105 -REMOTE_PATH="/home/rag-service/data/vectors" BACKUP_DIR="/mnt/backup/other/ct105-vectors" RETENTION_DAYS=14 +# Минимальный размер архива (байт). Пустой gzip ≈ 20 байт — каталог пуст или путь неверный. +MIN_BACKUP_BYTES=512 + if [ "$(id -u)" -ne 0 ]; then echo "Запускайте под root." exit 1 @@ -17,15 +20,31 @@ fi mkdir -p "$BACKUP_DIR" DATE=$(date +%Y%m%d-%H%M) OUTPUT="$BACKUP_DIR/vectors-$DATE.tar.gz" +ERR=$(mktemp) +trap "rm -f '$ERR'" EXIT -pct exec $CT_ID -- tar cf - -C /home/rag-service/data vectors 2>/dev/null | gzip > "$OUTPUT" +pct exec $CT_ID -- tar cf - -C /home/rag-service/data vectors 2>"$ERR" | gzip > "$OUTPUT" -if [ -s "$OUTPUT" ]; then - echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))" +SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0) +if [ -s "$OUTPUT" ] && [ "$SIZE_BYTES" -ge "$MIN_BACKUP_BYTES" ]; then + echo "Создан: $OUTPUT ($(du --apparent-size -h "$OUTPUT" | cut -f1))" else - echo "Ошибка: архив пустой или каталог недоступен." + echo "Ошибка: архив пустой или слишком мал (${SIZE_BYTES} байт). Проверьте /home/rag-service/data/vectors в CT $CT_ID." + [ -s "$ERR" ] && cat "$ERR" >&2 rm -f "$OUTPUT" exit 1 fi find "$BACKUP_DIR" -name 'vectors-*.tar.gz' -mtime +$RETENTION_DAYS -delete + +NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}" +if [ -x "$NOTIFY_SCRIPT" ]; then + SIZE=$(du --apparent-size -h "$OUTPUT" | cut -f1) + SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0) + BODY="Резервное копирование завершено. +Объекты: архив векторов RAG (CT 105, vectors.npz). +Размер копии: ${SIZE}." + [ "$SIZE_BYTES" -lt 10240 ] 2>/dev/null && BODY="${BODY} +⚠️ Подозрительно малый размер — проверьте /home/rag-service/data/vectors в CT 105." + "$NOTIFY_SCRIPT" "📐 Векторы RAG" "$BODY" || true +fi diff --git a/scripts/backup-etc-pve.sh b/scripts/backup-etc-pve.sh index 93af2ac..7852301 100644 --- a/scripts/backup-etc-pve.sh +++ b/scripts/backup-etc-pve.sh @@ -21,3 +21,12 @@ chmod 600 "$BACKUP_ROOT"/etc-pve-*.tar.gz "$BACKUP_ROOT"/etc-host-configs-*.tar. find "$BACKUP_ROOT" -name 'etc-pve-*.tar.gz' -mtime +$RETENTION_DAYS -delete find "$BACKUP_ROOT" -name 'etc-host-configs-*.tar.gz' -mtime +$RETENTION_DAYS -delete + +NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}" +if [ -x "$NOTIFY_SCRIPT" ]; then + SIZE=$(du -sh "$BACKUP_ROOT" 2>/dev/null | cut -f1) || true + BODY="Резервное копирование завершено. +Объекты: архивы /etc/pve, конфиги сети (interfaces, hosts, resolv.conf). +Размер копии: ${SIZE:-—}." + "$NOTIFY_SCRIPT" "⚙️ Конфиги хоста" "$BODY" || true +fi diff --git a/scripts/backup-immich-photos.sh b/scripts/backup-immich-photos.sh index 4fca34e..161c9ed 100644 --- a/scripts/backup-immich-photos.sh +++ b/scripts/backup-immich-photos.sh @@ -19,3 +19,12 @@ mkdir -p "$BACKUP_PATH" rsync -az --timeout=3600 \ --exclude=".stfolder" \ "$VM_SSH:$REMOTE_PATH/" "$BACKUP_PATH/" + +NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}" +if [ -x "$NOTIFY_SCRIPT" ]; then + SIZE=$(du -sh "$BACKUP_PATH" 2>/dev/null | cut -f1) || true + BODY="Резервное копирование завершено. +Объекты: библиотека фото Immich (rsync с VM 200). +Размер копии: ${SIZE:-—}." + "$NOTIFY_SCRIPT" "📷 Фото Immich (rsync)" "$BODY" || true +fi diff --git a/scripts/backup-restic-yandex-photos.sh b/scripts/backup-restic-yandex-photos.sh index 5d46416..ae870ae 100644 --- a/scripts/backup-restic-yandex-photos.sh +++ b/scripts/backup-restic-yandex-photos.sh @@ -1,46 +1,54 @@ #!/bin/bash # Выгрузка только /mnt/backup/photos в Yandex Object Storage (S3) через restic. # Тот же репозиторий, что и backup-restic-yandex.sh; фото вынесены в отдельный снимок (больше всего данных). -# Запускать на хосте Proxmox под root. Требуется тот же /root/.restic-yandex.env и /root/.restic-password. -# Cron: 0 1 * * * (01:00). +# Запускать на хосте Proxmox под root. Секреты из Vaultwarden (объект RESTIC), файл /root/.bw-master (chmod 600). +# Cron: 10 4 * * * (04:10, после основного restic в 04:00). set -e -ENV_FILE="/root/.restic-yandex.env" BACKUP_PATH="/mnt/backup/photos" +# Время запуска (для логов и уведомлений) +START_TS=$(date +%s) +START_HUMAN=$(date '+%Y-%m-%d %H:%M:%S') if [ "$(id -u)" -ne 0 ]; then echo "Запускайте под root." exit 1 fi -if [ ! -f "$ENV_FILE" ]; then - echo "Нет файла $ENV_FILE. Скопируйте restic-yandex-env.example и задайте ключи." +# Секреты из Vaultwarden (объект RESTIC) +BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" +if [ ! -f "$BW_MASTER_PASSWORD_FILE" ]; then + echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE (chmod 600). См. docs/backup/proxmox-phase1-backup.md" exit 1 fi - -# shellcheck source=/dev/null -source "$ENV_FILE" - +if ! command -v bw >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then + echo "Установите bw (Bitwarden CLI) и jq для получения секретов из Vaultwarden." + exit 1 +fi +export BW_SESSION +BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw 2>/dev/null) || { echo "Не удалось разблокировать Vaultwarden."; exit 1; } +RESTIC_ITEM=$(bw get item "RESTIC" 2>/dev/null) || { echo "Не найден объект RESTIC в Vaultwarden."; exit 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 if [ -z "${!var}" ]; then - echo "В $ENV_FILE не задано: $var" + echo "В Vaultwarden (RESTIC) не задано поле для $var" exit 1 fi done - -if [ -z "$RESTIC_PASSWORD_FILE" ]; then - RESTIC_PASSWORD_FILE="/root/.restic-password" -fi -if [ ! -f "$RESTIC_PASSWORD_FILE" ]; then - echo "Файл с паролем репозитория не найден: $RESTIC_PASSWORD_FILE" - exit 1 -fi - -export AWS_ACCESS_KEY_ID -export AWS_SECRET_ACCESS_KEY +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 -export RESTIC_REPOSITORY -export AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-ru-central1}" if ! command -v restic >/dev/null 2>&1; then echo "restic не установлен. Установите: apt install restic." @@ -53,7 +61,8 @@ if [ ! -d "$BACKUP_PATH" ]; then fi echo "Restic backup (photos): $BACKUP_PATH -> $RESTIC_REPOSITORY" -restic backup "$BACKUP_PATH" --quiet +# Показываем прогресс restic (без --quiet), чтобы был виден ход бэкапа +restic backup "$BACKUP_PATH" echo "Restic forget (retention 3 daily, 2 weekly, 2 monthly)..." restic forget --keep-daily 3 --keep-weekly 2 --keep-monthly 2 --prune --quiet @@ -61,4 +70,42 @@ restic forget --keep-daily 3 --keep-weekly 2 --keep-monthly 2 --prune --quiet echo "Restic prune..." restic prune --quiet +# Время окончания и длительность +END_TS=$(date +%s) +END_HUMAN=$(date '+%Y-%m-%d %H:%M:%S') +DURATION_SEC=$(( END_TS - START_TS )) +if [ "$DURATION_SEC" -lt 0 ] 2>/dev/null; then + DURATION_SEC=0 +fi +DUR_MIN=$(( DURATION_SEC / 60 )) +DUR_SEC=$(( DURATION_SEC % 60 )) + echo "Restic photos backup done." +echo "Время запуска: $START_HUMAN" +echo "Время завершения: $END_HUMAN" +echo "Длительность: ${DUR_MIN} мин ${DUR_SEC} сек" + +# Уведомление в Telegram (шлюз тихо выходит, если конфига нет) +NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}" +if [ -x "$NOTIFY_SCRIPT" ]; then + STATS=$(restic stats latest 2>/dev/null) || true + FILES=$(echo "$STATS" | grep "Total File Count" | sed 's/.*:[[:space:]]*//') + SIZE=$(echo "$STATS" | grep "Total Size" | sed 's/.*:[[:space:]]*//') + if [ -n "$FILES" ] && [ -n "$SIZE" ]; then + BODY="Резервное копирование завершено. +Объекты: снимок /mnt/backup/photos в Yandex. Файлов в снимке: $FILES. +Размер копии: ${SIZE}. +Время запуска: ${START_HUMAN}. +Время завершения: ${END_HUMAN}. +Длительность: ${DUR_MIN} мин ${DUR_SEC} сек." + "$NOTIFY_SCRIPT" "📷 Restic Yandex (photos)" "$BODY" || true + else + BODY="Резервное копирование завершено. +Объекты: снимок /mnt/backup/photos в Yandex. +Размер копии: — (stats недоступны). +Время запуска: ${START_HUMAN}. +Время завершения: ${END_HUMAN}. +Длительность: ${DUR_MIN} мин ${DUR_SEC} сек." + "$NOTIFY_SCRIPT" "📷 Restic Yandex (photos)" "$BODY" || true + fi +fi diff --git a/scripts/backup-restic-yandex.sh b/scripts/backup-restic-yandex.sh index c744e04..7ed18e3 100644 --- a/scripts/backup-restic-yandex.sh +++ b/scripts/backup-restic-yandex.sh @@ -2,12 +2,15 @@ # Выгрузка /mnt/backup в Yandex Object Storage (S3) через restic (без каталога photos). # Фото бэкапятся отдельно: backup-restic-yandex-photos.sh. # Запускать на хосте Proxmox под root. -# Перед первым запуском: установить restic, создать /root/.restic-yandex.env и /root/.restic-password, выполнить restic init. +# Секреты: из Vaultwarden (объект RESTIC). Требуется файл с мастер-паролем: /root/.bw-master (chmod 600). +# Перед первым запуском: установить 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 -ENV_FILE="/root/.restic-yandex.env" BACKUP_PATH="/mnt/backup" +# Время запуска (для логов и уведомлений) +START_TS=$(date +%s) +START_HUMAN=$(date '+%Y-%m-%d %H:%M:%S') # Исключаем служебные каталоги и photos (фото — отдельный бэкап) EXCLUDE_OPTS=(--exclude="$BACKUP_PATH/lost+found" --exclude="$BACKUP_PATH/photos") @@ -16,34 +19,40 @@ if [ "$(id -u)" -ne 0 ]; then exit 1 fi -if [ ! -f "$ENV_FILE" ]; then - echo "Нет файла $ENV_FILE. Скопируйте restic-yandex-env.example и задайте ключи." +# Секреты из Vaultwarden (объект RESTIC) +BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" +if [ ! -f "$BW_MASTER_PASSWORD_FILE" ]; then + echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE (chmod 600). См. docs/backup/proxmox-phase1-backup.md" exit 1 fi - -# shellcheck source=/dev/null -source "$ENV_FILE" - +if ! command -v bw >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then + echo "Установите bw (Bitwarden CLI) и jq для получения секретов из Vaultwarden." + exit 1 +fi +export BW_SESSION +BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw 2>/dev/null) || { echo "Не удалось разблокировать Vaultwarden. Проверьте мастер-пароль и доступ к vault.katykhin.ru."; exit 1; } +RESTIC_ITEM=$(bw get item "RESTIC" 2>/dev/null) || { echo "Не найден объект RESTIC в Vaultwarden."; exit 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 if [ -z "${!var}" ]; then - echo "В $ENV_FILE не задано: $var" + echo "В Vaultwarden (RESTIC) не задано поле для $var" exit 1 fi done - -if [ -z "$RESTIC_PASSWORD_FILE" ]; then - RESTIC_PASSWORD_FILE="/root/.restic-password" -fi -if [ ! -f "$RESTIC_PASSWORD_FILE" ]; then - echo "Файл с паролем репозитория не найден: $RESTIC_PASSWORD_FILE. Создайте его и выполните restic init." - exit 1 -fi - -export AWS_ACCESS_KEY_ID -export AWS_SECRET_ACCESS_KEY +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 -export RESTIC_REPOSITORY -export AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-ru-central1}" if ! command -v restic >/dev/null 2>&1; then echo "restic не установлен. Установите: apt install restic." @@ -51,7 +60,8 @@ if ! command -v restic >/dev/null 2>&1; then fi echo "Restic backup: $BACKUP_PATH (excl. photos) -> $RESTIC_REPOSITORY" -restic backup "$BACKUP_PATH" "${EXCLUDE_OPTS[@]}" --quiet +# Показываем прогресс restic (без --quiet), чтобы был виден ход бэкапа +restic backup "$BACKUP_PATH" "${EXCLUDE_OPTS[@]}" echo "Restic forget (retention 3 daily, 2 weekly, 2 monthly)..." restic forget --keep-daily 3 --keep-weekly 2 --keep-monthly 2 --prune --quiet @@ -59,4 +69,42 @@ restic forget --keep-daily 3 --keep-weekly 2 --keep-monthly 2 --prune --quiet echo "Restic prune..." restic prune --quiet +# Время окончания и длительность +END_TS=$(date +%s) +END_HUMAN=$(date '+%Y-%m-%d %H:%M:%S') +DURATION_SEC=$(( END_TS - START_TS )) +if [ "$DURATION_SEC" -lt 0 ] 2>/dev/null; then + DURATION_SEC=0 +fi +DUR_MIN=$(( DURATION_SEC / 60 )) +DUR_SEC=$(( DURATION_SEC % 60 )) + echo "Restic backup done." +echo "Время запуска: $START_HUMAN" +echo "Время завершения: $END_HUMAN" +echo "Длительность: ${DUR_MIN} мин ${DUR_SEC} сек" + +# Уведомление в Telegram (шлюз тихо выходит, если конфига нет) +NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}" +if [ -x "$NOTIFY_SCRIPT" ]; then + STATS=$(restic stats latest 2>/dev/null) || true + FILES=$(echo "$STATS" | grep "Total File Count" | sed 's/.*:[[:space:]]*//') + SIZE=$(echo "$STATS" | grep "Total Size" | sed 's/.*:[[:space:]]*//') + if [ -n "$FILES" ] && [ -n "$SIZE" ]; then + BODY="Резервное копирование завершено. +Объекты: снимок /mnt/backup в Yandex (без photos). Файлов в снимке: $FILES. +Размер копии: ${SIZE}. +Время запуска: ${START_HUMAN}. +Время завершения: ${END_HUMAN}. +Длительность: ${DUR_MIN} мин ${DUR_SEC} сек." + "$NOTIFY_SCRIPT" "☁️ Restic Yandex" "$BODY" || true + else + BODY="Резервное копирование завершено. +Объекты: снимок /mnt/backup в Yandex (без photos). +Размер копии: — (stats недоступны). +Время запуска: ${START_HUMAN}. +Время завершения: ${END_HUMAN}. +Длительность: ${DUR_MIN} мин ${DUR_SEC} сек." + "$NOTIFY_SCRIPT" "☁️ Restic Yandex" "$BODY" || true + fi +fi diff --git a/scripts/backup-vaultwarden-data.sh b/scripts/backup-vaultwarden-data.sh index fe55154..899cf2b 100644 --- a/scripts/backup-vaultwarden-data.sh +++ b/scripts/backup-vaultwarden-data.sh @@ -4,6 +4,7 @@ # Запускать на хосте Proxmox под root. Использует pct exec. # Результат: /mnt/backup/other/vaultwarden/vaultwarden-data-YYYYMMDD-HHMM.tar.gz set -e +export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}" CT_ID=103 REMOTE_PATH="/opt/docker/vaultwarden/data" @@ -15,19 +16,38 @@ if [ "$(id -u)" -ne 0 ]; then exit 1 fi +# Минимальный размер архива (байт). Пустой tar.gz ≈ 20 байт — значит каталог пуст или путь неверный. +MIN_BACKUP_BYTES=512 + mkdir -p "$BACKUP_DIR" DATE=$(date +%Y%m%d-%H%M) OUTPUT="$BACKUP_DIR/vaultwarden-data-$DATE.tar.gz" +ERR=$(mktemp) +trap "rm -f '$ERR'" EXIT -pct exec $CT_ID -- tar cf - -C /opt/docker/vaultwarden data 2>/dev/null | gzip > "$OUTPUT" +pct exec $CT_ID -- tar cf - -C /opt/docker/vaultwarden data 2>"$ERR" | gzip > "$OUTPUT" -if [ -s "$OUTPUT" ]; then +SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0) +if [ -s "$OUTPUT" ] && [ "$SIZE_BYTES" -ge "$MIN_BACKUP_BYTES" ]; then chmod 600 "$OUTPUT" - echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))" + echo "Создан: $OUTPUT ($(du --apparent-size -h "$OUTPUT" | cut -f1))" else - echo "Ошибка: архив пустой или каталог недоступен." + echo "Ошибка: архив пустой или слишком мал (${SIZE_BYTES} байт). Проверьте путь /opt/docker/vaultwarden/data в CT $CT_ID." + [ -s "$ERR" ] && cat "$ERR" >&2 rm -f "$OUTPUT" exit 1 fi find "$BACKUP_DIR" -name 'vaultwarden-data-*.tar.gz' -mtime +$RETENTION_DAYS -delete + +NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}" +if [ -x "$NOTIFY_SCRIPT" ]; then + SIZE=$(du --apparent-size -h "$OUTPUT" | cut -f1) + SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0) + BODY="Резервное копирование завершено. +Объекты: данные Vaultwarden (пароли). +Размер копии: ${SIZE}." + [ "$SIZE_BYTES" -lt 10240 ] 2>/dev/null && BODY="${BODY} +⚠️ Подозрительно малый размер — проверьте /opt/docker/vaultwarden/data в CT 103." + "$NOTIFY_SCRIPT" "🔐 Vaultwarden" "$BODY" || true +fi diff --git a/scripts/backup-vm200-pgdump.sh b/scripts/backup-vm200-pgdump.sh index d798081..828788f 100644 --- a/scripts/backup-vm200-pgdump.sh +++ b/scripts/backup-vm200-pgdump.sh @@ -30,7 +30,7 @@ else fi if [ -s "$OUTPUT" ]; then - echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))" + echo "Создан: $OUTPUT ($(du --apparent-size -h "$OUTPUT" | cut -f1))" else echo "Ошибка: дамп пустой или не создан." rm -f "$OUTPUT" @@ -39,3 +39,15 @@ fi # Удалить дампы старше RETENTION_DAYS find "$BACKUP_DIR" -name 'immich-db-*.sql.gz' -mtime +$RETENTION_DAYS -delete + +NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}" +if [ -x "$NOTIFY_SCRIPT" ]; then + SIZE=$(du --apparent-size -h "$OUTPUT" | cut -f1) + SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0) + BODY="Резервное копирование завершено. +Объекты: дамп БД Immich (PostgreSQL, VM 200). +Размер копии: ${SIZE}." + [ "$SIZE_BYTES" -lt 10240 ] 2>/dev/null && BODY="${BODY} +⚠️ Подозрительно малый размер — проверьте скрипт на VM 200 и наличие данных в БД." + "$NOTIFY_SCRIPT" "🗄️ Immich (БД)" "$BODY" || true +fi diff --git a/scripts/backup-vps-miran.sh b/scripts/backup-vps-miran.sh index 921860c..e4bb4af 100644 --- a/scripts/backup-vps-miran.sh +++ b/scripts/backup-vps-miran.sh @@ -71,3 +71,12 @@ if [ -f "$S3_ENV" ]; then else echo "Подсказка: для бэкапа S3 создайте $S3_ENV с S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET_NAME." fi + +NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}" +if [ -x "$NOTIFY_SCRIPT" ]; then + SIZE=$(du -sh "$BACKUP_ROOT" 2>/dev/null | cut -f1) || true + BODY="Резервное копирование завершено. +Объекты: БД, voice_users, S3 (telegram-helper-bot). +Размер копии: ${SIZE:-—}." + "$NOTIFY_SCRIPT" "🖥️ VPS Миран" "$BODY" || true +fi diff --git a/scripts/backup-vps-mtproto.sh b/scripts/backup-vps-mtproto.sh index ecd2237..3d9a525 100644 --- a/scripts/backup-vps-mtproto.sh +++ b/scripts/backup-vps-mtproto.sh @@ -37,6 +37,18 @@ ssh "${SSH_OPTS[@]}" "${VPS_USER}@${VPS_HOST}" "tar -chzf - -C / \ var/www/katykhin.store" > "$ARCHIVE" chmod 600 "$ARCHIVE" -echo "Бэкап MTProto (VPS DE): $ARCHIVE ($(du -h "$ARCHIVE" | cut -f1))" +echo "Бэкап MTProto (VPS DE): $ARCHIVE ($(du --apparent-size -h "$ARCHIVE" | cut -f1))" find "$BACKUP_ROOT" -name 'mtproto-config-*.tar.gz' -mtime +$RETENTION_DAYS -delete + +NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}" +if [ -x "$NOTIFY_SCRIPT" ]; then + SIZE=$(du --apparent-size -h "$ARCHIVE" | cut -f1) + SIZE_BYTES=$(stat -c%s "$ARCHIVE" 2>/dev/null || echo 0) + BODY="Резервное копирование завершено. +Объекты: конфиги MTProto, nginx, Let's Encrypt, сайт (VPS DE). +Размер копии: ${SIZE}." + [ "$SIZE_BYTES" -lt 1024 ] 2>/dev/null && BODY="${BODY} +⚠️ Подозрительно малый размер — проверьте SSH и наличие файлов на VPS." + "$NOTIFY_SCRIPT" "🌐 VPS MTProto (DE)" "$BODY" || true +fi diff --git a/scripts/notify-telegram.sh b/scripts/notify-telegram.sh new file mode 100644 index 0000000..30e8e36 --- /dev/null +++ b/scripts/notify-telegram.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# Единая точка отправки уведомлений в Telegram (шлюз). +# Вызывают скрипты бэкапов на хосте Proxmox. Позже тот же шлюз можно вызывать с VM 200 / VPS по SSH. +# Использование: notify-telegram.sh "Заголовок" "Текст сообщения" +# Секреты: из Vaultwarden (токен — пароль объекта HOME_BOT_TOKEN, chat_id — поле TELEGRAM_SELF_CHAT_ID объекта RESTIC). +# Файл с мастер-паролем: /root/.bw-master (chmod 600). Если его нет — тихо выходим с 0, не ломаем вызывающий скрипт. + +set -e + +TITLE="${1:-Notification}" +BODY="${2:-}" + +# Креды из Vaultwarden или из старого конфига (fallback) +TELEGRAM_BOT_TOKEN="" +TELEGRAM_CHAT_ID="" +BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" +if [ -f "$BW_MASTER_PASSWORD_FILE" ] && command -v bw >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then + BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw 2>/dev/null) || true + if [ -n "$BW_SESSION" ]; then + export BW_SESSION + TELEGRAM_BOT_TOKEN=$(bw get password "HOME_BOT_TOKEN" 2>/dev/null) || true + RESTIC_ITEM=$(bw get item "RESTIC" 2>/dev/null) || true + if [ -n "$RESTIC_ITEM" ]; then + TELEGRAM_CHAT_ID=$(echo "$RESTIC_ITEM" | jq -r '.fields[] | select(.name=="TELEGRAM_SELF_CHAT_ID") | .value' 2>/dev/null) || true + fi + fi +fi +if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then + ENV_FILE="${TELEGRAM_NOTIFY_ENV:-/root/.telegram-notify.env}" + if [ -f "$ENV_FILE" ]; then + # shellcheck source=/dev/null + source "$ENV_FILE" + fi +fi +if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then + exit 0 +fi + +if [ -z "$BODY" ]; then + TEXT="$TITLE" +else + TEXT="$TITLE + +$BODY" +fi + +URL="https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" +if [ -n "${TELEGRAM_DEBUG:-}" ]; then + curl -s -w "\nHTTP_CODE:%{http_code}\n" -X POST "$URL" \ + --data-urlencode "chat_id=$TELEGRAM_CHAT_ID" \ + --data-urlencode "text=$TEXT" \ + -d "disable_web_page_preview=true" \ + --max-time 10 +else + curl -sf -X POST "$URL" \ + --data-urlencode "chat_id=$TELEGRAM_CHAT_ID" \ + --data-urlencode "text=$TEXT" \ + -d "disable_web_page_preview=true" \ + --max-time 10 \ + >/dev/null 2>&1 || true +fi + +exit 0 diff --git a/scripts/notify-vzdump-success.sh b/scripts/notify-vzdump-success.sh new file mode 100644 index 0000000..ecc9b8e --- /dev/null +++ b/scripts/notify-vzdump-success.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# Проверяет каталог локальных vzdump за последние 2 часа и отправляет в Telegram сводку. +# Задание Proxmox Backup выполняется в 02:00; этот скрипт запускают по cron в 03:00. +# Использование: notify-vzdump-success.sh [путь_к_dump] +# По умолчанию: /mnt/backup/proxmox/dump/dump/ + +DUMP_DIR="${1:-/mnt/backup/proxmox/dump/dump}" +NOTIFY_SCRIPT="${NOTIFY_SCRIPT:-/root/scripts/notify-telegram.sh}" +# Файлы, изменённые за последние 120 минут (2 часа) +MAX_AGE_MIN=120 + +if [ ! -d "$DUMP_DIR" ]; then + exit 0 +fi + +if [ ! -x "$NOTIFY_SCRIPT" ]; then + exit 0 +fi + +# Список файлов vzdump, изменённых за последние MAX_AGE_MIN минут +RECENT=$(find "$DUMP_DIR" -maxdepth 1 -type f \( -name 'vzdump-*.tar.zst' -o -name 'vzdump-*.vma.zst' -o -name 'vzdump-*.vma' \) -mmin "-$MAX_AGE_MIN" 2>/dev/null) + +if [ -z "$RECENT" ]; then + exit 0 +fi + +COUNT=$(echo "$RECENT" | grep -c . 2>/dev/null || echo 0) +[ "$COUNT" -eq 0 ] && exit 0 + +TOTAL_BYTES=$(echo "$RECENT" | while read -r f; do stat -c %s "$f" 2>/dev/null; done | awk '{s+=$1} END {print s+0}') +[ -z "$TOTAL_BYTES" ] && TOTAL_BYTES=0 + +# Размер в ГБ (округление до 2 знаков; если bc нет — целое число) +TOTAL_GB=$(echo "scale=2; $TOTAL_BYTES / 1024 / 1024 / 1024" | bc 2>/dev/null) +[ -z "$TOTAL_GB" ] && TOTAL_GB="$((TOTAL_BYTES / 1024 / 1024 / 1024))" + +# Время последнего изменения (последний записанный файл = время завершения бэкапа) +LATEST_MTIME=$(echo "$RECENT" | while read -r f; do stat -c %Y "$f" 2>/dev/null; done | sort -n | tail -1) +FINISH_TIME="" +[ -n "$LATEST_MTIME" ] && FINISH_TIME=$(date -d "@$LATEST_MTIME" +%H:%M 2>/dev/null) || true + +BODY="Резервное копирование завершено. +Объекты: локальный vzdump (LXC/VM). Контейнеров/ВМ: $COUNT. +Размер копии: ${TOTAL_GB} ГБ." +[ -n "$FINISH_TIME" ] && BODY="${BODY} +Время завершения: ${FINISH_TIME}." +"$NOTIFY_SCRIPT" "💾 Backup local" "$BODY" || true +exit 0 diff --git a/scripts/restore-one-vzdump-from-restic.sh b/scripts/restore-one-vzdump-from-restic.sh index 3323c39..1e99b62 100644 --- a/scripts/restore-one-vzdump-from-restic.sh +++ b/scripts/restore-one-vzdump-from-restic.sh @@ -2,6 +2,7 @@ # Восстановление одного файла vzdump из restic (Yandex S3) через mount. # Не выкачивает весь репозиторий — подгружаются только нужные данные для выбранного файла. # Запускать на хосте Proxmox под root. Требуется FUSE (restic mount). +# Секреты из Vaultwarden (объект RESTIC), файл /root/.bw-master (chmod 600). # # Использование: # restore-one-vzdump-from-restic.sh [SNAPSHOT] [ПУТЬ_В_СНИМКЕ] [КУДА_СОХРАНИТЬ] @@ -14,9 +15,7 @@ # Список файлов в снимке: restic ls SNAPSHOT set -e -ENV_FILE="${ENV_FILE:-/root/.restic-yandex.env}" MOUNT_DIR="${MOUNT_DIR:-/mnt/backup/restic-mount}" -RESTIC_PASSWORD_FILE="${RESTIC_PASSWORD_FILE:-/root/.restic-password}" SNAPSHOT="${1:-latest}" # Путь к файлу внутри снимка (как в restic ls) — бэкапим /mnt/backup, пути вида /mnt/backup/proxmox/dump/dump/vzdump-lxc-107-... @@ -28,22 +27,40 @@ if [ "$(id -u)" -ne 0 ]; then exit 1 fi -if [ ! -f "$ENV_FILE" ]; then - echo "Нет файла $ENV_FILE. Скопируйте restic-yandex-env.example и задайте ключи." +# Секреты из Vaultwarden (объект RESTIC) +BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" +if [ ! -f "$BW_MASTER_PASSWORD_FILE" ]; then + echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE (chmod 600). См. docs/backup/proxmox-phase1-backup.md" exit 1 fi - -# shellcheck source=/dev/null -source "$ENV_FILE" +if ! command -v bw >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then + echo "Установите bw (Bitwarden CLI) и jq для получения секретов из Vaultwarden." + exit 1 +fi +export BW_SESSION +BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw 2>/dev/null) || { echo "Не удалось разблокировать Vaultwarden."; exit 1; } +RESTIC_ITEM=$(bw get item "RESTIC" 2>/dev/null) || { echo "Не найден объект RESTIC в Vaultwarden."; exit 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 if [ -z "${!var}" ]; then - echo "В $ENV_FILE не задано: $var" + echo "В Vaultwarden (RESTIC) не задано поле для $var" exit 1 fi done -export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY RESTIC_REPOSITORY +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 -export AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-ru-central1}" if ! command -v restic >/dev/null 2>&1; then echo "restic не установлен. Установите: apt install restic." @@ -80,7 +97,7 @@ fi echo "Монтируем репозиторий в $MOUNT_DIR ..." restic mount "$MOUNT_DIR" & MOUNT_PID=$! -trap 'kill $MOUNT_PID 2>/dev/null; fusermount -u "$MOUNT_DIR" 2>/dev/null; exit' EXIT INT TERM +trap 'rm -f "$RESTIC_PASSWORD_FILE"; kill $MOUNT_PID 2>/dev/null; fusermount -u "$MOUNT_DIR" 2>/dev/null; exit' EXIT INT TERM # В смонтированном репо файлы снимка лежат в ids//<путь> SOURCE_FILE="$MOUNT_DIR/ids/$SNAPSHOT_ID/$FILE_IN_SNAPSHOT" diff --git a/scripts/telegram-notify.env.example b/scripts/telegram-notify.env.example new file mode 100644 index 0000000..dd625ac --- /dev/null +++ b/scripts/telegram-notify.env.example @@ -0,0 +1,13 @@ +# Пример конфига для уведомлений в Telegram (скрипт notify-telegram.sh). +# Скопируйте на хост Proxmox в /root/.telegram-notify.env и подставьте свои значения. +# +# Как получить: +# 1. Создать бота: в Telegram написать @BotFather, команда /newbot, получить токен. +# 2. Узнать chat_id: написать боту любое сообщение, затем открыть в браузере: +# https://api.telegram.org/bot/getUpdates +# В ответе в updates[].message.chat.id — ваш chat_id (число или отрицательное для групп). +# +# На хосте: cp telegram-notify.env.example /root/.telegram-notify.env && chmod 600 /root/.telegram-notify.env + +TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz +TELEGRAM_CHAT_ID=123456789