Add notification feature to backup scripts for various services
Enhance backup scripts for Nextcloud, Gitea, Paperless, Vaultwarden, Immich, and VPS configurations by adding Telegram notifications upon completion. Include details such as backup size and objects backed up. Update backup documentation to reflect these changes and ensure clarity on backup processes and retention policies.
This commit is contained in:
@@ -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 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).
|
Если восстанавливаем поверх существующего контейнера: сначала удалить его (`pct destroy 107`), затем в команде указать тот же VMID (107). Доп. опции: `pct create --help` (режим restore).
|
||||||
|
|
||||||
- **VM (KVM)** — порядок аргументов: сначала архив, потом VMID:
|
- **VM (KVM)** — порядок аргументов: сначала архив, потом VMID:
|
||||||
```bash
|
```bash
|
||||||
qm restore /mnt/backup/proxmox/dump/dump/vzdump-qemu-200-YYYY_MM_DD-HH_MM_SS.vma.zst 200 --storage local-lvm
|
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):**
|
**После восстановления (пример для LXC):**
|
||||||
|
|
||||||
- Если восстановили в новый слот (например 999) и не нужен конфликт IP с оригиналом — сменить IP:
|
- Если восстановили в новый слот (например 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 start 999` (LXC) или `qm start 200` (VM).
|
||||||
- Проверка: пинг, консоль (`pct exec 999 -- bash`), при необходимости сервисы и порты внутри контейнера.
|
- Проверка: пинг, консоль (`pct exec 999 -- bash`), при необходимости сервисы и порты внутри контейнера.
|
||||||
|
|
||||||
@@ -99,17 +99,17 @@
|
|||||||
**Когда нужно:** переустановка Proxmox или потеря конфигов узла (при этом диск с бэкапами доступен).
|
**Когда нужно:** переустановка Proxmox или потеря конфигов узла (при этом диск с бэкапами доступен).
|
||||||
Если конфигов нет локально, но есть в Yandex — см. раздел **Восстановление из restic** → «Восстановление конфигов хоста (/etc/pve)».
|
Если конфигов нет локально, но есть в Yandex — см. раздел **Восстановление из restic** → «Восстановление конфигов хоста (/etc/pve)».
|
||||||
|
|
||||||
1. Скопировать нужный архив с хоста, например:
|
1. Скопировать нужный архив с хоста, например:
|
||||||
`etc-pve-YYYYMMDD-HHMM.tar.gz` и/или `etc-host-configs-YYYYMMDD-HHMM.tar.gz` из `/mnt/backup/proxmox/etc-pve/`.
|
`etc-pve-YYYYMMDD-HHMM.tar.gz` и/или `etc-host-configs-YYYYMMDD-HHMM.tar.gz` из `/mnt/backup/proxmox/etc-pve/`.
|
||||||
2. **Восстановление /etc/pve** (на переустановленном хосте, от root):
|
2. **Восстановление /etc/pve** (на переустановленном хосте, от root):
|
||||||
```bash
|
```bash
|
||||||
tar -xzf etc-pve-YYYYMMDD-HHMM.tar.gz -C /
|
tar -xzf etc-pve-YYYYMMDD-HHMM.tar.gz -C /
|
||||||
```
|
```
|
||||||
При одномузловой установке обычно достаточно распаковать в `/`. При кластере — аккуратно с нодами и storage.
|
При одномузловой установке обычно достаточно распаковать в `/`. При кластере — аккуратно с нодами и storage.
|
||||||
3. **Восстановление конфигов сети/хоста** (interfaces, hosts, resolv.conf):
|
3. **Восстановление конфигов сети/хоста** (interfaces, hosts, resolv.conf):
|
||||||
```bash
|
```bash
|
||||||
tar -xzf etc-host-configs-YYYYMMDD-HHMM.tar.gz -C /
|
tar -xzf etc-host-configs-YYYYMMDD-HHMM.tar.gz -C /
|
||||||
```
|
```
|
||||||
При необходимости поправить под текущее железо (интерфейсы, IP) и перезапустить сеть.
|
При необходимости поправить под текущее железо (интерфейсы, IP) и перезапустить сеть.
|
||||||
|
|
||||||
После восстановления конфигов — заново добавить storage для бэкапов (если переустанавливали с нуля) и восстанавливать гостей из vzdump по шагу 1.
|
После восстановления конфигов — заново добавить storage для бэкапов (если переустанавливали с нуля) и восстанавливать гостей из vzdump по шагу 1.
|
||||||
@@ -120,17 +120,14 @@
|
|||||||
|
|
||||||
**Когда нужно:** повреждение или потеря базы Immich при рабочей ВМ (образ VM можно не трогать, восстанавливаем только БД).
|
**Когда нужно:** повреждение или потеря базы Immich при рабочей ВМ (образ VM можно не трогать, восстанавливаем только БД).
|
||||||
|
|
||||||
1. Скопировать нужный дамп на VM 200, например:
|
1. Скопировать нужный дамп на VM 200, например:
|
||||||
`immich-db-YYYYMMDD-HHMM.sql.gz` из `/mnt/backup/databases/vm200-immich/`.
|
`immich-db-YYYYMMDD-HHMM.sql.gz` из `/mnt/backup/databases/vm200-immich/`.
|
||||||
2. На VM 200 (ssh admin@192.168.1.200):
|
2. На VM 200 (ssh [admin@192.168.1.200](mailto:admin@192.168.1.200)):
|
||||||
```bash
|
```bash
|
||||||
cd /opt/immich
|
cd /opt/immich
|
||||||
gunzip -c /path/to/immich-db-YYYYMMDD-HHMM.sql.gz | docker compose exec -T database psql -U <DB_USERNAME> -d <DB_DATABASE_NAME>
|
gunzip -c /path/to/immich-db-YYYYMMDD-HHMM.sql.gz | docker compose exec -T database psql -U <DB_USERNAME> -d <DB_DATABASE_NAME>
|
||||||
```
|
```
|
||||||
Или распаковать `.sql.gz`, затем:
|
Или распаковать `.sql.gz`, затем:
|
||||||
```bash
|
|
||||||
docker compose exec -T database psql -U <DB_USERNAME> -d <DB_DATABASE_NAME> < backup.sql
|
|
||||||
```
|
|
||||||
`<DB_USERNAME>` и `<DB_DATABASE_NAME>` — из `/opt/immich/.env` (обычно `postgres` и `immich`).
|
`<DB_USERNAME>` и `<DB_DATABASE_NAME>` — из `/opt/immich/.env` (обычно `postgres` и `immich`).
|
||||||
|
|
||||||
Перед восстановлением лучше остановить приложение Immich (или как минимум не писать в БД). При полной пересоздании БД — очистить каталог данных PostgreSQL в контейнере и затем загрузить дамп.
|
Перед восстановлением лучше остановить приложение Immich (или как минимум не писать в БД). При полной пересоздании БД — очистить каталог данных PostgreSQL в контейнере и затем загрузить дамп.
|
||||||
@@ -178,18 +175,20 @@ rsync -av /mnt/backup/photos/library/ admin@192.168.1.200:/mnt/data/library/
|
|||||||
**Когда нужно:** потеря данных на VPS или перенос бота на другой хост.
|
**Когда нужно:** потеря данных на VPS или перенос бота на другой хост.
|
||||||
|
|
||||||
В бэкапе есть:
|
В бэкапе есть:
|
||||||
|
|
||||||
- **БД:** `/mnt/backup/vps/miran/db/tg-bot-database-YYYYMMDD.db` — копии SQLite.
|
- **БД:** `/mnt/backup/vps/miran/db/tg-bot-database-YYYYMMDD.db` — копии SQLite.
|
||||||
- **Голосовые сообщения:** `/mnt/backup/vps/miran/voice_users/` — каталог .ogg.
|
- **Голосовые сообщения:** `/mnt/backup/vps/miran/voice_users/` — каталог .ogg.
|
||||||
- **S3 (контент бота):** `/mnt/backup/vps/miran/s3/` — полная копия бакета (photos, videos, voice и т.д.).
|
- **S3 (контент бота):** `/mnt/backup/vps/miran/s3/` — полная копия бакета (photos, videos, voice и т.д.).
|
||||||
|
|
||||||
**Восстановление на VPS:**
|
**Восстановление на 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`
|
1. Скопировать выбранный файл БД на VPS:
|
||||||
2. Восстановить `voice_users`:
|
`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`
|
||||||
`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/`
|
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).
|
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 и сайта-заглушки на другой хост.
|
**Когда нужно:** потеря конфигов на VPS 185.103.253.99 или перенос MTProto и сайта-заглушки на другой хост.
|
||||||
|
|
||||||
В архиве `mtproto-config-YYYYMMDD-HHMM.tar.gz` из `/mnt/backup/vps/mtproto-germany/` лежат:
|
В архиве `mtproto-config-YYYYMMDD-HHMM.tar.gz` из `/mnt/backup/vps/mtproto-germany/` лежат:
|
||||||
|
|
||||||
- **mtg:** `etc/systemd/system/mtg.service` (в т.ч. секрет и cloak-port).
|
- **mtg:** `etc/systemd/system/mtg.service` (в т.ч. секрет и cloak-port).
|
||||||
- **nginx:** `etc/nginx/sites-available/`, `etc/nginx/sites-enabled/` (конфиг для katykhin.store на порту 993).
|
- **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`.
|
- **Let's Encrypt:** `etc/letsencrypt/live/katykhin.store/`, `archive/katykhin.store/`, `renewal/katykhin.store.conf`.
|
||||||
- **Сайт:** `var/www/katykhin.store/`.
|
- **Сайт:** `var/www/katykhin.store/`.
|
||||||
|
|
||||||
**Восстановление на VPS (от root):** скопировать архив на сервер и распаковать в корень:
|
**Восстановление на VPS (от root):** скопировать архив на сервер и распаковать в корень:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scp /mnt/backup/vps/mtproto-germany/mtproto-config-YYYYMMDD-HHMM.tar.gz root@185.103.253.99:/tmp/
|
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 /"
|
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)).
|
После распаковки: `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`). Восстановление — создание новой ВМ с теми же параметрами и перенос данных.
|
VM 200 **не входит** в задание vzdump (образ ~380 ГБ, не помещается в политику 7 копий). В бэкапе есть: **конфиг ВМ** (в архивах `/etc/pve`), **БД** (pg_dump), **фото** (rsync в `photos/library`). Восстановление — создание новой ВМ с теми же параметрами и перенос данных.
|
||||||
|
|
||||||
**Что есть после восстановления хоста:**
|
**Что есть после восстановления хоста:**
|
||||||
|
|
||||||
- Из бэкапа `etc-pve`: файл `/etc/pve/qemu-server/200.conf` — полное описание ВМ (CPU, память, диски, **hostpci для GPU**, сеть). Его можно использовать как образец при создании новой ВМ.
|
- Из бэкапа `etc-pve`: файл `/etc/pve/qemu-server/200.conf` — полное описание ВМ (CPU, память, диски, **hostpci для GPU**, сеть). Его можно использовать как образец при создании новой ВМ.
|
||||||
- Дамп БД: `/mnt/backup/databases/vm200-immich/immich-db-*.sql.gz`.
|
- Дамп БД: `/mnt/backup/databases/vm200-immich/immich-db-*.sql.gz`.
|
||||||
- Фото: `/mnt/backup/photos/library/`.
|
- Фото: `/mnt/backup/photos/library/`.
|
||||||
|
|
||||||
**Ключевые параметры VM 200** (если восстанавливать вручную без конфига):
|
**Ключевые параметры VM 200** (если восстанавливать вручную без конфига):
|
||||||
|
|
||||||
- **Ресурсы:** 3 ядра, 10 GB RAM.
|
- **Ресурсы:** 3 ядра, 10 GB RAM.
|
||||||
- **GPU:** проброс видеокарты (hostpci) — в Proxmox: Hardware → Add → PCI Device → выбрать VGA/NVIDIA, поставить «All Functions» и «ROM-Bar» при необходимости. В конфиге это выглядит как `hostpci0: 0000:xx:00.0` и т.п.
|
- **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.
|
- **Сеть:** статический IP 192.168.1.200/24, шлюз 192.168.1.1.
|
||||||
- **ОС:** Debian 13 (trixie), пользователь **admin**, SSH.
|
- **ОС:** Debian 13 (trixie), пользователь **admin**, SSH.
|
||||||
|
|
||||||
@@ -243,8 +247,8 @@ VM 200 **не входит** в задание vzdump (образ ~380 ГБ, н
|
|||||||
3. **Разметить второй диск** и смонтировать в `/mnt/data` (как в [container-200](../containers/container-200.md)).
|
3. **Разметить второй диск** и смонтировать в `/mnt/data` (как в [container-200](../containers/container-200.md)).
|
||||||
4. **Установить Docker**, склонировать/восстановить каталоги Immich: `/opt/immich/` (docker-compose.yml, .env — из своих заметок или копии; секреты из Vaultwarden).
|
4. **Установить Docker**, склонировать/восстановить каталоги Immich: `/opt/immich/` (docker-compose.yml, .env — из своих заметок или копии; секреты из Vaultwarden).
|
||||||
5. **Создать каталоги** `/mnt/data/library`, `/mnt/data/postgres` (и др. по .env).
|
5. **Создать каталоги** `/mnt/data/library`, `/mnt/data/postgres` (и др. по .env).
|
||||||
6. **Скопировать фото** с хоста бэкапов на ВМ:
|
6. **Скопировать фото** с хоста бэкапов на ВМ:
|
||||||
`rsync -av /mnt/backup/photos/library/ admin@192.168.1.200:/mnt/data/library/`
|
`rsync -av /mnt/backup/photos/library/ admin@192.168.1.200:/mnt/data/library/`
|
||||||
7. **Запустить только контейнер БД** (database), восстановить дамп (см. раздел 3 выше), затем поднять весь стек Immich.
|
7. **Запустить только контейнер БД** (database), восстановить дамп (см. раздел 3 выше), затем поднять весь стек Immich.
|
||||||
8. Проверить NPM (прокси на 192.168.1.200:2283), при необходимости заново включить ML и настройки в Immich.
|
8. Проверить NPM (прокси на 192.168.1.200:2283), при необходимости заново включить ML и настройки в Immich.
|
||||||
|
|
||||||
@@ -254,7 +258,7 @@ VM 200 **не входит** в задание vzdump (образ ~380 ГБ, н
|
|||||||
|
|
||||||
## Restic и Yandex
|
## 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`.
|
- Установлены **restic** и **FUSE** (для `restic mount`): `apt install restic fuse`.
|
||||||
- Восстановление делаем **на раздел с достаточным местом** (например `/mnt/backup/restore-...`), не в `/tmp`.
|
- Восстановление делаем **на раздел с достаточным местом** (например `/mnt/backup/restore-...`), не в `/tmp`.
|
||||||
|
|
||||||
@@ -272,10 +276,12 @@ VM 200 **не входит** в задание vzdump (образ ~380 ГБ, н
|
|||||||
|
|
||||||
В репозитории два вида снимков (различаются по полю **Paths** в `restic snapshots`):
|
В репозитории два вида снимков (различаются по полю **Paths** в `restic snapshots`):
|
||||||
|
|
||||||
| Paths в снимке | Откуда | Что внутри |
|
|
||||||
|------------------|--------|-----------|
|
| 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) |
|
| `/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**.
|
При восстановлении **конфигов, паролей, дампов БД, 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 ... |
|
| Один LXC/VM (vzdump) | /mnt/backup/proxmox/dump/dump/... | /mnt/backup | Скрипт mount + cp (см. ниже) |
|
||||||
| Vaultwarden (пароли) | /mnt/backup/other/vaultwarden/ | /mnt/backup | restic restore --path ... |
|
| Конфиги /etc/pve | /mnt/backup/proxmox/etc-pve/ | /mnt/backup | restic restore --path ... |
|
||||||
| Дампы БД | /mnt/backup/databases/... | /mnt/backup | restic restore --path ... |
|
| Vaultwarden (пароли) | /mnt/backup/other/vaultwarden/ | /mnt/backup | restic restore --path ... |
|
||||||
| VPS, other | /mnt/backup/vps/, other/ | /mnt/backup | restic restore --path ... |
|
| Дампы БД | /mnt/backup/databases/... | /mnt/backup | restic restore --path ... |
|
||||||
| Фото Immich | /mnt/backup/photos/ | **/mnt/backup/photos** | restic restore из снимка photos |
|
| 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** и копирование одного файла.
|
Чтобы не выкачивать весь репо, используется **mount** и копирование одного файла.
|
||||||
|
|
||||||
1. Узнать имя нужного архива в снимке (например CT 107):
|
1. Узнать имя нужного архива в снимке (например CT 107):
|
||||||
```bash
|
```bash
|
||||||
restic ls latest --path /mnt/backup/proxmox/dump/dump | grep vzdump-lxc-107
|
restic ls latest --path /mnt/backup/proxmox/dump/dump | grep vzdump-lxc-107
|
||||||
```
|
```
|
||||||
Использовать снимок с path `/mnt/backup` (не photos).
|
Использовать снимок с path `/mnt/backup` (не photos).
|
||||||
|
|
||||||
2. Запустить скрипт (он сам монтирует, копирует файл, размонтирует):
|
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
|
/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`.
|
Файл появится в `/mnt/backup/vzdump-lxc-107-....tar.zst`.
|
||||||
|
|
||||||
3. Восстановить контейнер из файла (как в разделе 1 выше):
|
3. Восстановить контейнер из файла (как в разделе 1 выше):
|
||||||
```bash
|
```bash
|
||||||
pct create 999 /mnt/backup/vzdump-lxc-107-....tar.zst --restore 1 --storage local-lvm
|
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 set 999 --net0 name=eth0,bridge=vmbr0,gw=192.168.1.1,ip=192.168.1.199/24,type=veth
|
||||||
pct start 999
|
pct start 999
|
||||||
```
|
```
|
||||||
|
|
||||||
Если скрипта нет — вручную: `restic mount /mnt/backup/restic-mount &`, подождать, скопировать из `.../restic-mount/ids/<SNAPSHOT_ID>/mnt/backup/proxmox/dump/dump/vzdump-lxc-107-....tar.zst` в нужное место, затем `fusermount -u /mnt/backup/restic-mount`.
|
Если скрипта нет — вручную: `restic mount /mnt/backup/restic-mount &`, подождать, скопировать из `.../restic-mount/ids/<SNAPSHOT_ID>/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`.
|
1. Выбрать снимок с path **/mnt/backup** (по дате): `restic snapshots`.
|
||||||
2. Восстановить только каталог etc-pve:
|
2. Восстановить только каталог etc-pve:
|
||||||
```bash
|
```bash
|
||||||
restic restore SNAPSHOT_ID --target /mnt/backup/restore-etc-pve --path /mnt/backup/proxmox/etc-pve
|
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`).
|
Файлы появятся в `/mnt/backup/restore-etc-pve/mnt/backup/proxmox/etc-pve/` (архивы `etc-pve-*.tar.gz`, `etc-host-configs-*.tar.gz`).
|
||||||
3. Распаковать нужный архив в корень (как в разделе 2 выше):
|
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-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 /
|
tar -xzf /mnt/backup/restore-etc-pve/mnt/backup/proxmox/etc-pve/etc-host-configs-YYYYMMDD-HHMM.tar.gz -C /
|
||||||
```
|
```
|
||||||
4. При необходимости поправить сеть и перезапустить сервисы.
|
4. При необходимости поправить сеть и перезапустить сервисы.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -348,9 +354,9 @@ restic snapshots
|
|||||||
|
|
||||||
1. Снимок с path **/mnt/backup**.
|
1. Снимок с path **/mnt/backup**.
|
||||||
2. Восстановить каталог vaultwarden:
|
2. Восстановить каталог vaultwarden:
|
||||||
```bash
|
```bash
|
||||||
restic restore SNAPSHOT_ID --target /mnt/backup/restore-vw --path /mnt/backup/other/vaultwarden
|
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`.
|
Результат: `/mnt/backup/restore-vw/mnt/backup/other/vaultwarden/vaultwarden-data-*.tar.gz`.
|
||||||
3. Скопировать архив на хост, откуда можно отправить на CT 103, либо распаковать во временный каталог и скопировать каталог `data/` на CT 103 в `/opt/docker/vaultwarden/` (остановив Vaultwarden). Детали — раздел 6 выше.
|
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, векторы RAG) из restic
|
||||||
|
|
||||||
- **VPS Миран / MTProto:**
|
- **VPS Миран / MTProto:**
|
||||||
`restic restore SNAPSHOT_ID --target /mnt/backup/restore-vps --path /mnt/backup/vps`
|
`restic restore SNAPSHOT_ID --target /mnt/backup/restore-vps --path /mnt/backup/vps`
|
||||||
Дальше — копировать нужные файлы на VPS по разделам 7–8.
|
Дальше — копировать нужные файлы на VPS по разделам 7–8.
|
||||||
|
|
||||||
- **Векторы RAG (ct105-vectors):**
|
- **Векторы RAG (ct105-vectors):**
|
||||||
`restic restore SNAPSHOT_ID --target /mnt/backup/restore-other --path /mnt/backup/other/ct105-vectors`
|
`restic restore SNAPSHOT_ID --target /mnt/backup/restore-other --path /mnt/backup/other/ct105-vectors`
|
||||||
Дальше — по разделу 9.
|
Дальше — по разделу 9.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Скрипты на хосте Proxmox
|
## Скрипты на хосте Proxmox
|
||||||
|
|
||||||
|
|
||||||
| Скрипт | Назначение | Cron |
|
| Скрипт | Назначение | Cron |
|
||||||
|--------|------------|------|
|
|--------|------------|------|
|
||||||
| `/root/scripts/backup-vps-miran.sh` | Бэкап VPS Миран: БД бота, voice_users, S3 (Miran) | 0 1 * * * |
|
| `/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-ct101-pgdump.sh` | Логический дамп БД Nextcloud из CT 101 | 15 1 * * * |
|
||||||
| `/root/scripts/backup-immich-photos.sh` | Копирование библиотеки фото Immich (rsync с VM 200) | 30 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-etc-pve.sh` | Бэкап /etc/pve и конфигов хоста | 15 2 * * * |
|
||||||
| `/root/scripts/backup-ct104-pgdump.sh` | Логический дамп БД Paperless из CT 104 | 30 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-vaultwarden-data.sh` | Копирование данных Vaultwarden (пароли) из CT 103 | 45 2 * * * |
|
||||||
| `/root/scripts/backup-ct103-gitea-pgdump.sh` | Логический дамп БД Gitea из CT 103 | 0 3 * * * |
|
| `/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-vm200-pgdump.sh` | Логический дамп БД Immich с VM 200 | 15 3 * * * |
|
||||||
| `/root/scripts/backup-ct105-vectors.sh` | Копирование векторов RAG (vectors.npz) из CT 105 | 30 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.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** оставлено свободным для плановой перезагрузки сервера.
|
Задание 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<TOKEN>/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) — общий план и принятые решения.
|
- [Стратегия бэкапов (фаза 1)](proxmox-phase1-backup.md) — общий план и принятые решения.
|
||||||
- [Архитектура](../architecture/architecture.md) — хост, IP, доступ.
|
- [Архитектура](../architecture/architecture.md) — хост, IP, доступ.
|
||||||
- [VM 200 (Immich)](../containers/container-200.md) — сервисы, пути, .env.
|
- [VM 200 (Immich)](../containers/container-200.md) — сервисы, пути, .env.
|
||||||
|
|
||||||
|
|||||||
@@ -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. Тестовое восстановление одного контейнера
|
### Шаг 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] Разметка: 1 ТБ на sdb1, ФС, монтирование в `/mnt/backup` (без LUKS). *(скрипт `scripts/backup-setup-sdb1-mount.sh`, каталоги созданы.)*
|
||||||
- [x] В Proxmox добавлен Storage для VZDump → `/mnt/backup/proxmox/dump`.
|
- [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).*
|
- [x] Настроена регулярная задача Backup: LXC (100–108), расписание ночь (02:00), retention задан. *VM 200 исключена из задания (образ ~380 ГБ); восстановление VM 200 — по инструкции «с нуля» в [backup-howto](backup-howto.md).*
|
||||||
- [ ] Проверен ручной запуск Backup now — файлы появляются в storage. *(рекомендуется проверить разово.)*
|
- [x] Проверен ручной запуск Backup now — файлы появляются в storage. *(рекомендуется проверить разово.)*
|
||||||
- [x] Настроен бэкап `/etc/pve` (скрипт + cron) → `/mnt/backup/proxmox/etc-pve`. *(backup-etc-pve.sh, 03:00, 30 дней.)*
|
- [x] Настроен бэкап `/etc/pve` (скрипт + cron) → `/mnt/backup/proxmox/etc-pve`. *(backup-etc-pve.sh, 02:15, 30 дней.)*
|
||||||
- [ ] Restic: cron на хосте, выгрузка нужных каталогов из `/mnt/backup` в Yandex S3, retention 7/4/6.
|
- [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.)*
|
||||||
- [ ] Yandex: ключи и endpoint зафиксированы, restic успешно пишет в бакет.
|
- [x] Yandex: ключи и endpoint зафиксированы в `/root/.restic-yandex.env`, restic пишет в бакет.
|
||||||
- [x] Vaultwarden развёрнут (CT 103).
|
- [x] Vaultwarden развёрнут (CT 103).
|
||||||
- [ ] Секреты перенесены в Vaultwarden. *(на усмотрение: root PVE, пароли БД, API и т.д.)*
|
- [ ] Секреты перенесены в 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] Выполнено тестовое восстановление одного контейнера (другой 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 и др.*
|
- [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, домены.
|
- [Архитектура и подключение](../architecture/architecture.md) — хосты, IP, домены.
|
||||||
- [Схема сети и зависимости](../network/network-topology.md) — SPOF, зависимость от Proxmox и бэкапов.
|
- [Схема сети и зависимости](../network/network-topology.md) — SPOF, зависимость от Proxmox и бэкапов.
|
||||||
|
- [Vaultwarden и использование секретов](../vaultwarden-secrets.md) — установка bw, разблокировка, получение секретов в скриптах.
|
||||||
- Документация контейнеров (100–108, 200) — бэкапы *данных внутри* сервисов (БД, тома); фаза 1 дополняет это бэкапом на уровне PVE.
|
- Документация контейнеров (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:** один раз запустить «Backup now» в Proxmox UI (Datacenter → Backup) и убедиться, что файлы появляются в `/mnt/backup/proxmox/dump/dump/`.
|
||||||
- **Проверка:** один раз запустить Backup now в Proxmox UI и убедиться, что файлы появляются в storage.
|
- **Секреты (по желанию):** перенести пароли/ключи (root PVE, БД, API) в Vaultwarden и обновлять при смене.
|
||||||
- **Тестовое восстановление:** ~~восстановить один контейнер (например 105 или 107) под другим VMID~~ выполнено 26.02.2026 (CT 107 → 999).
|
|
||||||
- **Секреты:** при необходимости перенести пароли/ключи (root PVE, БД, API) в Vaultwarden и обновлять при смене.
|
*Выполнено ранее: Yandex + Restic (cron, retention 3/2/2), тестовое восстановление CT 107 → 999 (26.02.2026).*
|
||||||
|
|||||||
@@ -57,7 +57,9 @@
|
|||||||
**Certbot на хосте (внутри CT 100):**
|
**Certbot на хосте (внутри CT 100):**
|
||||||
- Установлен в системе, таймер `certbot.timer` (проверка продления дважды в день).
|
- Установлен в системе, таймер `certbot.timer` (проверка продления дважды в день).
|
||||||
- Учётные данные Beget API: `/root/.secrets/certbot/beget.ini`.
|
- Учётные данные 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-<id>/` и делают `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-<id>/` и делают `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).
|
Подробнее по SSL: [Выпуск сертификата Let's Encrypt (DNS-01)](../network/ssl-letsencrypt-dns01.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` (пароль — в менеджере паролей или как настраивал при установке).
|
- **Debian (CT 103):** логин `root` (пароль — в менеджере паролей или как настраивал при установке).
|
||||||
@@ -162,7 +174,7 @@ docker compose up -d
|
|||||||
**Доступ по домену (опционально):** если нужен **https://vault.katykhin.ru** и из LAN, и по VPN, в NPM (контейнер 100) настраивают:
|
**Доступ по домену (опционально):** если нужен **https://vault.katykhin.ru** и из LAN, и по VPN, в NPM (контейнер 100) настраивают:
|
||||||
- **Proxy Host:** `vault.katykhin.ru` → upstream `192.168.1.103:8280`, включить SSL (Let's Encrypt или custom).
|
- **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 — открыт.
|
- **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: база, вложения, и т.п.).
|
- `/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`.
|
После этого интерфейс открывается по **`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), схема сети.
|
- [Архитектура и подключение](../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.
|
- [Контейнер 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) в скриптах.
|
||||||
|
|||||||
221
docs/vaultwarden-secrets.md
Normal file
221
docs/vaultwarden-secrets.md
Normal file
@@ -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.
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
# Запускать на хосте Proxmox под root. Использует pct exec (SSH не нужен).
|
# Запускать на хосте Proxmox под root. Использует pct exec (SSH не нужен).
|
||||||
# Результат: /mnt/backup/databases/ct101-nextcloud/nextcloud-db-YYYYMMDD-HHMM.sql.gz
|
# Результат: /mnt/backup/databases/ct101-nextcloud/nextcloud-db-YYYYMMDD-HHMM.sql.gz
|
||||||
set -e
|
set -e
|
||||||
|
# Чтобы из cron находились bw и jq (часто в /usr/local/bin)
|
||||||
|
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||||||
|
|
||||||
CT_ID=101
|
CT_ID=101
|
||||||
BACKUP_DIR="/mnt/backup/databases/ct101-nextcloud"
|
BACKUP_DIR="/mnt/backup/databases/ct101-nextcloud"
|
||||||
@@ -16,18 +18,56 @@ if [ "$(id -u)" -ne 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Минимальный размер дампа (байт). Пустой gzip ≈ 20 байт — значит pg_dump не отдал данные.
|
||||||
|
MIN_BACKUP_BYTES=512
|
||||||
|
|
||||||
mkdir -p "$BACKUP_DIR"
|
mkdir -p "$BACKUP_DIR"
|
||||||
DATE=$(date +%Y%m%d-%H%M)
|
DATE=$(date +%Y%m%d-%H%M)
|
||||||
OUTPUT="$BACKUP_DIR/nextcloud-db-$DATE.sql.gz"
|
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
|
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||||
echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
|
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
|
else
|
||||||
echo "Ошибка: дамп пустой или контейнер недоступен."
|
echo "Ошибка: дамп пустой или слишком мал (${SIZE_BYTES} байт). Проверьте контейнер $PG_CONTAINER и пароль БД в Vaultwarden (NEXTCLOUD)."
|
||||||
|
[ -s "$ERR" ] && cat "$ERR" >&2
|
||||||
rm -f "$OUTPUT"
|
rm -f "$OUTPUT"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
find "$BACKUP_DIR" -name 'nextcloud-db-*.sql.gz' -mtime +$RETENTION_DAYS -delete
|
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
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
# Запускать на хосте Proxmox под root. Использует pct exec.
|
# Запускать на хосте Proxmox под root. Использует pct exec.
|
||||||
# Результат: /mnt/backup/databases/ct103-gitea/gitea-db-YYYYMMDD-HHMM.sql.gz
|
# Результат: /mnt/backup/databases/ct103-gitea/gitea-db-YYYYMMDD-HHMM.sql.gz
|
||||||
set -e
|
set -e
|
||||||
|
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||||||
|
|
||||||
CT_ID=103
|
CT_ID=103
|
||||||
BACKUP_DIR="/mnt/backup/databases/ct103-gitea"
|
BACKUP_DIR="/mnt/backup/databases/ct103-gitea"
|
||||||
@@ -14,18 +15,47 @@ if [ "$(id -u)" -ne 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Минимальный размер дампа (байт). Пустой gzip ≈ 20 байт — значит pg_dump не отдал данные.
|
||||||
|
MIN_BACKUP_BYTES=512
|
||||||
|
|
||||||
mkdir -p "$BACKUP_DIR"
|
mkdir -p "$BACKUP_DIR"
|
||||||
DATE=$(date +%Y%m%d-%H%M)
|
DATE=$(date +%Y%m%d-%H%M)
|
||||||
OUTPUT="$BACKUP_DIR/gitea-db-$DATE.sql.gz"
|
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
|
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||||
echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
|
if [ -s "$OUTPUT" ] && [ "$SIZE_BYTES" -ge "$MIN_BACKUP_BYTES" ]; then
|
||||||
|
echo "Создан: $OUTPUT ($(du --apparent-size -h "$OUTPUT" | cut -f1))"
|
||||||
else
|
else
|
||||||
echo "Ошибка: дамп пустой или контейнер недоступен."
|
echo "Ошибка: дамп пустой или слишком мал (${SIZE_BYTES} байт). Проверьте контейнер $PG_CONTAINER и пароль БД в Vaultwarden (GITEA)."
|
||||||
|
[ -s "$ERR" ] && cat "$ERR" >&2
|
||||||
rm -f "$OUTPUT"
|
rm -f "$OUTPUT"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
find "$BACKUP_DIR" -name 'gitea-db-*.sql.gz' -mtime +$RETENTION_DAYS -delete
|
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
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
# Запускать на хосте Proxmox под root. Использует pct exec.
|
# Запускать на хосте Proxmox под root. Использует pct exec.
|
||||||
# Результат: /mnt/backup/databases/ct104-paperless/paperless-db-YYYYMMDD-HHMM.sql.gz
|
# Результат: /mnt/backup/databases/ct104-paperless/paperless-db-YYYYMMDD-HHMM.sql.gz
|
||||||
set -e
|
set -e
|
||||||
|
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||||||
|
|
||||||
CT_ID=104
|
CT_ID=104
|
||||||
BACKUP_DIR="/mnt/backup/databases/ct104-paperless"
|
BACKUP_DIR="/mnt/backup/databases/ct104-paperless"
|
||||||
@@ -14,18 +15,47 @@ if [ "$(id -u)" -ne 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Минимальный размер дампа (байт). Пустой gzip ≈ 20 байт — значит pg_dump не отдал данные.
|
||||||
|
MIN_BACKUP_BYTES=512
|
||||||
|
|
||||||
mkdir -p "$BACKUP_DIR"
|
mkdir -p "$BACKUP_DIR"
|
||||||
DATE=$(date +%Y%m%d-%H%M)
|
DATE=$(date +%Y%m%d-%H%M)
|
||||||
OUTPUT="$BACKUP_DIR/paperless-db-$DATE.sql.gz"
|
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
|
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||||
echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
|
if [ -s "$OUTPUT" ] && [ "$SIZE_BYTES" -ge "$MIN_BACKUP_BYTES" ]; then
|
||||||
|
echo "Создан: $OUTPUT ($(du --apparent-size -h "$OUTPUT" | cut -f1))"
|
||||||
else
|
else
|
||||||
echo "Ошибка: дамп пустой или контейнер недоступен."
|
echo "Ошибка: дамп пустой или слишком мал (${SIZE_BYTES} байт). Проверьте контейнер $PG_CONTAINER и пароль БД в Vaultwarden (PAPERLESS)."
|
||||||
|
[ -s "$ERR" ] && cat "$ERR" >&2
|
||||||
rm -f "$OUTPUT"
|
rm -f "$OUTPUT"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
find "$BACKUP_DIR" -name 'paperless-db-*.sql.gz' -mtime +$RETENTION_DAYS -delete
|
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
|
||||||
|
|||||||
@@ -3,12 +3,15 @@
|
|||||||
# Запускать на хосте Proxmox под root. Использует pct exec.
|
# Запускать на хосте Proxmox под root. Использует pct exec.
|
||||||
# Результат: /mnt/backup/other/ct105-vectors/vectors-YYYYMMDD-HHMM.tar.gz
|
# Результат: /mnt/backup/other/ct105-vectors/vectors-YYYYMMDD-HHMM.tar.gz
|
||||||
set -e
|
set -e
|
||||||
|
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||||||
|
|
||||||
CT_ID=105
|
CT_ID=105
|
||||||
REMOTE_PATH="/home/rag-service/data/vectors"
|
|
||||||
BACKUP_DIR="/mnt/backup/other/ct105-vectors"
|
BACKUP_DIR="/mnt/backup/other/ct105-vectors"
|
||||||
RETENTION_DAYS=14
|
RETENTION_DAYS=14
|
||||||
|
|
||||||
|
# Минимальный размер архива (байт). Пустой gzip ≈ 20 байт — каталог пуст или путь неверный.
|
||||||
|
MIN_BACKUP_BYTES=512
|
||||||
|
|
||||||
if [ "$(id -u)" -ne 0 ]; then
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
echo "Запускайте под root."
|
echo "Запускайте под root."
|
||||||
exit 1
|
exit 1
|
||||||
@@ -17,15 +20,31 @@ fi
|
|||||||
mkdir -p "$BACKUP_DIR"
|
mkdir -p "$BACKUP_DIR"
|
||||||
DATE=$(date +%Y%m%d-%H%M)
|
DATE=$(date +%Y%m%d-%H%M)
|
||||||
OUTPUT="$BACKUP_DIR/vectors-$DATE.tar.gz"
|
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
|
SIZE_BYTES=$(stat -c%s "$OUTPUT" 2>/dev/null || echo 0)
|
||||||
echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
|
if [ -s "$OUTPUT" ] && [ "$SIZE_BYTES" -ge "$MIN_BACKUP_BYTES" ]; then
|
||||||
|
echo "Создан: $OUTPUT ($(du --apparent-size -h "$OUTPUT" | cut -f1))"
|
||||||
else
|
else
|
||||||
echo "Ошибка: архив пустой или каталог недоступен."
|
echo "Ошибка: архив пустой или слишком мал (${SIZE_BYTES} байт). Проверьте /home/rag-service/data/vectors в CT $CT_ID."
|
||||||
|
[ -s "$ERR" ] && cat "$ERR" >&2
|
||||||
rm -f "$OUTPUT"
|
rm -f "$OUTPUT"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
find "$BACKUP_DIR" -name 'vectors-*.tar.gz' -mtime +$RETENTION_DAYS -delete
|
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
|
||||||
|
|||||||
@@ -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-pve-*.tar.gz' -mtime +$RETENTION_DAYS -delete
|
||||||
find "$BACKUP_ROOT" -name 'etc-host-configs-*.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
|
||||||
|
|||||||
@@ -19,3 +19,12 @@ mkdir -p "$BACKUP_PATH"
|
|||||||
rsync -az --timeout=3600 \
|
rsync -az --timeout=3600 \
|
||||||
--exclude=".stfolder" \
|
--exclude=".stfolder" \
|
||||||
"$VM_SSH:$REMOTE_PATH/" "$BACKUP_PATH/"
|
"$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
|
||||||
|
|||||||
@@ -1,46 +1,54 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Выгрузка только /mnt/backup/photos в Yandex Object Storage (S3) через restic.
|
# Выгрузка только /mnt/backup/photos в Yandex Object Storage (S3) через restic.
|
||||||
# Тот же репозиторий, что и backup-restic-yandex.sh; фото вынесены в отдельный снимок (больше всего данных).
|
# Тот же репозиторий, что и backup-restic-yandex.sh; фото вынесены в отдельный снимок (больше всего данных).
|
||||||
# Запускать на хосте Proxmox под root. Требуется тот же /root/.restic-yandex.env и /root/.restic-password.
|
# Запускать на хосте Proxmox под root. Секреты из Vaultwarden (объект RESTIC), файл /root/.bw-master (chmod 600).
|
||||||
# Cron: 0 1 * * * (01:00).
|
# Cron: 10 4 * * * (04:10, после основного restic в 04:00).
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
ENV_FILE="/root/.restic-yandex.env"
|
|
||||||
BACKUP_PATH="/mnt/backup/photos"
|
BACKUP_PATH="/mnt/backup/photos"
|
||||||
|
# Время запуска (для логов и уведомлений)
|
||||||
|
START_TS=$(date +%s)
|
||||||
|
START_HUMAN=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
|
||||||
if [ "$(id -u)" -ne 0 ]; then
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
echo "Запускайте под root."
|
echo "Запускайте под root."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f "$ENV_FILE" ]; then
|
# Секреты из Vaultwarden (объект RESTIC)
|
||||||
echo "Нет файла $ENV_FILE. Скопируйте restic-yandex-env.example и задайте ключи."
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
if ! command -v bw >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then
|
||||||
# shellcheck source=/dev/null
|
echo "Установите bw (Bitwarden CLI) и jq для получения секретов из Vaultwarden."
|
||||||
source "$ENV_FILE"
|
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
|
for var in RESTIC_REPOSITORY AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY; do
|
||||||
if [ -z "${!var}" ]; then
|
if [ -z "${!var}" ]; then
|
||||||
echo "В $ENV_FILE не задано: $var"
|
echo "В Vaultwarden (RESTIC) не задано поле для $var"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
RESTIC_PASSWORD_FILE=$(mktemp -u)
|
||||||
if [ -z "$RESTIC_PASSWORD_FILE" ]; then
|
echo -n "$RESTIC_PASS" > "$RESTIC_PASSWORD_FILE"
|
||||||
RESTIC_PASSWORD_FILE="/root/.restic-password"
|
chmod 600 "$RESTIC_PASSWORD_FILE"
|
||||||
fi
|
trap 'rm -f "$RESTIC_PASSWORD_FILE"' EXIT INT TERM
|
||||||
if [ ! -f "$RESTIC_PASSWORD_FILE" ]; then
|
|
||||||
echo "Файл с паролем репозитория не найден: $RESTIC_PASSWORD_FILE"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
export AWS_ACCESS_KEY_ID
|
|
||||||
export AWS_SECRET_ACCESS_KEY
|
|
||||||
export RESTIC_PASSWORD_FILE
|
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
|
if ! command -v restic >/dev/null 2>&1; then
|
||||||
echo "restic не установлен. Установите: apt install restic."
|
echo "restic не установлен. Установите: apt install restic."
|
||||||
@@ -53,7 +61,8 @@ if [ ! -d "$BACKUP_PATH" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Restic backup (photos): $BACKUP_PATH -> $RESTIC_REPOSITORY"
|
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)..."
|
echo "Restic forget (retention 3 daily, 2 weekly, 2 monthly)..."
|
||||||
restic forget --keep-daily 3 --keep-weekly 2 --keep-monthly 2 --prune --quiet
|
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..."
|
echo "Restic prune..."
|
||||||
restic prune --quiet
|
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 "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
|
||||||
|
|||||||
@@ -2,12 +2,15 @@
|
|||||||
# Выгрузка /mnt/backup в Yandex Object Storage (S3) через restic (без каталога photos).
|
# Выгрузка /mnt/backup в Yandex Object Storage (S3) через restic (без каталога photos).
|
||||||
# Фото бэкапятся отдельно: backup-restic-yandex-photos.sh.
|
# Фото бэкапятся отдельно: backup-restic-yandex-photos.sh.
|
||||||
# Запускать на хосте Proxmox под root.
|
# Запускать на хосте 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 зарезервировано под перезагрузку).
|
# Cron: 0 4 * * * (04:00, после окна 01:00–03:30; 05:00 зарезервировано под перезагрузку).
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
ENV_FILE="/root/.restic-yandex.env"
|
|
||||||
BACKUP_PATH="/mnt/backup"
|
BACKUP_PATH="/mnt/backup"
|
||||||
|
# Время запуска (для логов и уведомлений)
|
||||||
|
START_TS=$(date +%s)
|
||||||
|
START_HUMAN=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
# Исключаем служебные каталоги и photos (фото — отдельный бэкап)
|
# Исключаем служебные каталоги и photos (фото — отдельный бэкап)
|
||||||
EXCLUDE_OPTS=(--exclude="$BACKUP_PATH/lost+found" --exclude="$BACKUP_PATH/photos")
|
EXCLUDE_OPTS=(--exclude="$BACKUP_PATH/lost+found" --exclude="$BACKUP_PATH/photos")
|
||||||
|
|
||||||
@@ -16,34 +19,40 @@ if [ "$(id -u)" -ne 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f "$ENV_FILE" ]; then
|
# Секреты из Vaultwarden (объект RESTIC)
|
||||||
echo "Нет файла $ENV_FILE. Скопируйте restic-yandex-env.example и задайте ключи."
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
if ! command -v bw >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then
|
||||||
# shellcheck source=/dev/null
|
echo "Установите bw (Bitwarden CLI) и jq для получения секретов из Vaultwarden."
|
||||||
source "$ENV_FILE"
|
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
|
for var in RESTIC_REPOSITORY AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY; do
|
||||||
if [ -z "${!var}" ]; then
|
if [ -z "${!var}" ]; then
|
||||||
echo "В $ENV_FILE не задано: $var"
|
echo "В Vaultwarden (RESTIC) не задано поле для $var"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
RESTIC_PASSWORD_FILE=$(mktemp -u)
|
||||||
if [ -z "$RESTIC_PASSWORD_FILE" ]; then
|
echo -n "$RESTIC_PASS" > "$RESTIC_PASSWORD_FILE"
|
||||||
RESTIC_PASSWORD_FILE="/root/.restic-password"
|
chmod 600 "$RESTIC_PASSWORD_FILE"
|
||||||
fi
|
trap 'rm -f "$RESTIC_PASSWORD_FILE"' EXIT INT TERM
|
||||||
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
|
|
||||||
export RESTIC_PASSWORD_FILE
|
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
|
if ! command -v restic >/dev/null 2>&1; then
|
||||||
echo "restic не установлен. Установите: apt install restic."
|
echo "restic не установлен. Установите: apt install restic."
|
||||||
@@ -51,7 +60,8 @@ if ! command -v restic >/dev/null 2>&1; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Restic backup: $BACKUP_PATH (excl. photos) -> $RESTIC_REPOSITORY"
|
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)..."
|
echo "Restic forget (retention 3 daily, 2 weekly, 2 monthly)..."
|
||||||
restic forget --keep-daily 3 --keep-weekly 2 --keep-monthly 2 --prune --quiet
|
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..."
|
echo "Restic prune..."
|
||||||
restic prune --quiet
|
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 "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
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
# Запускать на хосте Proxmox под root. Использует pct exec.
|
# Запускать на хосте Proxmox под root. Использует pct exec.
|
||||||
# Результат: /mnt/backup/other/vaultwarden/vaultwarden-data-YYYYMMDD-HHMM.tar.gz
|
# Результат: /mnt/backup/other/vaultwarden/vaultwarden-data-YYYYMMDD-HHMM.tar.gz
|
||||||
set -e
|
set -e
|
||||||
|
export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||||||
|
|
||||||
CT_ID=103
|
CT_ID=103
|
||||||
REMOTE_PATH="/opt/docker/vaultwarden/data"
|
REMOTE_PATH="/opt/docker/vaultwarden/data"
|
||||||
@@ -15,19 +16,38 @@ if [ "$(id -u)" -ne 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Минимальный размер архива (байт). Пустой tar.gz ≈ 20 байт — значит каталог пуст или путь неверный.
|
||||||
|
MIN_BACKUP_BYTES=512
|
||||||
|
|
||||||
mkdir -p "$BACKUP_DIR"
|
mkdir -p "$BACKUP_DIR"
|
||||||
DATE=$(date +%Y%m%d-%H%M)
|
DATE=$(date +%Y%m%d-%H%M)
|
||||||
OUTPUT="$BACKUP_DIR/vaultwarden-data-$DATE.tar.gz"
|
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"
|
chmod 600 "$OUTPUT"
|
||||||
echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
|
echo "Создан: $OUTPUT ($(du --apparent-size -h "$OUTPUT" | cut -f1))"
|
||||||
else
|
else
|
||||||
echo "Ошибка: архив пустой или каталог недоступен."
|
echo "Ошибка: архив пустой или слишком мал (${SIZE_BYTES} байт). Проверьте путь /opt/docker/vaultwarden/data в CT $CT_ID."
|
||||||
|
[ -s "$ERR" ] && cat "$ERR" >&2
|
||||||
rm -f "$OUTPUT"
|
rm -f "$OUTPUT"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
find "$BACKUP_DIR" -name 'vaultwarden-data-*.tar.gz' -mtime +$RETENTION_DAYS -delete
|
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
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -s "$OUTPUT" ]; then
|
if [ -s "$OUTPUT" ]; then
|
||||||
echo "Создан: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
|
echo "Создан: $OUTPUT ($(du --apparent-size -h "$OUTPUT" | cut -f1))"
|
||||||
else
|
else
|
||||||
echo "Ошибка: дамп пустой или не создан."
|
echo "Ошибка: дамп пустой или не создан."
|
||||||
rm -f "$OUTPUT"
|
rm -f "$OUTPUT"
|
||||||
@@ -39,3 +39,15 @@ fi
|
|||||||
|
|
||||||
# Удалить дампы старше RETENTION_DAYS
|
# Удалить дампы старше RETENTION_DAYS
|
||||||
find "$BACKUP_DIR" -name 'immich-db-*.sql.gz' -mtime +$RETENTION_DAYS -delete
|
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
|
||||||
|
|||||||
@@ -71,3 +71,12 @@ if [ -f "$S3_ENV" ]; then
|
|||||||
else
|
else
|
||||||
echo "Подсказка: для бэкапа S3 создайте $S3_ENV с S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET_NAME."
|
echo "Подсказка: для бэкапа S3 создайте $S3_ENV с S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET_NAME."
|
||||||
fi
|
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
|
||||||
|
|||||||
@@ -37,6 +37,18 @@ ssh "${SSH_OPTS[@]}" "${VPS_USER}@${VPS_HOST}" "tar -chzf - -C / \
|
|||||||
var/www/katykhin.store" > "$ARCHIVE"
|
var/www/katykhin.store" > "$ARCHIVE"
|
||||||
|
|
||||||
chmod 600 "$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
|
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
|
||||||
|
|||||||
63
scripts/notify-telegram.sh
Normal file
63
scripts/notify-telegram.sh
Normal file
@@ -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
|
||||||
48
scripts/notify-vzdump-success.sh
Normal file
48
scripts/notify-vzdump-success.sh
Normal file
@@ -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
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
# Восстановление одного файла vzdump из restic (Yandex S3) через mount.
|
# Восстановление одного файла vzdump из restic (Yandex S3) через mount.
|
||||||
# Не выкачивает весь репозиторий — подгружаются только нужные данные для выбранного файла.
|
# Не выкачивает весь репозиторий — подгружаются только нужные данные для выбранного файла.
|
||||||
# Запускать на хосте Proxmox под root. Требуется FUSE (restic mount).
|
# Запускать на хосте Proxmox под root. Требуется FUSE (restic mount).
|
||||||
|
# Секреты из Vaultwarden (объект RESTIC), файл /root/.bw-master (chmod 600).
|
||||||
#
|
#
|
||||||
# Использование:
|
# Использование:
|
||||||
# restore-one-vzdump-from-restic.sh [SNAPSHOT] [ПУТЬ_В_СНИМКЕ] [КУДА_СОХРАНИТЬ]
|
# restore-one-vzdump-from-restic.sh [SNAPSHOT] [ПУТЬ_В_СНИМКЕ] [КУДА_СОХРАНИТЬ]
|
||||||
@@ -14,9 +15,7 @@
|
|||||||
# Список файлов в снимке: restic ls SNAPSHOT
|
# Список файлов в снимке: restic ls SNAPSHOT
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
ENV_FILE="${ENV_FILE:-/root/.restic-yandex.env}"
|
|
||||||
MOUNT_DIR="${MOUNT_DIR:-/mnt/backup/restic-mount}"
|
MOUNT_DIR="${MOUNT_DIR:-/mnt/backup/restic-mount}"
|
||||||
RESTIC_PASSWORD_FILE="${RESTIC_PASSWORD_FILE:-/root/.restic-password}"
|
|
||||||
|
|
||||||
SNAPSHOT="${1:-latest}"
|
SNAPSHOT="${1:-latest}"
|
||||||
# Путь к файлу внутри снимка (как в restic ls) — бэкапим /mnt/backup, пути вида /mnt/backup/proxmox/dump/dump/vzdump-lxc-107-...
|
# Путь к файлу внутри снимка (как в restic ls) — бэкапим /mnt/backup, пути вида /mnt/backup/proxmox/dump/dump/vzdump-lxc-107-...
|
||||||
@@ -28,22 +27,40 @@ if [ "$(id -u)" -ne 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f "$ENV_FILE" ]; then
|
# Секреты из Vaultwarden (объект RESTIC)
|
||||||
echo "Нет файла $ENV_FILE. Скопируйте restic-yandex-env.example и задайте ключи."
|
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
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
if ! command -v bw >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then
|
||||||
# shellcheck source=/dev/null
|
echo "Установите bw (Bitwarden CLI) и jq для получения секретов из Vaultwarden."
|
||||||
source "$ENV_FILE"
|
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
|
for var in RESTIC_REPOSITORY AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY; do
|
||||||
if [ -z "${!var}" ]; then
|
if [ -z "${!var}" ]; then
|
||||||
echo "В $ENV_FILE не задано: $var"
|
echo "В Vaultwarden (RESTIC) не задано поле для $var"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
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 RESTIC_PASSWORD_FILE
|
||||||
export AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-ru-central1}"
|
|
||||||
|
|
||||||
if ! command -v restic >/dev/null 2>&1; then
|
if ! command -v restic >/dev/null 2>&1; then
|
||||||
echo "restic не установлен. Установите: apt install restic."
|
echo "restic не установлен. Установите: apt install restic."
|
||||||
@@ -80,7 +97,7 @@ fi
|
|||||||
echo "Монтируем репозиторий в $MOUNT_DIR ..."
|
echo "Монтируем репозиторий в $MOUNT_DIR ..."
|
||||||
restic mount "$MOUNT_DIR" &
|
restic mount "$MOUNT_DIR" &
|
||||||
MOUNT_PID=$!
|
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/<short_id>/<путь>
|
# В смонтированном репо файлы снимка лежат в ids/<short_id>/<путь>
|
||||||
SOURCE_FILE="$MOUNT_DIR/ids/$SNAPSHOT_ID/$FILE_IN_SNAPSHOT"
|
SOURCE_FILE="$MOUNT_DIR/ids/$SNAPSHOT_ID/$FILE_IN_SNAPSHOT"
|
||||||
|
|||||||
13
scripts/telegram-notify.env.example
Normal file
13
scripts/telegram-notify.env.example
Normal file
@@ -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<TOKEN>/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
|
||||||
Reference in New Issue
Block a user