Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05d8c748ab | ||
|
|
ca48ef2696 | ||
|
|
a23e944756 | ||
|
|
d2e1e6876a | ||
|
|
5e27ba2513 | ||
|
|
a3fe8031fe | ||
|
|
e349f19e68 | ||
|
|
79878e36f9 | ||
|
|
e24b75534e | ||
|
|
baef2c49b3 | ||
|
|
7031d7dbb3 | ||
|
|
646f2bbc8f | ||
|
|
e0c53a1b1b | ||
|
|
22394cbaba |
@@ -26,3 +26,12 @@ result-*
|
|||||||
|
|
||||||
# Ansible local runtime dir (facts cache, locks)
|
# Ansible local runtime dir (facts cache, locks)
|
||||||
.ansible/
|
.ansible/
|
||||||
|
|
||||||
|
# OpenTofu: state содержит фактическую конфигурацию гостей и потому не
|
||||||
|
# коммитится. Лок-файл провайдера (.terraform.lock.hcl) наоборот нужен в Git —
|
||||||
|
# он закрепляет версию, поэтому под исключение не попадает.
|
||||||
|
tofu/.terraform/
|
||||||
|
tofu/*.tfstate
|
||||||
|
tofu/*.tfstate.*
|
||||||
|
tofu/*.tfplan
|
||||||
|
tofu/.env
|
||||||
|
|||||||
@@ -1,361 +1,155 @@
|
|||||||
# AGENTS.md
|
# AGENTS.md
|
||||||
|
|
||||||
## Project Summary
|
## Назначение
|
||||||
|
|
||||||
**HomeLab infras** is a GitOps-like repository for managing home infrastructure through Ansible.
|
HomeLab infras - Ansible-first control plane для домашней инфраструктуры на
|
||||||
|
Proxmox VE. Желаемое состояние хранится в inventory, vars, roles и playbooks;
|
||||||
|
прямые изменения на серверах допустимы только для read-only диагностики или
|
||||||
|
break-glass восстановления и затем должны быть отражены в Ansible.
|
||||||
|
|
||||||
- Management: `ansible/` (playbooks, roles, inventory)
|
Стабильный контекст проекта находится в [`docs/ai/`](docs/ai/README.md). Не
|
||||||
- Documentation: Obsidian vault `/home/ada/Documents/Vaults/SecondBrain/02 Projects/HomeLab/`
|
загружай весь набор по умолчанию: читай только документы, относящиеся к задаче.
|
||||||
- Active infrastructure: Proxmox VE cluster (`cloud-pc`, `mini-pc`), PBS, OpenVPN, ZeroTier
|
|
||||||
- Archive: `archive/2026-07-proxmox-migration/` (historical NixOS/Docker configs, reference only)
|
|
||||||
|
|
||||||
## Design Goal
|
## Что читать
|
||||||
|
|
||||||
**Ansible-first infrastructure**: HomeLab configuration is managed through Ansible inventory, roles and playbooks. Direct server changes are allowed only for break-glass recovery or read-only diagnostics; afterwards the intended state must be captured in Ansible.
|
| Задача | Контекст |
|
||||||
|
|---|---|
|
||||||
|
| Любое изменение | `README.md`, [`docs/ai/README.md`](docs/ai/README.md), релевантная секция [`architecture.md`](docs/ai/architecture.md) |
|
||||||
|
| Inventory или новый service | `ansible/inventory/hosts.yml`, `ansible/inventory/group_vars/all/services.yml`, [`architecture.md`](docs/ai/architecture.md), [`edge-cases.md`](docs/ai/edge-cases.md) |
|
||||||
|
| Provisioning или update | `ansible/README.md`, relevant playbook/role, [`edge-cases.md`](docs/ai/edge-cases.md), [`legacy-warning.md`](docs/ai/legacy-warning.md) |
|
||||||
|
| Network, SSH, OpenVPN, Caddy | `ansible/ssh_config`, `ansible/inventory/group_vars/all/main.yml`, [`architecture.md`](docs/ai/architecture.md), [`links.md`](docs/ai/links.md) |
|
||||||
|
| Backups и recovery | `ansible/playbooks/pve-backup-jobs.yml`, `ansible/roles/backup_audit/`, [`architecture.md`](docs/ai/architecture.md), [`edge-cases.md`](docs/ai/edge-cases.md) |
|
||||||
|
| Grimmory MCP | `tools/grimmory-mcp/README.md`, [`tech-stack.md`](docs/ai/tech-stack.md), [`edge-cases.md`](docs/ai/edge-cases.md) |
|
||||||
|
| Legacy/frozen code | [`legacy-warning.md`](docs/ai/legacy-warning.md) |
|
||||||
|
| Переезд на OpenTofu | [`migration-tofu.md`](docs/ai/migration-tofu.md), `tofu/README.md`, `tofu/*.tf` |
|
||||||
|
| Текущая работа | [`plan.md`](docs/ai/plan.md); `current-task.md` является историческим планом |
|
||||||
|
|
||||||
## Obsidian Integration
|
Перед инфраструктурными изменениями прочитай релевантные заметки в Obsidian:
|
||||||
|
|
||||||
Project documentation lives in `/home/ada/Documents/Vaults/SecondBrain/02 Projects/HomeLab/`. Use it as wiki:
|
`/home/ada/Documents/Vaults/SecondBrain/02 Projects/HomeLab/`
|
||||||
|
|
||||||
- **Read** before making infra changes — context, decisions, constraints.
|
Основные заметки: `HomeLab.md`,
|
||||||
- **Update** after making changes — document non-obvious decisions, new patterns, lessons learned.
|
`Notes/Текущее состояние HomeLab после миграции на Proxmox.md`, `Log.md`.
|
||||||
- Key files: `HomeLab.md`, `Notes/Текущее состояние HomeLab после миграции на Proxmox.md`, `Log.md`.
|
После изменения обнови соответствующую заметку, если изменились topology,
|
||||||
|
операционная процедура, решение или неочевидное ограничение.
|
||||||
|
|
||||||
When introducing infra changes, update the relevant Obsidian note to keep documentation in sync.
|
## Источники правды
|
||||||
|
|
||||||
## Operating Model
|
- Hosts и groups: `ansible/inventory/hosts.yml`.
|
||||||
|
- Shared network/access vars: `ansible/inventory/group_vars/all/main.yml`.
|
||||||
|
- Service facts: `ansible/inventory/group_vars/all/services.yml`.
|
||||||
|
- SSH users, keys, ports и ProxyJump: `ansible/ssh_config`.
|
||||||
|
- Manual operations и safety gates: `ansible/Makefile`.
|
||||||
|
- Secrets: ignored `.env` в КОРНЕ репозитория; его читают и Make/Ansible, и OpenTofu.
|
||||||
|
- Human decisions и operations log: HomeLab Obsidian vault.
|
||||||
|
|
||||||
- User: passwords, SSH access, base network reachability.
|
Программные потребители реестра:
|
||||||
- Agent: infrastructure changes via `ansible/` files.
|
|
||||||
- Prefer adding/updating a role or playbook over ad-hoc commands.
|
|
||||||
- For every new VM, LXC container, or managed host, provision the user's SSH public key by default unless explicitly told otherwise.
|
|
||||||
- Run smallest safe Ansible check before broader changes.
|
|
||||||
- Do not commit secrets (`.env`, tokens, keys, real passwords).
|
|
||||||
|
|
||||||
## Quick Start
|
- `playbooks/reverse-proxy.yml` — сборка Caddyfile;
|
||||||
|
- `playbooks/ru-vps-base.yml` — стек Caddy и его закреплённый образ;
|
||||||
|
- `playbooks/pve-backup-jobs.yml` — списки VMID заданий PBS (поле `backup.job`);
|
||||||
|
- `roles/backup_audit` — VMID для аудита (флаг `monitoring.backup_audit_vmid`);
|
||||||
|
- `playbooks/validate.yml` — сверка реестра с фактическим состоянием Proxmox;
|
||||||
|
- `playbooks/dashboard.yml` — `services.yaml` для Homepage-обзора инфраструктуры.
|
||||||
|
|
||||||
Окружение собрано в Nix — venv и системные пакеты не нужны.
|
Остальное (`pve-*.yml`, `status.yml`, monitoring, SSH config) по-прежнему
|
||||||
|
дублирует значения и должно меняться согласованно.
|
||||||
|
|
||||||
|
`make validate` — read-only gate, проверяющий это расхождение.
|
||||||
|
|
||||||
|
## Границы
|
||||||
|
|
||||||
|
- Active control plane находится в `ansible/`.
|
||||||
|
- `archive/2026-07-proxmox-migration/` - только историческая справка; не редактировать
|
||||||
|
и не возвращать из него конфигурацию как active implementation.
|
||||||
|
- Не редактировать generated artifacts, `ansible/collections/`, `.venv/`,
|
||||||
|
`node_modules/`, `.direnv/` и secret-bearing ignored files.
|
||||||
|
- `roles/compose_service` подключён в `playbooks/ru-vps-base.yml` (стек Caddy).
|
||||||
|
`roles/lxc_docker_host` не подключён нигде.
|
||||||
|
Не считать active service playbooks устаревшими до отдельной миграции.
|
||||||
|
- Не исправлять найденный technical debt в несвязанной задаче без отдельного решения.
|
||||||
|
- Для новой VM/LXC/managed host по умолчанию provision public SSH key пользователя,
|
||||||
|
если пользователь явно не указал иное.
|
||||||
|
|
||||||
|
## Setup и команды
|
||||||
|
|
||||||
|
Предпочтительное окружение - Nix:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nix develop # или один раз: direnv allow
|
nix develop # или один раз: direnv allow
|
||||||
|
ansible-galaxy collection install \
|
||||||
# Один раз на клон: galaxy-коллекции
|
-r ansible/requirements.yml \
|
||||||
ansible-galaxy collection install -r ansible/requirements.yml -p ansible/collections
|
-p ansible/collections # один раз на clone
|
||||||
```
|
```
|
||||||
|
|
||||||
Дальше всё управление идёт через `make` из каталога `ansible/`:
|
Работай через Make из `ansible/` или с `make -C ansible` из root:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ansible
|
make -C ansible help
|
||||||
make help # список всех целей — начинать отсюда
|
make -C ansible check
|
||||||
make check # связность и ожидаемые IP
|
make -C ansible status EXTRA="--limit '!gyro'"
|
||||||
make status # сводное состояние всей инфраструктуры (read-only)
|
make -C ansible inventory
|
||||||
make lint # ansible-lint + yamllint
|
make -C ansible docs
|
||||||
make docs # актуальная таблица хостов из inventory
|
make -C ansible lint
|
||||||
|
|
||||||
make deploy-gitea # playbooks/pve-gitea.yml, .env подхватывается сам
|
|
||||||
make dry-gitea # то же в режиме --check --diff
|
|
||||||
make update-gitea # бэкап -> обновление -> health-check
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`make` без аргументов печатает `help`. Опасные цели (`mihomo-harden`, `monitoring`,
|
Локальный syntax-check всех playbooks, соответствующий CI:
|
||||||
`update-all`) требуют явного `CONFIRM=1`. Дополнительные флаги — через `EXTRA`:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make status EXTRA="--limit '!gyro'"
|
nix develop -c sh -c \
|
||||||
|
'cd ansible && for f in playbooks/*.yml; do ansible-playbook --syntax-check "$f"; done'
|
||||||
```
|
```
|
||||||
|
|
||||||
`gyro` использует шифрованный `host_vars/gyro/vault.yml`, поэтому для него нужен
|
Grimmory MCP требует отдельный Node.js `>=22` runtime:
|
||||||
`--ask-vault-pass` (цель `make gyro` добавляет флаг сама).
|
|
||||||
|
|
||||||
### SSH руками
|
|
||||||
|
|
||||||
Транспорт описан в `ansible/ssh_config` — один источник правды и для Ansible,
|
|
||||||
и для терминала. Чтобы заработал `ssh gitea`, добавь в `~/.ssh/config`:
|
|
||||||
|
|
||||||
```
|
|
||||||
Include /home/ada/Documents/Projects/HomeLab/infras/ansible/ssh_config
|
|
||||||
```
|
|
||||||
|
|
||||||
SSH берёт первое совпадение: Include в начале файла — побеждают настройки репозитория,
|
|
||||||
в конце — личные записи из `~/.ssh/config.d/`. На Ansible порядок не влияет, он ходит
|
|
||||||
с явным `-F`.
|
|
||||||
|
|
||||||
## Active Infrastructure
|
|
||||||
|
|
||||||
Каноничный источник — `ansible/inventory/hosts.yml` и реестр сервисов
|
|
||||||
`ansible/inventory/group_vars/all/services.yml`. Актуальную таблицу всегда можно
|
|
||||||
получить командой `make docs`, поэтому здесь — только опорная картина.
|
|
||||||
|
|
||||||
### Nodes
|
|
||||||
|
|
||||||
| Name | Role | LAN IP | Notes |
|
|
||||||
|---|---|---:|---|
|
|
||||||
| `ru-vps` | Public VPS, JumpHost, qdevice, OpenVPN server, Caddy | `157.22.231.198:3422` | OpenVPN `10.78.0.1` |
|
|
||||||
| `cloud-pc` | Proxmox VE node, storage | `192.168.1.5` | Main node |
|
|
||||||
| `mini-pc` | Proxmox VE node | `192.168.1.10` | Secondary node |
|
|
||||||
| `pbs` | Proxmox Backup Server LXC (CT 120) | `192.168.1.20` | Прямой доступ, без ProxyJump |
|
|
||||||
| `ovpn-mini` | OpenVPN gateway LXC (CT 132) | `192.168.1.23` | OpenVPN `10.78.0.2`, без ProxyJump |
|
|
||||||
| `vaultwarden` | Vaultwarden LXC (CT 140) | `192.168.1.24` | `pass.ada-dev.ru` |
|
|
||||||
| `gitea` | Gitea LXC (CT 141) | `192.168.1.25` | `git.ada-dev.ru`, SSH `:2222` |
|
|
||||||
| `memoir-bot` | Telegram memoir bot LXC (CT 142) | `192.168.1.26` | образ собирается локально |
|
|
||||||
| `mihomo` | Local proxy/UI LXC (CT 143) | `192.168.1.27` | UI `:8080`, API `:9090`, proxy `:7890/:7891` |
|
|
||||||
| `adguard` | AdGuard Home LXC (CT 144) | `192.168.1.28` | DNS `:53`, UI `:3000` |
|
|
||||||
| `docker-test` | Песочница/кандидат в CI-раннеры (CT 145) | `192.168.1.29` | |
|
|
||||||
| `monitoring` | Monitoring LXC (CT 146) | `192.168.1.30` | Uptime Kuma активен, Prometheus заморожен |
|
|
||||||
| `hermes-ai` | AI-хост (CT 147) | `192.168.1.31` | ходит наружу через mihomo |
|
|
||||||
| `emergency-bot` | Telegram-бот break-glass доступа (CT 148) | `192.168.1.32` | |
|
|
||||||
| `grimmory` | Grimmory + MariaDB (CT 149) | `192.168.1.34` | `books.ada-dev.ru` |
|
|
||||||
| `gyro` | Investment allocator (CT 150) | `192.168.1.35` | vault-переменные, outbound-only |
|
|
||||||
|
|
||||||
### Inventory Groups
|
|
||||||
|
|
||||||
- `homelab` — ru-vps
|
|
||||||
- `pve_nodes` — cloud-pc, mini-pc
|
|
||||||
- `lxc_infra` — все LXC (13 шт.)
|
|
||||||
- `monitoring_server` — monitoring
|
|
||||||
- `monitoring_exporters` — хосты с Node Exporter
|
|
||||||
- `monitoring_smart_exporters` — cloud-pc, mini-pc
|
|
||||||
- `vpn_openvpn` — ru-vps, ovpn-mini
|
|
||||||
- `shell_hosts` — ru-vps, cloud-pc, mini-pc, hermes-ai
|
|
||||||
- `servers` — все управляемые хосты
|
|
||||||
|
|
||||||
Общие переменные живут в `inventory/group_vars/`, а не в `hosts.yml`:
|
|
||||||
`group_vars/all/main.yml` (сеть, доступ, OpenVPN), `group_vars/lxc_infra/main.yml`
|
|
||||||
(LXC ходят под root без sudo), `group_vars/all/services.yml` (реестр сервисов).
|
|
||||||
|
|
||||||
### Transport
|
|
||||||
|
|
||||||
- **OpenVPN**: ru-vps (10.78.0.1) ↔ ovpn-mini (10.78.0.2), порт 8443/tcp
|
|
||||||
- **JumpHost**: SSH к нодам и LXC через ProxyJump `ru-vps`; исключения — `pbs` и
|
|
||||||
`ovpn-mini`, они доступны напрямую по LAN
|
|
||||||
- Всё это описано в `ansible/ssh_config`; Ansible подключает его через
|
|
||||||
`ansible_ssh_common_args` в `group_vars/all/main.yml` (путь считается от
|
|
||||||
`inventory_dir`, поэтому не зависит от cwd и места клона)
|
|
||||||
|
|
||||||
## Directory Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
flake.nix / .envrc # Nix dev-окружение (ansible, линтеры, python-зависимости)
|
|
||||||
.ansible-lint / .yamllint # Конфигурация линтеров
|
|
||||||
.gitea/workflows/lint.yml # CI: yamllint + ansible-lint + syntax-check
|
|
||||||
|
|
||||||
ansible/
|
|
||||||
├── Makefile # ЕДИНАЯ точка входа для ручного управления (make help)
|
|
||||||
├── ansible.cfg
|
|
||||||
├── ssh_config # Транспорт: ProxyJump, пользователи, ключи
|
|
||||||
├── scripts/
|
|
||||||
│ └── gen-inventory-docs.py
|
|
||||||
├── inventory/
|
|
||||||
│ ├── hosts.yml # Только адреса и индивидуальные факты хостов
|
|
||||||
│ ├── group_vars/
|
|
||||||
│ │ ├── all/main.yml # Общие переменные
|
|
||||||
│ │ ├── all/services.yml # Реестр сервисов homelab_services
|
|
||||||
│ │ └── lxc_infra/main.yml
|
|
||||||
│ └── host_vars/gyro/ # main.yml + шифрованный vault.yml
|
|
||||||
├── playbooks/
|
|
||||||
│ ├── check.yml # Связность и ожидаемые IP
|
|
||||||
│ ├── status.yml # Read-only сводка по всей инфраструктуре
|
|
||||||
│ ├── reverse-proxy.yml # Единый Caddy-плейбук по реестру сервисов
|
|
||||||
│ ├── pve-*.yml # Создание LXC (нужен .env)
|
|
||||||
│ ├── *-update.yml # Обновления: бэкап -> апдейт -> health-check
|
|
||||||
│ └── openvpn-*.yml
|
|
||||||
├── roles/
|
|
||||||
│ ├── lxc_docker_host/ # LXC под Docker: пакеты, fuse-overlayfs, ufw
|
|
||||||
│ ├── compose_service/ # compose.yml + systemd-юнит + health-check
|
|
||||||
│ ├── pve_lxc/ # Создание LXC через Proxmox API
|
|
||||||
│ ├── backup_audit/ # Аудит PBS и restic
|
|
||||||
│ ├── monitoring_*/ # Prometheus-стек (заморожен), экспортеры
|
|
||||||
│ ├── uptime_kuma/ # Активный мониторинг
|
|
||||||
│ ├── emergency_access/ # Break-glass reverse SSH
|
|
||||||
│ ├── emergency_bot/ # Telegram-бот break-glass
|
|
||||||
│ ├── gyro/ # Investment allocator
|
|
||||||
│ ├── openvpn_gateway/ # OpenVPN транспорт
|
|
||||||
│ └── bash_config/ # Единый bash-конфиг
|
|
||||||
└── tasks/
|
|
||||||
|
|
||||||
archive/2026-07-proxmox-migration/ # Историческое, только как справка
|
|
||||||
```
|
|
||||||
|
|
||||||
Роли `lxc_docker_host` и `compose_service` созданы, но пока не подключены ни к одному
|
|
||||||
плейбуку — миграция `pve-*.yml` на них не выполнена. Пример использования и перечень
|
|
||||||
того, что меняется на живом хосте при переходе, — в `roles/compose_service/README.md`.
|
|
||||||
|
|
||||||
## Common Playbooks
|
|
||||||
|
|
||||||
Предпочитай `make` — он сам грузит `.env` и защищает опасные цели.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make check # связность и ожидаемые IP
|
npm install --prefix tools/grimmory-mcp
|
||||||
make status # read-only сводка: хосты, юниты, диски, VPN, бэкапы
|
npm test --prefix tools/grimmory-mcp
|
||||||
make deploy-<service> # playbooks/pve-<service>.yml
|
|
||||||
make dry-<service> # то же с --check --diff
|
|
||||||
make update-<service> # gitea | vaultwarden | adguard | mihomo | grimmory
|
|
||||||
make reverse-proxy # Caddy на ru-vps по реестру сервисов
|
|
||||||
make backup-audit # аудит PBS + offsite restic
|
|
||||||
make openvpn-check # проверка транспорта ru-vps <-> ovpn-mini
|
|
||||||
make bash-config # единый bash-конфиг на shell_hosts
|
|
||||||
make user-ssh-key # разложить публичный ключ на все хосты
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Обновления всегда идут в порядке: свежий бэкап/аудит -> обновление -> health-check.
|
## Safety Contract
|
||||||
Образы закреплены по `tag@sha256:digest`; floating-теги и авто-апдейтеры не используются.
|
|
||||||
## Proxmox API Tasks
|
|
||||||
|
|
||||||
Плейбукам `pve-*.yml` нужны переменные Proxmox API. Держи их в `ansible/.env`
|
- Сначала выполняй smallest safe local validation, затем bounded live check только
|
||||||
(файл в `.gitignore`, шаблон — `.env.example`):
|
когда он нужен задаче.
|
||||||
|
- `make dry-<service>` использует `--check --diff`, но не является полной симуляцией
|
||||||
|
Proxmox API или command-heavy `pct` playbooks.
|
||||||
|
- `make status` read-only, но его exit code не является health gate.
|
||||||
|
- `update-all`, `mihomo-harden` и frozen `monitoring` требуют `CONFIRM=1`.
|
||||||
|
- `gyro` требует Vault password; не обходи `make gyro` без явной причины.
|
||||||
|
- Service update order: fresh backup/audit -> update -> health check. Backup не
|
||||||
|
означает automatic rollback.
|
||||||
|
- Uptime Kuma активен. Prometheus/Alertmanager/Grafana stack заморожен; не запускать
|
||||||
|
его параллельно без отдельного architecture decision.
|
||||||
|
- Active remote images обычно pin по `tag@sha256:digest`; frozen Prometheus images
|
||||||
|
tag-only. Floating auto-updaters не используются.
|
||||||
|
- Не включать cluster-wide PVE firewall без аудита всех guests с `firewall=1`.
|
||||||
|
- Не останавливать production services для fault injection без согласованного
|
||||||
|
maintenance window.
|
||||||
|
|
||||||
```bash
|
## Secrets
|
||||||
PROXMOX_HOST=<host>
|
|
||||||
PROXMOX_USER=<user@realm>
|
|
||||||
PROXMOX_TOKEN_ID=<token_id>
|
|
||||||
PROXMOX_TOKEN_SECRET=<secret>
|
|
||||||
PROXMOX_VALIDATE_CERTS=false
|
|
||||||
```
|
|
||||||
|
|
||||||
`make` подхватывает `.env` сам и падает с внятным сообщением, если его нет —
|
- Никогда не коммить и не цитировать реальные passwords, tokens, private keys,
|
||||||
руками `. ./.env` делать не нужно:
|
`.env`, Vault plaintext, PBS/restic credentials или TLS keys.
|
||||||
|
- Используй ignored `.env` в корне репозитория, Ansible Vault, runtime prompt или
|
||||||
|
external local secret file.
|
||||||
|
- `PROXMOX_ROOT_PASSWORD` (root@pam) нужен ТОЛЬКО целям `tofu-*` и только для
|
||||||
|
привилегированных полей LXC. Ansible им не пользуется. Не логировать и не
|
||||||
|
передавать в playbook vars.
|
||||||
|
- В Git допустимы только sanitized `.env.example` и encrypted Vault content.
|
||||||
|
- Secret-bearing tasks должны использовать `no_log: true`, а files - минимальные
|
||||||
|
permissions.
|
||||||
|
|
||||||
```bash
|
## Рабочий процесс
|
||||||
make env-check # проверить, что .env на месте и заполнен
|
|
||||||
make dry-gitea # сначала всегда --check --diff
|
|
||||||
make deploy-gitea
|
|
||||||
```
|
|
||||||
|
|
||||||
Выпустить токен, если его ещё нет:
|
1. Прочитай минимальный релевантный context и Obsidian notes.
|
||||||
|
2. Проверь active inventory, vars, playbook/role и связанные consumers.
|
||||||
|
3. Внеси smallest correct declarative change; не используй ad-hoc server edits.
|
||||||
|
4. Запусти минимальные local checks, затем только необходимые bounded live checks.
|
||||||
|
5. Проверь diff на broad targeting, secrets, destructive behavior и docs drift.
|
||||||
|
6. Обнови repository/Obsidian documentation для изменившихся решений и процедур.
|
||||||
|
7. В отчете перечисли changed files, проверки, неисполненные live checks и unknowns.
|
||||||
|
|
||||||
```bash
|
Для read-only reconnaissance, Ansible safety review, syntax validation, network/log
|
||||||
make bootstrap-pve-token # прямо с ноды, нужен sudo
|
diagnostics, backup audit и Obsidian context используй специализированные агенты из
|
||||||
make bootstrap-monitoring-token # отдельный read-only токен для мониторинга
|
`.opencode/agents/`. Большие logs и command outputs передавай соответствующему
|
||||||
```
|
read-only summarizer и всегда ограничивай `--since`, `-n` или `--tail`.
|
||||||
|
|
||||||
## Secrets Policy
|
|
||||||
|
|
||||||
- Never commit real secrets to git.
|
|
||||||
- Use `.env` files (in `.gitignore`), Ansible vault, or runtime prompt.
|
|
||||||
- Store `.env.example` templates in repo.
|
|
||||||
- Keep: passwords, tokens, private keys, PBS secrets, TLS keys outside repo.
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
1. **Read Obsidian** — understand context, constraints, prior decisions.
|
|
||||||
2. **Make change** — edit/add role/playbook in `ansible/`.
|
|
||||||
3. **Test safe** — run smallest applicable check/playbook.
|
|
||||||
4. **Update Obsidian** — document non-obvious decisions and changes.
|
|
||||||
|
|
||||||
## Archive Policy
|
|
||||||
|
|
||||||
- `archive/2026-07-proxmox-migration/` contains historical NixOS, Docker Compose, GitOps configs.
|
|
||||||
- Use archived files only as reference when porting behavior to Ansible.
|
|
||||||
- Do not edit archived files for active infrastructure.
|
|
||||||
- Restore services via new Ansible roles/playbooks, not by moving old files back.
|
|
||||||
|
|
||||||
## Token Economy
|
|
||||||
|
|
||||||
Принципы минимизации токенов при управлении инфраструктурой.
|
|
||||||
|
|
||||||
### Delegate Heavy Reading
|
|
||||||
|
|
||||||
Когда нужно анализировать большие логи или выводы — делегируй субагенту:
|
|
||||||
|
|
||||||
```
|
|
||||||
agent("Read this log and summarize key errors")
|
|
||||||
```
|
|
||||||
|
|
||||||
**Субагент использует для:**
|
|
||||||
- Анализа логов (journalctl, docker logs, PBS logs)
|
|
||||||
- Summarization больших файлов
|
|
||||||
- Поиска паттернов в выводах
|
|
||||||
- Диагностики статусов
|
|
||||||
|
|
||||||
**Основной агент для:**
|
|
||||||
- Принятия решений на основе конспекта
|
|
||||||
- Сложных рассуждений над findings
|
|
||||||
- Изменения кода и архитектуры
|
|
||||||
|
|
||||||
### Prefer Scripts Over LLM
|
|
||||||
|
|
||||||
Для повторяющихся задач — скрипты, а не объяснения:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Вместо: "проверь статус всех LXC"
|
|
||||||
ansible/scripts/check-lxc-status.sh
|
|
||||||
|
|
||||||
# Вместо: "верифицируй PBS backup jobs"
|
|
||||||
ansible/scripts/check-pbs-backups.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Паттерн: написать один раз → переиспользовать. LLM только пишет или вызывает.
|
|
||||||
|
|
||||||
### Focused Context — читай только нужное
|
|
||||||
|
|
||||||
Вместо чтения всего файла — только релевантные части:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Вместо всего inventory.yml
|
|
||||||
Read(file_path, offset=1, limit=50) # только hosts vars
|
|
||||||
|
|
||||||
# Или grep для конкретного хоста
|
|
||||||
grep("mini-pc", "inventory/hosts.yml")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tool Limits — ограничивай вывод команд
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Вместо: journalctl -u openvpn (тысячи строк)
|
|
||||||
journalctl -u openvpn --since "1 hour ago" -n 100
|
|
||||||
|
|
||||||
# Вместо: docker logs (весь лог)
|
|
||||||
docker logs --tail 50 gitea
|
|
||||||
```
|
|
||||||
|
|
||||||
Всегда ограничивай вывод: `--tail`, `--since`, `-n`, `head`.
|
|
||||||
|
|
||||||
### Batch Operations — группируй задачи
|
|
||||||
|
|
||||||
Вместо нескольких запусков — один с несколькими role:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Плохо
|
|
||||||
ansible-playbook playbooks/bash-config.yml
|
|
||||||
ansible-playbook playbooks/docker.yml
|
|
||||||
ansible-playbook playbooks/ufw.yml
|
|
||||||
|
|
||||||
# Хорошо — один плейбук
|
|
||||||
ansible-playbook playbooks/base-setup.yml # включает bash, docker, ufw
|
|
||||||
```
|
|
||||||
|
|
||||||
### State Caching — не перечитывай статичное
|
|
||||||
|
|
||||||
Структура инфры меняется редко. Не перечитывай `hosts.yml`, если структура не изменилась. Кэшируй в memory стабильные данные: network topology, storage layout.
|
|
||||||
|
|
||||||
### Structured Output — избегай повторного парсинга
|
|
||||||
|
|
||||||
Когда субагент анализирует логи — проси сразу JSON с findings:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"summary": "OpenVPN handshake fails due to certificate expired",
|
|
||||||
"findings": ["cert expired 2026-07-01", "client retries every 5s"],
|
|
||||||
"relevant_lines": ["Jul 08 10:23:01 TLS auth error"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Работа со структурированным результатом вместо повторного чтения логов.
|
|
||||||
|
|
||||||
### Declarative Over Imperative
|
|
||||||
|
|
||||||
Описывай желаемое состояние, а не шаги:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# Плохо: каждый раз перечислять шаги
|
|
||||||
"Создай LXC, установи Docker, добавь пользователя..."
|
|
||||||
|
|
||||||
# Хорошо: декларативно
|
|
||||||
lxc_container:
|
|
||||||
name: "vaultwarden"
|
|
||||||
features: ["docker", "autostart"]
|
|
||||||
user: "ansible"
|
|
||||||
```
|
|
||||||
|
|
||||||
Ansible/Terraform сами разберут шаги.
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
Активная инфраструктура домашней лаборатории управляется через Ansible.
|
Активная инфраструктура домашней лаборатории управляется через Ansible.
|
||||||
Каноничные инструкции для людей и агентов — в [AGENTS.md](./AGENTS.md).
|
Каноничные инструкции для людей и агентов — в [AGENTS.md](./AGENTS.md).
|
||||||
|
Стабильный архитектурный контекст и риски — в [docs/ai/](./docs/ai/README.md).
|
||||||
|
|
||||||
## Быстрый старт
|
## Быстрый старт
|
||||||
|
|
||||||
@@ -32,7 +33,8 @@ make deploy-gitea # применить
|
|||||||
make update-gitea # бэкап -> обновление -> health-check
|
make update-gitea # бэкап -> обновление -> health-check
|
||||||
```
|
```
|
||||||
|
|
||||||
`.env` с Proxmox-токенами подхватывается автоматически. Опасные цели требуют `CONFIRM=1`.
|
Секреты лежат в `.env` в **корне репозитория** (в `.gitignore`); его подхватывают
|
||||||
|
и Ansible, и OpenTofu. Опасные цели требуют `CONFIRM=1`.
|
||||||
|
|
||||||
## SSH руками
|
## SSH руками
|
||||||
|
|
||||||
@@ -49,12 +51,13 @@ Include /home/ada/Documents/Projects/HomeLab/infras/ansible/ssh_config
|
|||||||
- `ansible/inventory/group_vars/all/services.yml` — реестр сервисов (VMID, IP, порты, домены, образы)
|
- `ansible/inventory/group_vars/all/services.yml` — реестр сервисов (VMID, IP, порты, домены, образы)
|
||||||
- `flake.nix` — dev-окружение
|
- `flake.nix` — dev-окружение
|
||||||
- `.gitea/workflows/lint.yml` — CI: yamllint, ansible-lint, syntax-check
|
- `.gitea/workflows/lint.yml` — CI: yamllint, ansible-lint, syntax-check
|
||||||
|
- `docs/ai/` — архитектура, stack, edge cases и legacy boundaries для агентов
|
||||||
- `archive/2026-07-proxmox-migration/` — исторические NixOS/Docker конфиги, только как справка
|
- `archive/2026-07-proxmox-migration/` — исторические NixOS/Docker конфиги, только как справка
|
||||||
|
|
||||||
## Grimmory MCP
|
## Grimmory MCP
|
||||||
|
|
||||||
`tools/grimmory-mcp/` содержит read-only интеграцию с Grimmory API для OpenCode
|
`tools/grimmory-mcp/` содержит read-only интеграцию с Grimmory API для OpenCode.
|
||||||
и явно вызываемые инструменты синхронизации с Obsidian.
|
Явно вызываемые sync tools записывают сгенерированные заметки и обложки в Obsidian.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install --prefix tools/grimmory-mcp
|
npm install --prefix tools/grimmory-mcp
|
||||||
@@ -62,6 +65,6 @@ npm run configure --prefix tools/grimmory-mcp
|
|||||||
npm test --prefix tools/grimmory-mcp
|
npm test --prefix tools/grimmory-mcp
|
||||||
```
|
```
|
||||||
|
|
||||||
OpenCode регистрирует сервер глобально. После перезапуска OpenCode используй
|
Глобальная регистрация MCP в OpenCode выполняется вне этого репозитория. После
|
||||||
`/grimmory-sync` для обновления заметок книг в `90 Library/Books` и обложек
|
настройки используй `/grimmory-sync` для обновления заметок книг в
|
||||||
в `99 System/Export/Grimmory/Covers`.
|
`90 Library/Books` и обложек в `99 System/Export/Grimmory/Covers`.
|
||||||
|
|||||||
+124
-11
@@ -24,6 +24,7 @@ SHELL := /bin/sh
|
|||||||
|
|
||||||
MAKEFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST)))
|
MAKEFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST)))
|
||||||
ANSIBLE_DIR := $(patsubst %/,%,$(dir $(MAKEFILE_PATH)))
|
ANSIBLE_DIR := $(patsubst %/,%,$(dir $(MAKEFILE_PATH)))
|
||||||
|
REPO_ROOT := $(abspath $(ANSIBLE_DIR)/..)
|
||||||
VENV_BIN := $(ANSIBLE_DIR)/.venv/bin
|
VENV_BIN := $(ANSIBLE_DIR)/.venv/bin
|
||||||
|
|
||||||
# Разрешение бинарей: сначала рабочий локальный venv, иначе PATH (nix devshell,
|
# Разрешение бинарей: сначала рабочий локальный venv, иначе PATH (nix devshell,
|
||||||
@@ -42,7 +43,11 @@ YAMLLINT ?= $(call venv_bin,yamllint)
|
|||||||
# python3 из PATH, а не из .venv.
|
# python3 из PATH, а не из .venv.
|
||||||
PYTHON ?= python3
|
PYTHON ?= python3
|
||||||
|
|
||||||
ENV_FILE ?= .env
|
# Секреты живут в КОРНЕ репозитория, а не в ansible/: их потребляет не только
|
||||||
|
# Ansible, но и OpenTofu (tofu/), а держать два файла или ходить в подкаталог
|
||||||
|
# из соседнего инструмента неудобно. Путь абсолютный, поэтому цели работают
|
||||||
|
# из любого cwd.
|
||||||
|
ENV_FILE ?= $(REPO_ROOT)/.env
|
||||||
|
|
||||||
# Дополнительные аргументы ansible-playbook для любой цели.
|
# Дополнительные аргументы ansible-playbook для любой цели.
|
||||||
EXTRA ?=
|
EXTRA ?=
|
||||||
@@ -51,19 +56,19 @@ ASK_BECOME ?= -K
|
|||||||
# Спрашивать пароль Ansible Vault. `make gyro ASK_VAULT=` отключает.
|
# Спрашивать пароль Ansible Vault. `make gyro ASK_VAULT=` отключает.
|
||||||
ASK_VAULT ?= --ask-vault-pass
|
ASK_VAULT ?= --ask-vault-pass
|
||||||
|
|
||||||
# Строгая загрузка ansible/.env: обязательна для Proxmox API и секретов.
|
# Строгая загрузка корневого .env: обязательна для Proxmox API и секретов.
|
||||||
# Каждая строка рецепта make — отдельный шелл, поэтому source и запуск идут одной строкой.
|
# Каждая строка рецепта make — отдельный шелл, поэтому source и запуск идут одной строкой.
|
||||||
REQUIRE_ENV = if [ ! -f '$(ENV_FILE)' ]; then \
|
REQUIRE_ENV = if [ ! -f '$(ENV_FILE)' ]; then \
|
||||||
printf 'ОШИБКА: не найден %s/%s\n' '$(ANSIBLE_DIR)' '$(ENV_FILE)' >&2; \
|
printf 'ОШИБКА: не найден %s\n' '$(ENV_FILE)' >&2; \
|
||||||
printf 'Создай его и заполни реальными значениями:\n' >&2; \
|
printf 'Создай его и заполни реальными значениями:\n' >&2; \
|
||||||
printf ' cp .env.example .env\n' >&2; \
|
printf ' cp %s/.env.example %s\n' '$(REPO_ROOT)' '$(ENV_FILE)' >&2; \
|
||||||
printf 'Нужны PROXMOX_* (и MONITORING_* / EMERGENCY_* для профильных целей).\n' >&2; \
|
printf 'Нужны PROXMOX_* (и MONITORING_* / EMERGENCY_* для профильных целей).\n' >&2; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi; \
|
fi; \
|
||||||
set -a; . './$(ENV_FILE)'; set +a
|
set -a; . '$(ENV_FILE)'; set +a
|
||||||
|
|
||||||
# Мягкая загрузка: .env подхватывается если есть, иначе просто предупреждение.
|
# Мягкая загрузка: .env подхватывается если есть, иначе просто предупреждение.
|
||||||
LOAD_ENV = if [ -f '$(ENV_FILE)' ]; then set -a; . './$(ENV_FILE)'; set +a; \
|
LOAD_ENV = if [ -f '$(ENV_FILE)' ]; then set -a; . '$(ENV_FILE)'; set +a; \
|
||||||
else printf 'ВНИМАНИЕ: %s не найден, продолжаю без него.\n' '$(ENV_FILE)' >&2; fi
|
else printf 'ВНИМАНИЕ: %s не найден, продолжаю без него.\n' '$(ENV_FILE)' >&2; fi
|
||||||
|
|
||||||
# Защита от случайного запуска опасных плейбуков.
|
# Защита от случайного запуска опасных плейбуков.
|
||||||
@@ -85,7 +90,7 @@ help: ## Показать этот список целей
|
|||||||
print "HomeLab Ansible — ручное управление инфраструктурой"; \
|
print "HomeLab Ansible — ручное управление инфраструктурой"; \
|
||||||
print ""; \
|
print ""; \
|
||||||
print " Использование: make <цель> [EXTRA=\"--limit host -vv\"]"; \
|
print " Использование: make <цель> [EXTRA=\"--limit host -vv\"]"; \
|
||||||
print " Плейбуки Proxmox API сами подхватывают ./.env"; \
|
print " Секреты берутся из .env в КОРНЕ репозитория"; \
|
||||||
} \
|
} \
|
||||||
/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5); next } \
|
/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5); next } \
|
||||||
/^[a-zA-Z0-9_%.-]+:.*##/ { printf " \033[36m%-24s\033[0m %s\n", $$1, $$2 } \
|
/^[a-zA-Z0-9_%.-]+:.*##/ { printf " \033[36m%-24s\033[0m %s\n", $$1, $$2 } \
|
||||||
@@ -126,10 +131,21 @@ check: ## Проверка связности и ожидаемых IP (playbook
|
|||||||
status: ## Сводный статус инфраструктуры (playbooks/status.yml)
|
status: ## Сводный статус инфраструктуры (playbooks/status.yml)
|
||||||
ANSIBLE_CALLBACK_RESULT_FORMAT=yaml $(ANSIBLE_PLAYBOOK) playbooks/status.yml $(EXTRA)
|
ANSIBLE_CALLBACK_RESULT_FORMAT=yaml $(ANSIBLE_PLAYBOOK) playbooks/status.yml $(EXTRA)
|
||||||
|
|
||||||
|
# Оба линтера ищут конфиг (.ansible-lint / .yamllint) в текущем каталоге, а не
|
||||||
|
# у родителей. Запуск из ansible/ конфиг не находил, из-за чего в проверку
|
||||||
|
# затягивался вендоренный collections/ и цель выдавала около тысячи чужих
|
||||||
|
# нарушений. Запускаем из корня — ровно как .gitea/workflows/lint.yml.
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
lint: ## Прогнать ansible-lint и yamllint по репозиторию
|
lint: ## Прогнать ansible-lint, yamllint и syntax-check — то же, что делает CI
|
||||||
$(ANSIBLE_LINT)
|
cd '$(REPO_ROOT)' && $(ANSIBLE_LINT)
|
||||||
$(YAMLLINT) .
|
cd '$(REPO_ROOT)' && $(YAMLLINT) .
|
||||||
|
@rc=0; for f in playbooks/*.yml; do \
|
||||||
|
if $(ANSIBLE_PLAYBOOK) --syntax-check "$$f" >/dev/null 2>&1; then \
|
||||||
|
printf 'ok %s\n' "$$f"; \
|
||||||
|
else \
|
||||||
|
rc=1; printf 'FAIL %s\n' "$$f"; $(ANSIBLE_PLAYBOOK) --syntax-check "$$f" 2>&1 | sed 's/^/ /'; \
|
||||||
|
fi; \
|
||||||
|
done; exit $$rc
|
||||||
|
|
||||||
.PHONY: docs
|
.PHONY: docs
|
||||||
docs: ## Напечатать markdown-таблицу хостов и групп в stdout
|
docs: ## Напечатать markdown-таблицу хостов и групп в stdout
|
||||||
@@ -139,6 +155,10 @@ docs: ## Напечатать markdown-таблицу хостов и групп
|
|||||||
inventory: ## Показать дерево инвентаря (ansible-inventory --graph)
|
inventory: ## Показать дерево инвентаря (ansible-inventory --graph)
|
||||||
@$(INVENTORY_ENV) $(ANSIBLE_INVENTORY_BIN) -i inventory/hosts.yml --graph
|
@$(INVENTORY_ENV) $(ANSIBLE_INVENTORY_BIN) -i inventory/hosts.yml --graph
|
||||||
|
|
||||||
|
.PHONY: validate
|
||||||
|
validate: ## Сверить реестр homelab_services с фактическим состоянием Proxmox (gate)
|
||||||
|
$(ANSIBLE_PLAYBOOK) playbooks/validate.yml $(EXTRA)
|
||||||
|
|
||||||
.PHONY: openvpn-check
|
.PHONY: openvpn-check
|
||||||
openvpn-check: ## Проверить OpenVPN-транспорт ru-vps <-> ovpn-mini
|
openvpn-check: ## Проверить OpenVPN-транспорт ru-vps <-> ovpn-mini
|
||||||
$(ANSIBLE_PLAYBOOK) playbooks/openvpn-check.yml $(EXTRA)
|
$(ANSIBLE_PLAYBOOK) playbooks/openvpn-check.yml $(EXTRA)
|
||||||
@@ -159,6 +179,10 @@ deploy-%: ## Применить playbooks/pve-<имя>.yml (пример: make d
|
|||||||
backup-jobs: ## Настроить PBS backup jobs (playbooks/pve-backup-jobs.yml)
|
backup-jobs: ## Настроить PBS backup jobs (playbooks/pve-backup-jobs.yml)
|
||||||
@$(REQUIRE_ENV); $(ANSIBLE_PLAYBOOK) playbooks/pve-backup-jobs.yml $(EXTRA)
|
@$(REQUIRE_ENV); $(ANSIBLE_PLAYBOOK) playbooks/pve-backup-jobs.yml $(EXTRA)
|
||||||
|
|
||||||
|
.PHONY: pbs-storage
|
||||||
|
pbs-storage: ## Убрать storage-level prune policy с PBS storage (playbooks/pve-storage-pbs.yml)
|
||||||
|
$(ANSIBLE_PLAYBOOK) playbooks/pve-storage-pbs.yml $(EXTRA)
|
||||||
|
|
||||||
.PHONY: bootstrap-pve-token
|
.PHONY: bootstrap-pve-token
|
||||||
bootstrap-pve-token: ## Выпустить Proxmox API-токен прямо с ноды (нужен sudo)
|
bootstrap-pve-token: ## Выпустить Proxmox API-токен прямо с ноды (нужен sudo)
|
||||||
$(ANSIBLE_PLAYBOOK) playbooks/bootstrap-pve-api-token.yml $(ASK_BECOME) $(EXTRA)
|
$(ANSIBLE_PLAYBOOK) playbooks/bootstrap-pve-api-token.yml $(ASK_BECOME) $(EXTRA)
|
||||||
@@ -167,12 +191,24 @@ bootstrap-pve-token: ## Выпустить Proxmox API-токен прямо с
|
|||||||
bootstrap-monitoring-token: ## Выпустить read-only PVE-токен для мониторинга
|
bootstrap-monitoring-token: ## Выпустить read-only PVE-токен для мониторинга
|
||||||
$(ANSIBLE_PLAYBOOK) playbooks/bootstrap-monitoring-pve-token.yml $(EXTRA)
|
$(ANSIBLE_PLAYBOOK) playbooks/bootstrap-monitoring-pve-token.yml $(EXTRA)
|
||||||
|
|
||||||
|
.PHONY: bootstrap-dashboard-token
|
||||||
|
bootstrap-dashboard-token: ## Выпустить read-only PVE-токен homepage@pve!dashboard для виджета Proxmox
|
||||||
|
$(ANSIBLE_PLAYBOOK) playbooks/bootstrap-dashboard-pve-token.yml $(EXTRA)
|
||||||
|
|
||||||
.PHONY: bootstrap-ansible-user
|
.PHONY: bootstrap-ansible-user
|
||||||
bootstrap-ansible-user: ## Завести сервисный аккаунт ansible на shell-хостах (нужен sudo)
|
bootstrap-ansible-user: ## Завести сервисный аккаунт ansible на shell-хостах (нужен sudo)
|
||||||
$(ANSIBLE_PLAYBOOK) playbooks/bootstrap-ansible-user.yml $(ASK_BECOME) $(EXTRA)
|
$(ANSIBLE_PLAYBOOK) playbooks/bootstrap-ansible-user.yml $(ASK_BECOME) $(EXTRA)
|
||||||
|
|
||||||
##@ Настройка сервисов
|
##@ Настройка сервисов
|
||||||
|
|
||||||
|
.PHONY: ru-vps-base
|
||||||
|
ru-vps-base: ## Базовое состояние ru-vps и стек Caddy (закрепляет образ, ставит homelab-caddy.service)
|
||||||
|
$(ANSIBLE_PLAYBOOK) playbooks/ru-vps-base.yml $(EXTRA)
|
||||||
|
|
||||||
|
.PHONY: dry-ru-vps-base
|
||||||
|
dry-ru-vps-base: ## Предпросмотр ru-vps-base.yml (--check --diff)
|
||||||
|
$(ANSIBLE_PLAYBOOK) playbooks/ru-vps-base.yml --check --diff $(EXTRA)
|
||||||
|
|
||||||
.PHONY: reverse-proxy
|
.PHONY: reverse-proxy
|
||||||
reverse-proxy: ## Единый reverse-proxy для gitea/vaultwarden/grimmory
|
reverse-proxy: ## Единый reverse-proxy для gitea/vaultwarden/grimmory
|
||||||
@$(LOAD_ENV); $(ANSIBLE_PLAYBOOK) playbooks/reverse-proxy.yml $(EXTRA)
|
@$(LOAD_ENV); $(ANSIBLE_PLAYBOOK) playbooks/reverse-proxy.yml $(EXTRA)
|
||||||
@@ -198,13 +234,21 @@ emergency-access: ## Настроить emergency reverse-SSH и Telegram-бот
|
|||||||
@$(REQUIRE_ENV); $(ANSIBLE_PLAYBOOK) playbooks/emergency-access.yml $(EXTRA)
|
@$(REQUIRE_ENV); $(ANSIBLE_PLAYBOOK) playbooks/emergency-access.yml $(EXTRA)
|
||||||
|
|
||||||
.PHONY: gyro
|
.PHONY: gyro
|
||||||
gyro: ## Настроить gyro-аллокатор в CT 150 (спрашивает пароль Vault)
|
gyro: ## Настроить gyro-аллокатор в production CT 156 (Tofu-managed); CT 150 — остановленный rollback (спрашивает пароль Vault)
|
||||||
@$(REQUIRE_ENV); $(ANSIBLE_PLAYBOOK) playbooks/gyro.yml $(ASK_VAULT) $(EXTRA)
|
@$(REQUIRE_ENV); $(ANSIBLE_PLAYBOOK) playbooks/gyro.yml $(ASK_VAULT) $(EXTRA)
|
||||||
|
|
||||||
.PHONY: uptime-kuma
|
.PHONY: uptime-kuma
|
||||||
uptime-kuma: ## Развернуть/обновить Uptime Kuma на monitoring LXC
|
uptime-kuma: ## Развернуть/обновить Uptime Kuma на monitoring LXC
|
||||||
$(ANSIBLE_PLAYBOOK) playbooks/uptime-kuma.yml $(EXTRA)
|
$(ANSIBLE_PLAYBOOK) playbooks/uptime-kuma.yml $(EXTRA)
|
||||||
|
|
||||||
|
.PHONY: dashboard
|
||||||
|
dashboard: ## Дашборд-обзор инфраструктуры (Homepage) на monitoring LXC, конфиг из реестра
|
||||||
|
@$(LOAD_ENV); $(ANSIBLE_PLAYBOOK) playbooks/dashboard.yml $(EXTRA)
|
||||||
|
|
||||||
|
.PHONY: dry-dashboard
|
||||||
|
dry-dashboard: ## Предпросмотр dashboard.yml (--check --diff)
|
||||||
|
@$(LOAD_ENV); $(ANSIBLE_PLAYBOOK) playbooks/dashboard.yml --check --diff $(EXTRA)
|
||||||
|
|
||||||
.PHONY: offsite-restic
|
.PHONY: offsite-restic
|
||||||
offsite-restic: ## Настроить offsite restic-бэкапы на Yandex.Disk
|
offsite-restic: ## Настроить offsite restic-бэкапы на Yandex.Disk
|
||||||
@$(LOAD_ENV); $(ANSIBLE_PLAYBOOK) playbooks/offsite-restic-yadisk.yml $(EXTRA)
|
@$(LOAD_ENV); $(ANSIBLE_PLAYBOOK) playbooks/offsite-restic-yadisk.yml $(EXTRA)
|
||||||
@@ -226,6 +270,65 @@ update-all: ## Последовательно обновить gitea, vaultwarde
|
|||||||
$(MAKE) update-mihomo
|
$(MAKE) update-mihomo
|
||||||
$(MAKE) update-grimmory
|
$(MAKE) update-grimmory
|
||||||
|
|
||||||
|
##@ OpenTofu (пилот provisioning LXC)
|
||||||
|
|
||||||
|
TOFU_DIR := $(REPO_ROOT)/tofu
|
||||||
|
TOFU ?= tofu
|
||||||
|
|
||||||
|
# Tofu, в отличие от Ansible, ходит в Proxmox API напрямую по HTTPS и НЕ умеет
|
||||||
|
# ProxyJump из ssh_config. Вне локальной сети узлы недоступны (проверено:
|
||||||
|
# https://192.168.1.10:8006 отдаёт connection failure), поэтому на время команды
|
||||||
|
# поднимаем SSH-туннель через ru-vps и направляем провайдер в localhost.
|
||||||
|
# Изнутри LAN это тоже работает — просто лишний хоп, зато поведение одинаково.
|
||||||
|
TOFU_TUNNEL_PORT ?= 18006
|
||||||
|
TOFU_SSH_SOCKET := $(ANSIBLE_DIR)/.ansible/tofu-tunnel.sock
|
||||||
|
|
||||||
|
# insecure=true здесь не настройка вкуса: сертификат Proxmox выписан на узел,
|
||||||
|
# а обращаемся мы к 127.0.0.1, так что проверка имени не пройдёт в любом случае.
|
||||||
|
#
|
||||||
|
# Способ аутентификации выбирается автоматически: если в корневом .env задан
|
||||||
|
# PROXMOX_ROOT_PASSWORD — идём как root@pam (только так Proxmox разрешает
|
||||||
|
# features кроме nesting, device passthrough и bind mount каталога хоста),
|
||||||
|
# иначе токеном ansible@pve. Выбранный режим печатается в stderr, чтобы из
|
||||||
|
# вывода было видно, какими правами шёл apply.
|
||||||
|
TOFU_ENV = $(REQUIRE_ENV); \
|
||||||
|
mkdir -p '$(dir $(TOFU_SSH_SOCKET))'; \
|
||||||
|
ssh -F '$(ANSIBLE_DIR)/ssh_config' -M -S '$(TOFU_SSH_SOCKET)' -fN \
|
||||||
|
-L $(TOFU_TUNNEL_PORT):$$PROXMOX_HOST:$${PROXMOX_PORT:-8006} ru-vps; \
|
||||||
|
trap "ssh -S '$(TOFU_SSH_SOCKET)' -O exit ru-vps 2>/dev/null || true" EXIT; \
|
||||||
|
export TF_VAR_pve_endpoint="https://127.0.0.1:$(TOFU_TUNNEL_PORT)"; \
|
||||||
|
export TF_VAR_pve_insecure=true; \
|
||||||
|
if [ -n "$${PROXMOX_ROOT_PASSWORD:-}" ] && [ "$${PROXMOX_ROOT_PASSWORD}" != 'replace-me' ]; then \
|
||||||
|
export TF_VAR_pve_username="$${PROXMOX_ROOT_USER:-root@pam}"; \
|
||||||
|
export TF_VAR_pve_password="$$PROXMOX_ROOT_PASSWORD"; \
|
||||||
|
export TF_VAR_pve_api_token=""; \
|
||||||
|
printf 'Proxmox: аутентификация %s (привилегированный режим)\n' "$${PROXMOX_ROOT_USER:-root@pam}" >&2; \
|
||||||
|
else \
|
||||||
|
export TF_VAR_pve_api_token="$$PROXMOX_USER!$$PROXMOX_TOKEN_ID=$$PROXMOX_TOKEN_SECRET"; \
|
||||||
|
export TF_VAR_pve_username=""; \
|
||||||
|
export TF_VAR_pve_password=""; \
|
||||||
|
printf 'Proxmox: аутентификация токеном %s (features кроме nesting и dev-passthrough недоступны)\n' "$$PROXMOX_USER" >&2; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
.PHONY: tofu-init
|
||||||
|
tofu-init: ## Скачать провайдер и инициализировать рабочий каталог tofu/
|
||||||
|
@cd '$(TOFU_DIR)' && $(TOFU) init -input=false
|
||||||
|
|
||||||
|
.PHONY: tofu-plan
|
||||||
|
tofu-plan: ## Показать план (read-only, ничего не меняет)
|
||||||
|
@$(TOFU_ENV); cd '$(TOFU_DIR)' && $(TOFU) plan -input=false
|
||||||
|
|
||||||
|
.PHONY: tofu-apply
|
||||||
|
tofu-apply: ## Применить план — СОЗДАЁТ гостей в Proxmox (требует CONFIRM=1)
|
||||||
|
@$(REQUIRE_CONFIRM)
|
||||||
|
@$(TOFU_ENV); cd '$(TOFU_DIR)' && $(TOFU) apply -input=false -auto-approve
|
||||||
|
|
||||||
|
.PHONY: tofu-destroy
|
||||||
|
tofu-destroy: ## УНИЧТОЖИТЬ всё, что создано в tofu/ (требует CONFIRM=1)
|
||||||
|
@$(REQUIRE_CONFIRM)
|
||||||
|
@printf 'Удаляет гостей Proxmox из состояния tofu вместе с их дисками.\n' >&2
|
||||||
|
@$(TOFU_ENV); cd '$(TOFU_DIR)' && $(TOFU) destroy -input=false -auto-approve
|
||||||
|
|
||||||
##@ Опасное (только осознанно, требует CONFIRM=1)
|
##@ Опасное (только осознанно, требует CONFIRM=1)
|
||||||
|
|
||||||
.PHONY: mihomo-harden
|
.PHONY: mihomo-harden
|
||||||
@@ -234,6 +337,16 @@ mihomo-harden: ## РОТИРУЕТ живые SOCKS-креды Mihomo на ru-vp
|
|||||||
@printf 'Ротация кредов необратима для клиентов без отката бэкапа конфига.\n' >&2
|
@printf 'Ротация кредов необратима для клиентов без отката бэкапа конфига.\n' >&2
|
||||||
$(ANSIBLE_PLAYBOOK) playbooks/ru-vps-mihomo-harden.yml -e ru_vps_mihomo_harden_confirm=true $(EXTRA)
|
$(ANSIBLE_PLAYBOOK) playbooks/ru-vps-mihomo-harden.yml -e ru_vps_mihomo_harden_confirm=true $(EXTRA)
|
||||||
|
|
||||||
|
.PHONY: zerotier-decommission
|
||||||
|
zerotier-decommission: ## Вывести ZeroTier с ru-vps и почистить его правила UFW (требует CONFIRM=1)
|
||||||
|
@$(REQUIRE_CONFIRM)
|
||||||
|
@printf 'Останавливает контейнер zerotier и удаляет правила UFW. Данные и файлы остаются.\n' >&2
|
||||||
|
$(ANSIBLE_PLAYBOOK) playbooks/ru-vps-zerotier-decommission.yml $(EXTRA)
|
||||||
|
|
||||||
|
.PHONY: dry-zerotier-decommission
|
||||||
|
dry-zerotier-decommission: ## Предпросмотр вывода ZeroTier (--check --diff)
|
||||||
|
$(ANSIBLE_PLAYBOOK) playbooks/ru-vps-zerotier-decommission.yml --check --diff $(EXTRA)
|
||||||
|
|
||||||
.PHONY: monitoring
|
.PHONY: monitoring
|
||||||
monitoring: ## ЗАМОРОЖЕН: стек Prometheus. Запускать только при восстановлении мониторинга
|
monitoring: ## ЗАМОРОЖЕН: стек Prometheus. Запускать только при восстановлении мониторинга
|
||||||
@$(REQUIRE_CONFIRM)
|
@$(REQUIRE_CONFIRM)
|
||||||
|
|||||||
+54
-41
@@ -12,8 +12,10 @@ Ansible is the control plane for HomeLab infrastructure changes.
|
|||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
- `inventory/hosts.yml` — canonical host list and host-specific facts.
|
- `inventory/hosts.yml` — canonical host list and host-specific facts.
|
||||||
|
- `inventory/group_vars/all/services.yml` — service registry and reverse-proxy input; deployment playbooks still duplicate these facts.
|
||||||
- `playbooks/` — entry points for tasks.
|
- `playbooks/` — entry points for tasks.
|
||||||
- `roles/` — reusable configuration units.
|
- `roles/` — reusable configuration units.
|
||||||
|
- `Makefile` — canonical manual entry point and safety gates.
|
||||||
|
|
||||||
## Current Groups
|
## Current Groups
|
||||||
|
|
||||||
@@ -24,38 +26,43 @@ Ansible is the control plane for HomeLab infrastructure changes.
|
|||||||
- `monitoring_exporters` — hosts exposing Node Exporter metrics.
|
- `monitoring_exporters` — hosts exposing Node Exporter metrics.
|
||||||
- `monitoring_smart_exporters` — Proxmox nodes exposing SMART metrics.
|
- `monitoring_smart_exporters` — Proxmox nodes exposing SMART metrics.
|
||||||
- `vpn_openvpn` — OpenVPN transport hosts: `ru-vps`, `ovpn-mini`.
|
- `vpn_openvpn` — OpenVPN transport hosts: `ru-vps`, `ovpn-mini`.
|
||||||
- `shell_hosts` — hosts with unified bash config: `ru-vps`, `cloud-pc`, `mini-pc`.
|
- `shell_hosts` — hosts with unified bash config: `ru-vps`, `cloud-pc`, `mini-pc`, `hermes-ai`.
|
||||||
- `servers` — all managed hosts.
|
- `servers` — all managed hosts.
|
||||||
|
|
||||||
## First Checks
|
## First Checks
|
||||||
|
|
||||||
Install control-node dependencies locally:
|
From the repository root, use the Nix environment and install Galaxy collections once per clone:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -m venv .venv
|
nix develop
|
||||||
. .venv/bin/activate
|
ansible-galaxy collection install -r ansible/requirements.yml -p ansible/collections
|
||||||
pip install -r requirements.txt
|
|
||||||
ansible-galaxy collection install -r requirements.yml -p collections
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Run from `ansible/`:
|
Run manual operations through Make from `ansible/`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ansible-playbook playbooks/check.yml
|
make help
|
||||||
|
make check
|
||||||
|
make status EXTRA="--limit '!gyro'"
|
||||||
|
make lint
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`make setup` remains a local venv fallback when Nix is unavailable.
|
||||||
|
|
||||||
## Controlled Updates
|
## Controlled Updates
|
||||||
|
|
||||||
Service updates are manual and use pinned `tag@sha256:digest` image references only; floating tags and auto-update agents are not used.
|
Service updates are manual. Active update-managed remote images are pinned as
|
||||||
|
`tag@sha256:digest`; the frozen Prometheus stack is tag-only. Floating
|
||||||
|
auto-update agents are not used.
|
||||||
|
|
||||||
Run the dedicated playbook for the target service from `ansible/`:
|
Run the dedicated Make target from `ansible/`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
.venv/bin/ansible-playbook playbooks/vaultwarden-update.yml
|
make update-vaultwarden
|
||||||
.venv/bin/ansible-playbook playbooks/gitea-update.yml
|
make update-gitea
|
||||||
.venv/bin/ansible-playbook playbooks/adguard-update.yml
|
make update-adguard
|
||||||
.venv/bin/ansible-playbook playbooks/mihomo-update.yml
|
make update-mihomo
|
||||||
.venv/bin/ansible-playbook playbooks/grimmory-update.yml
|
make update-grimmory
|
||||||
```
|
```
|
||||||
|
|
||||||
Update flow is always: fresh backup/audit first, then the update playbook, then health verification.
|
Update flow is always: fresh backup/audit first, then the update playbook, then health verification.
|
||||||
@@ -69,55 +76,62 @@ Update flow is always: fresh backup/audit first, then the update playbook, then
|
|||||||
Use the dedicated hardening playbook only when explicitly approved:
|
Use the dedicated hardening playbook only when explicitly approved:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
.venv/bin/ansible-playbook playbooks/ru-vps-mihomo-harden.yml -e ru_vps_mihomo_harden_confirm=true
|
make mihomo-harden CONFIRM=1
|
||||||
```
|
```
|
||||||
|
|
||||||
It rotates the live Mihomo SOCKS credentials on `ru-vps`, locks the proxy to loopback, and removes the public UFW exposure for ports `7890` and `7891`.
|
It rotates the live Mihomo SOCKS credentials on `ru-vps`, locks the proxy to loopback, and removes the public UFW exposure for ports `7890` and `7891`.
|
||||||
|
|
||||||
The rotated credentials are not recoverable for clients unless you roll back the saved config backup.
|
The rotated credentials are not recoverable for clients unless you roll back the saved config backup.
|
||||||
|
|
||||||
For Proxmox API playbooks, create ignored `.env` from `.env.example` and load it:
|
For Proxmox API playbooks, create ignored `.env` in the **repository root** from
|
||||||
|
`.env.example`. It is shared with OpenTofu (`tofu/`). Make loads it
|
||||||
|
automatically:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp ../.env.example ../.env # секреты живут в корне репозитория
|
||||||
. ./.env
|
make env-check
|
||||||
.venv/bin/ansible-playbook playbooks/pve-ovpn-mini.yml
|
make dry-ovpn-mini
|
||||||
|
make deploy-ovpn-mini
|
||||||
```
|
```
|
||||||
|
|
||||||
Or bootstrap the token from `mini-pc` with sudo:
|
Or bootstrap the token from `mini-pc` with sudo:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
.venv/bin/ansible-playbook playbooks/bootstrap-pve-api-token.yml -K
|
make bootstrap-pve-token
|
||||||
```
|
```
|
||||||
|
|
||||||
Create the separate read-only PVE token used by the monitoring exporter:
|
Create the separate read-only PVE token used by the monitoring exporter:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
.venv/bin/ansible-playbook playbooks/bootstrap-monitoring-pve-token.yml
|
make bootstrap-monitoring-token
|
||||||
```
|
```
|
||||||
|
|
||||||
OpenVPN transport:
|
OpenVPN transport:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
.venv/bin/ansible-playbook playbooks/openvpn-vps-mini.yml -K
|
make openvpn
|
||||||
.venv/bin/ansible-playbook playbooks/openvpn-check.yml
|
make openvpn-check
|
||||||
```
|
```
|
||||||
|
|
||||||
Monitoring is provisioned in two steps after loading the monitoring secrets from ignored `.env` or Ansible Vault:
|
CT 146 can be provisioned separately. Uptime Kuma is the active monitoring service:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
. ./.env
|
make deploy-monitoring
|
||||||
.venv/bin/ansible-playbook playbooks/pve-monitoring.yml
|
make uptime-kuma
|
||||||
.venv/bin/ansible-playbook playbooks/monitoring.yml
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`pve-monitoring.yml` creates CT `146` (`monitoring`, `192.168.1.30`) on `cloud-pc`. `monitoring.yml` configures exporters, the `ru-vps` probe vantage point, and the central Prometheus stack. It is frozen while Uptime Kuma is in use; do not run it unless restoring Prometheus monitoring.
|
`pve-monitoring.yml` creates CT `146` (`monitoring`, `192.168.1.30`) on
|
||||||
|
`cloud-pc`. The older `monitoring.yml` configures Prometheus, Alertmanager,
|
||||||
|
Grafana and exporters; it is frozen and `make monitoring CONFIRM=1` is reserved
|
||||||
|
for an explicitly approved restoration decision.
|
||||||
|
|
||||||
## Gyro Investment Allocator
|
## Gyro Investment Allocator
|
||||||
|
|
||||||
`pve-gyro.yml` creates unprivileged CT `150` (`gyro`, `192.168.1.35`) on `mini-pc`. `gyro.yml` installs Python 3.13+, pinned `uv`, the `gyro` service user, a container-local GitHub deploy key, restrictive firewall rules, and a weekday systemd timer.
|
Production `gyro` now runs in Tofu-provisioned CT `156` (`gyro`, `192.168.1.35`) on `mini-pc`. CT `150` is stopped and kept only as rollback for at least a week; it is not removed.
|
||||||
|
|
||||||
UFW is the currently enforced isolation layer: inbound is denied except SSH from LAN/OpenVPN, and east-west outbound is denied except the Mihomo HTTP proxy. The equivalent CT `150` Proxmox firewall is staged, but the cluster-wide PVE firewall remains disabled; do not enable it without auditing every node and guest with `firewall=1`.
|
`make gyro` configures Python 3.13+, pinned `uv`, the `gyro` service user, the container-local GitHub deploy key, restrictive firewall rules, and the weekday systemd timer. Do not use `make deploy-gyro` for the cutover path.
|
||||||
|
|
||||||
|
UFW is the currently enforced isolation layer: inbound is denied except SSH from LAN/OpenVPN, and east-west outbound is denied except the Mihomo HTTP proxy. The equivalent CT `156` Proxmox firewall is already carried over; the cluster-wide PVE firewall remains disabled, so do not enable it without auditing every node and guest with `firewall=1`.
|
||||||
|
|
||||||
The role keeps deployment and the timer disabled by default. The active host vars deploy `git@github.com:ada-dmitry/t_tech-gyro.git` with GitHub's verified ED25519 host key; the timer still requires the ignored Vault file:
|
The role keeps deployment and the timer disabled by default. The active host vars deploy `git@github.com:ada-dmitry/t_tech-gyro.git` with GitHub's verified ED25519 host key; the timer still requires the ignored Vault file:
|
||||||
|
|
||||||
@@ -129,17 +143,17 @@ ansible-vault encrypt inventory/host_vars/gyro/vault.yml
|
|||||||
After encrypting the secrets, set `gyro_timer_enabled: true` in `main.yml` and apply with `--ask-vault-pass`. `DRY_RUN_OVERRIDE` remains `true` until real trading is explicitly approved.
|
After encrypting the secrets, set `gyro_timer_enabled: true` in `main.yml` and apply with `--ask-vault-pass`. `DRY_RUN_OVERRIDE` remains `true` until real trading is explicitly approved.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
. ./.env
|
make gyro
|
||||||
.venv/bin/ansible-playbook playbooks/pve-gyro.yml
|
|
||||||
.venv/bin/ansible-playbook playbooks/gyro.yml --ask-vault-pass
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The timer runs at 11:00 Europe/Moscow from Monday through Friday and uses `OnFailure=` for a best-effort Telegram alert. CT `150` is included in the daily mini-pc PBS job and backup freshness audit.
|
The timer runs at 11:00 Europe/Moscow from Monday through Friday and uses `OnFailure=` for a best-effort Telegram alert. PBS backup jobs and the backup freshness audit derive the current Gyro VMID from the registry once regenerated, so CT `156` is picked up automatically.
|
||||||
|
|
||||||
|
The legacy `pve-gyro.yml` remains available for rollback recovery only and is blocked by default after cutover unless an explicit override is passed.
|
||||||
|
|
||||||
Uptime Kuma uses the existing monitoring LXC and stops/disables `homelab-monitoring` without deleting its configuration or data. Its UI is available only from the LAN at `http://192.168.1.30:3001`; create monitors and notification settings in the UI.
|
Uptime Kuma uses the existing monitoring LXC and stops/disables `homelab-monitoring` without deleting its configuration or data. Its UI is available only from the LAN at `http://192.168.1.30:3001`; create monitors and notification settings in the UI.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
.venv/bin/ansible-playbook playbooks/uptime-kuma.yml
|
make uptime-kuma
|
||||||
```
|
```
|
||||||
|
|
||||||
## Emergency Reverse SSH
|
## Emergency Reverse SSH
|
||||||
@@ -149,9 +163,8 @@ Uptime Kuma uses the existing monitoring LXC and stops/disables `homelab-monitor
|
|||||||
Before applying, set `EMERGENCY_BOT_TOKEN`, `EMERGENCY_ALLOWED_USER_IDS`, `EMERGENCY_VPS_HOST_KEY`, and `EMERGENCY_MINI_PC_HOST_KEY` in ignored `.env` or Ansible Vault. The host-key variables must be verified public host keys, not values obtained during deployment.
|
Before applying, set `EMERGENCY_BOT_TOKEN`, `EMERGENCY_ALLOWED_USER_IDS`, `EMERGENCY_VPS_HOST_KEY`, and `EMERGENCY_MINI_PC_HOST_KEY` in ignored `.env` or Ansible Vault. The host-key variables must be verified public host keys, not values obtained during deployment.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
. ./.env
|
make deploy-emergency-bot
|
||||||
.venv/bin/ansible-playbook playbooks/pve-emergency-bot.yml
|
make emergency-access
|
||||||
.venv/bin/ansible-playbook playbooks/emergency-access.yml
|
|
||||||
```
|
```
|
||||||
|
|
||||||
From an authorized private Telegram chat, use the `Enable SSH`, `Status`, and `Stop` buttons or `/emergency ssh`, `/emergency status`, and `/emergency stop`. `/emergency ssh` enables a 60-minute tunnel only; it does not expose a public port. Connect while it is active with:
|
From an authorized private Telegram chat, use the `Enable SSH`, `Status`, and `Stop` buttons or `/emergency ssh`, `/emergency status`, and `/emergency stop`. `/emergency ssh` enables a 60-minute tunnel only; it does not expose a public port. Connect while it is active with:
|
||||||
@@ -167,13 +180,13 @@ The target account is `ansible`; it has no password login. Use the existing priv
|
|||||||
Bootstrap the Ansible service account on shell hosts:
|
Bootstrap the Ansible service account on shell hosts:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
.venv/bin/ansible-playbook -i inventory/hosts.yml playbooks/bootstrap-ansible-user.yml -K
|
make bootstrap-ansible-user
|
||||||
```
|
```
|
||||||
|
|
||||||
When a task needs privilege escalation:
|
When a task needs privilege escalation:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ansible-playbook playbooks/<name>.yml -K
|
make play-<name> EXTRA="-K"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|||||||
@@ -13,6 +13,16 @@ ansible_become: true
|
|||||||
homelab_lan_cidr: 192.168.1.0/24
|
homelab_lan_cidr: 192.168.1.0/24
|
||||||
homelab_service_range: 192.168.1.5-192.168.1.40
|
homelab_service_range: 192.168.1.5-192.168.1.40
|
||||||
|
|
||||||
|
# Публичный адрес, с которого обе ноды PVE выходят в интернет (домашний NAT).
|
||||||
|
# Нужен для правила UFW на ru-vps, открывающего corosync-qnetd (5403/tcp):
|
||||||
|
# арбитр кластера обязан быть достижим по пути, не зависящему ни от одной ноды,
|
||||||
|
# поэтому ходим напрямую, а не через OpenVPN-туннель (он живёт в CT 160 на
|
||||||
|
# mini-pc — при падении mini-pc арбитр исчез бы вместе с ним).
|
||||||
|
# ВНИМАНИЕ: адрес динамический. Если он сменится, qdevice замолчит так же тихо,
|
||||||
|
# как это уже случилось после вывода ZeroTier. Признак — `pvecm status`:
|
||||||
|
# Total votes меньше Expected votes и флаг NV у узлов.
|
||||||
|
homelab_pve_egress_ip: 85.143.112.108
|
||||||
|
|
||||||
openvpn_network_cidr: 10.78.0.0/30
|
openvpn_network_cidr: 10.78.0.0/30
|
||||||
openvpn_listen_port: 8443
|
openvpn_listen_port: 8443
|
||||||
openvpn_forwarded_services:
|
openvpn_forwarded_services:
|
||||||
|
|||||||
@@ -13,9 +13,13 @@
|
|||||||
# ansible/roles/*, ansible/inventory/hosts.yml). Ничего не выдумано.
|
# ansible/roles/*, ansible/inventory/hosts.yml). Ничего не выдумано.
|
||||||
# Там, где факта в коде нет, стоит комментарий "# нет в коде", а не догадка.
|
# Там, где факта в коде нет, стоит комментарий "# нет в коде", а не догадка.
|
||||||
#
|
#
|
||||||
# КТО ЭТО ПОТРЕБЛЯЕТ (на текущем этапе)
|
# КТО ЭТО ПОТРЕБЛЯЕТ
|
||||||
# * ansible/playbooks/reverse-proxy.yml — итерируется по сервисам,
|
# * playbooks/reverse-proxy.yml — Caddyfile по блокам `proxy`.
|
||||||
# у которых задан блок `proxy`, и собирает Caddyfile на ru-vps.
|
# * playbooks/ru-vps-base.yml — стек Caddy и закреплённый образ.
|
||||||
|
# * playbooks/pve-backup-jobs.yml — списки VMID заданий по полю `backup.job`.
|
||||||
|
# * roles/backup_audit — VMID по флагу `monitoring.backup_audit_vmid`.
|
||||||
|
# * playbooks/validate.yml — сверка с фактическим состоянием Proxmox.
|
||||||
|
# * playbooks/dashboard.yml — services.yaml для Homepage-обзора инфры.
|
||||||
# * Человек — как справочник вместо чтения семи playbook'ов по 200-400 строк.
|
# * Человек — как справочник вместо чтения семи playbook'ов по 200-400 строк.
|
||||||
#
|
#
|
||||||
# Существующие pve-*.yml пока НЕ читают этот реестр: их перевод на
|
# Существующие pve-*.yml пока НЕ читают этот реестр: их перевод на
|
||||||
@@ -57,13 +61,51 @@
|
|||||||
homelab_reverse_proxy_host: ru-vps
|
homelab_reverse_proxy_host: ru-vps
|
||||||
homelab_reverse_proxy_dir: /opt/services/ru-vps/caddy
|
homelab_reverse_proxy_dir: /opt/services/ru-vps/caddy
|
||||||
homelab_reverse_proxy_caddyfile: /opt/services/ru-vps/caddy/Caddyfile
|
homelab_reverse_proxy_caddyfile: /opt/services/ru-vps/caddy/Caddyfile
|
||||||
# ВНИМАНИЕ: сам контейнер Caddy на ru-vps ansible'ом НЕ управляется —
|
|
||||||
# в репозитории нет плейбука его установки. Управляется только Caddyfile.
|
|
||||||
homelab_reverse_proxy_container: caddy
|
homelab_reverse_proxy_container: caddy
|
||||||
|
# Стек Caddy разворачивает playbooks/ru-vps-base.yml (роль compose_service).
|
||||||
|
# Содержимое Caddyfile принадлежит playbooks/reverse-proxy.yml — ru-vps-base
|
||||||
|
# его не трогает, только проверяет наличие.
|
||||||
|
# Образ закреплён по digest: до принятия в Ansible на ru-vps крутился
|
||||||
|
# floating-тег caddy:2-alpine (на момент фиксации — v2.11.4).
|
||||||
|
homelab_reverse_proxy_image: >-
|
||||||
|
caddy:2-alpine@sha256:5f5c8640aae01df9654968d946d8f1a56c497f1dd5c5cda4cf95ab7c14d58648
|
||||||
|
# Systemd-юнит намеренно называется homelab-caddy, а не caddy: на ru-vps уже
|
||||||
|
# есть отключённый caddy.service от APT-пакета, и одноимённый unit в
|
||||||
|
# /etc/systemd/system его бы затенил.
|
||||||
|
homelab_reverse_proxy_unit: homelab-caddy
|
||||||
|
|
||||||
# Адрес центрального хоста мониторинга (источник scrape для node-exporter).
|
# Адрес центрального хоста мониторинга (источник scrape для node-exporter).
|
||||||
homelab_monitoring_host_ip: 192.168.1.30
|
homelab_monitoring_host_ip: 192.168.1.30
|
||||||
|
|
||||||
|
# --- Дашборд-обзор инфраструктуры (Homepage, gethomepage.dev) ----------------
|
||||||
|
# Потребитель реестра: playbooks/dashboard.yml рендерит services.yaml из
|
||||||
|
# homelab_services (плитка на сервис, ссылка на его UI, группировка по узлу).
|
||||||
|
# Живёт compose-стеком на LXC monitoring рядом с Uptime Kuma; наружу НЕ
|
||||||
|
# публикуется — доступ только из LAN и по OpenVPN.
|
||||||
|
homelab_dashboard_host: monitoring
|
||||||
|
homelab_dashboard_dir: /opt/homepage
|
||||||
|
homelab_dashboard_bind_ip: 192.168.1.30
|
||||||
|
# Порт на хосте: 3000 в модели реестра занят замороженной Grafana, 3001 —
|
||||||
|
# Uptime Kuma, поэтому Homepage слушает 8082 -> контейнерный 3000.
|
||||||
|
homelab_dashboard_port: 8082
|
||||||
|
# Образ закреплён по digest (закреплён 2026-09-03 после первого pull на CT 155).
|
||||||
|
# Обновление: docker pull <новый тег> на monitoring, взять digest
|
||||||
|
# docker inspect --format '{{ index .RepoDigests 0 }}' <image>
|
||||||
|
# и вписать сюда. playbooks/dashboard.yml падает на assert'е без @sha256
|
||||||
|
# (разовый обход — `-e dashboard_allow_floating_tag=true`).
|
||||||
|
homelab_dashboard_image: >-
|
||||||
|
ghcr.io/gethomepage/homepage:v2.2.0@sha256:753eeb0cc22ab7baad39ed47cbd1aae14e193dd1b264e965f193a9ea1d1e1bdd
|
||||||
|
# HOMEPAGE_ALLOWED_HOSTS: Homepage >=1.0 отклоняет запросы с прочих Host-ов.
|
||||||
|
homelab_dashboard_allowed_hosts: "192.168.1.30:8082"
|
||||||
|
# Виджет proxmox: URL любого узла кластера (без node: -> среднее по кластеру).
|
||||||
|
# Секрет — read-only токен homepage@pve!dashboard (роль PVEAuditor), выпускается
|
||||||
|
# playbooks/bootstrap-dashboard-pve-token.yml в корневой .env как DASHBOARD_PVE_*.
|
||||||
|
# Пустой URL -> плитка Proxmox не рендерится.
|
||||||
|
homelab_dashboard_proxmox_url: "https://192.168.1.10:8006"
|
||||||
|
# slug опубликованной Status Page в Uptime Kuma для виджета сводки up/down
|
||||||
|
# (мониторы и статус-страницы Kuma живут только в её UI — их тут не завести).
|
||||||
|
homelab_dashboard_kuma_slug: homelab
|
||||||
|
|
||||||
homelab_services:
|
homelab_services:
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
@@ -99,20 +141,22 @@ homelab_services:
|
|||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
ovpn-mini:
|
ovpn-mini:
|
||||||
vmid: 132
|
vmid: 160
|
||||||
node: mini-pc
|
node: mini-pc
|
||||||
ip: 192.168.1.23
|
ip: 192.168.1.23
|
||||||
hostname: ovpn-mini
|
hostname: ovpn-mini
|
||||||
role: OpenVPN-шлюз в LAN (клиент ru-vps)
|
role: OpenVPN-шлюз в LAN (клиент ru-vps)
|
||||||
provisioner: pve_lxc # playbooks/pve-ovpn-mini.yml
|
provisioner: tofu # tofu/svc-ovpn-mini.tf, переезд 2026-09-03 (migration-tofu.md)
|
||||||
lxc:
|
lxc:
|
||||||
cores: 1 # из roles/pve_lxc/defaults
|
cores: 1
|
||||||
memory: 256 # из roles/pve_lxc/defaults
|
memory: 256
|
||||||
swap: 128 # из roles/pve_lxc/defaults
|
swap: 128
|
||||||
disk: local-lvm:8
|
disk: local-lvm:8
|
||||||
startup: order=30 # из roles/pve_lxc/defaults
|
startup: order=30
|
||||||
features: "nesting=1"
|
features: "nesting=1" # keyctl/fuse намеренно не заданы
|
||||||
unprivileged: true
|
unprivileged: true
|
||||||
|
# /dev/net/tun — блок device_passthrough в Tofu (dev0: path=/dev/net/tun)
|
||||||
|
# вместо прежних сырых lxc.* строк из pve-ovpn-mini.yml. Эффект тот же.
|
||||||
devices: ["/dev/net/tun"]
|
devices: ["/dev/net/tun"]
|
||||||
ports:
|
ports:
|
||||||
- {name: openvpn, port: 8443, proto: tcp, note: "туннель к ru-vps 10.78.0.1"}
|
- {name: openvpn, port: 8443, proto: tcp, note: "туннель к ru-vps 10.78.0.1"}
|
||||||
@@ -129,19 +173,20 @@ homelab_services:
|
|||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
vaultwarden:
|
vaultwarden:
|
||||||
vmid: 140
|
vmid: 154
|
||||||
node: mini-pc
|
node: mini-pc
|
||||||
ip: 192.168.1.24
|
ip: 192.168.1.24
|
||||||
hostname: vaultwarden
|
hostname: vaultwarden
|
||||||
role: Менеджер паролей Vaultwarden
|
role: Менеджер паролей Vaultwarden
|
||||||
provisioner: pct_ssh # playbooks/pve-vaultwarden.yml
|
provisioner: tofu # tofu/svc-vaultwarden.tf, переезд 2026-09-02 (migration-tofu.md)
|
||||||
lxc:
|
lxc:
|
||||||
cores: 2
|
cores: 2
|
||||||
memory: 1024
|
memory: 1024
|
||||||
swap: 512
|
swap: 512
|
||||||
disk: local-lvm:16
|
disk: local-lvm:16
|
||||||
startup: order=40
|
startup: order=40
|
||||||
features: "nesting=1,keyctl=1"
|
# fuse=1 — штатный флаг PVE вместо прежнего обхода сырыми lxc.* строками.
|
||||||
|
features: "fuse=1,keyctl=1,nesting=1"
|
||||||
unprivileged: true
|
unprivileged: true
|
||||||
ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
||||||
devices: ["/dev/fuse"]
|
devices: ["/dev/fuse"]
|
||||||
@@ -182,25 +227,30 @@ homelab_services:
|
|||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
gitea:
|
gitea:
|
||||||
vmid: 141
|
vmid: 153
|
||||||
node: cloud-pc
|
node: cloud-pc
|
||||||
ip: 192.168.1.25
|
ip: 192.168.1.25
|
||||||
hostname: gitea
|
hostname: gitea
|
||||||
role: Git-хостинг Gitea
|
role: Git-хостинг Gitea
|
||||||
provisioner: pct_ssh # playbooks/pve-gitea.yml
|
provisioner: tofu # tofu/svc-gitea.tf, переезд 2026-09-02 (migration-tofu.md)
|
||||||
lxc:
|
lxc:
|
||||||
cores: 2
|
cores: 2
|
||||||
memory: 2048
|
memory: 2048
|
||||||
swap: 1024
|
swap: 1024
|
||||||
disk: data:32
|
disk: data:32
|
||||||
startup: order=50
|
startup: order=50
|
||||||
features: "nesting=1,keyctl=1"
|
# fuse=1 — штатный флаг PVE вместо прежнего обхода сырыми lxc.* строками.
|
||||||
|
features: "fuse=1,keyctl=1,nesting=1"
|
||||||
unprivileged: true
|
unprivileged: true
|
||||||
ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
||||||
devices: ["/dev/fuse"]
|
devices: ["/dev/fuse"]
|
||||||
mounts:
|
mounts:
|
||||||
# mp0 на cloud-pc; каталог на хосте принадлежит uid/gid 101000
|
# Был bind mount каталога ноды /opt/data/gitea. С переезда 2026-09-02 —
|
||||||
- {host_path: /opt/data/gitea, container_path: /opt/gitea/data, host_uid: 101000, host_gid: 101000}
|
# независимый volume на datastore: mp0 data:153/vm-153-disk-1.raw.
|
||||||
|
# Побочный выигрыш: bind mount vzdump ИСКЛЮЧАЛ из бэкапа ("not a
|
||||||
|
# volume"), то есть данные gitea в PBS никогда не попадали. Volume
|
||||||
|
# попадает.
|
||||||
|
- {volume: data, size: 32G, container_path: /opt/gitea/data}
|
||||||
data_dir: /opt/gitea/data
|
data_dir: /opt/gitea/data
|
||||||
ports:
|
ports:
|
||||||
- {name: http, port: 3000, proto: tcp}
|
- {name: http, port: 3000, proto: tcp}
|
||||||
@@ -215,12 +265,13 @@ homelab_services:
|
|||||||
storage: pbs
|
storage: pbs
|
||||||
restic:
|
restic:
|
||||||
profile: gitea
|
profile: gitea
|
||||||
# restic-профиль выполняется на cloud-pc, а не внутри LXC
|
# С переезда 2026-09-02 профиль выполняется ВНУТРИ LXC, как у
|
||||||
run_on: cloud-pc
|
# vaultwarden: пути ноды больше не существуют, данные в volume.
|
||||||
|
run_on: gitea
|
||||||
repository: rclone:yadisk:System/Backups/HomeLab/restic/gitea
|
repository: rclone:yadisk:System/Backups/HomeLab/restic/gitea
|
||||||
source_path: /opt/data/gitea
|
source_path: /opt/gitea/data
|
||||||
schedule: "*-*-* 04:15:00"
|
schedule: "*-*-* 04:15:00"
|
||||||
sqlite_db: /opt/data/gitea/gitea/gitea.db
|
sqlite_db: /opt/gitea/data/gitea/gitea.db
|
||||||
monitoring:
|
monitoring:
|
||||||
node_exporter: true
|
node_exporter: true
|
||||||
blackbox: true
|
blackbox: true
|
||||||
@@ -238,54 +289,24 @@ homelab_services:
|
|||||||
status_code: [200]
|
status_code: [200]
|
||||||
follow_redirects: none # соответствует `curl -fsS` без -L в старом плейбуке
|
follow_redirects: none # соответствует `curl -fsS` без -L в старом плейбуке
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
memoir-bot:
|
|
||||||
vmid: 142
|
|
||||||
node: mini-pc
|
|
||||||
ip: 192.168.1.26
|
|
||||||
hostname: memoir-bot
|
|
||||||
role: Telegram-бот дневника в Obsidian-хранилище
|
|
||||||
provisioner: pct_ssh # playbooks/pve-memoir-bot.yml
|
|
||||||
lxc:
|
|
||||||
cores: 1
|
|
||||||
memory: 512
|
|
||||||
swap: 512
|
|
||||||
disk: local-lvm:16
|
|
||||||
startup: order=60
|
|
||||||
features: "nesting=1,keyctl=1"
|
|
||||||
unprivileged: true
|
|
||||||
ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
|
||||||
devices: ["/dev/fuse"]
|
|
||||||
data_dir: /srv/memoir-bot
|
|
||||||
ports: [] # портов не публикует, только исходящие подключения к Telegram
|
|
||||||
images:
|
|
||||||
- memoir-bot:local # собирается на месте `docker build`, digest отсутствует
|
|
||||||
runtime: docker-run-systemd # /etc/systemd/system/memoir-bot.service
|
|
||||||
backup:
|
|
||||||
kind: pbs
|
|
||||||
job: homelab-pbs-daily-mini
|
|
||||||
schedule: "02:40"
|
|
||||||
storage: pbs
|
|
||||||
monitoring:
|
|
||||||
node_exporter: true
|
|
||||||
blackbox: false
|
|
||||||
backup_audit_vmid: true
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
mihomo:
|
mihomo:
|
||||||
vmid: 143
|
vmid: 159
|
||||||
node: mini-pc
|
node: mini-pc
|
||||||
ip: 192.168.1.27
|
ip: 192.168.1.27
|
||||||
hostname: mihomo
|
hostname: mihomo
|
||||||
role: Локальный прокси Mihomo + веб-интерфейс MetaCubeXD
|
role: Локальный прокси Mihomo + веб-интерфейс MetaCubeXD
|
||||||
provisioner: pct_ssh # playbooks/pve-mihomo.yml
|
provisioner: tofu # tofu/svc-mihomo.tf, переезд 2026-09-03 (migration-tofu.md)
|
||||||
lxc:
|
lxc:
|
||||||
cores: 1
|
cores: 1
|
||||||
memory: 512
|
memory: 512
|
||||||
swap: 512
|
swap: 512
|
||||||
disk: local-lvm:8
|
disk: local-lvm:8
|
||||||
startup: order=70
|
startup: order=70
|
||||||
features: "nesting=1,keyctl=1"
|
# fuse=1 — штатный флаг PVE вместо прежнего обхода сырыми lxc.* строками.
|
||||||
|
# /dev/net/tun — блок device_passthrough в Tofu (dev0: path=/dev/net/tun),
|
||||||
|
# тоже вместо сырых lxc.* строк. Эффект тот же, запись в pct config иная.
|
||||||
|
features: "fuse=1,keyctl=1,nesting=1"
|
||||||
unprivileged: true
|
unprivileged: true
|
||||||
ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
||||||
devices: ["/dev/fuse", "/dev/net/tun"]
|
devices: ["/dev/fuse", "/dev/net/tun"]
|
||||||
@@ -311,19 +332,21 @@ homelab_services:
|
|||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
adguard:
|
adguard:
|
||||||
vmid: 144
|
vmid: 158
|
||||||
node: mini-pc
|
node: mini-pc
|
||||||
ip: 192.168.1.28
|
ip: 192.168.1.28
|
||||||
hostname: adguard
|
hostname: adguard
|
||||||
role: AdGuard Home — DNS с фильтрацией
|
role: AdGuard Home — DNS с фильтрацией
|
||||||
provisioner: pct_ssh # playbooks/pve-adguard.yml
|
provisioner: tofu # tofu/svc-adguard.tf, переезд 2026-09-03 (migration-tofu.md)
|
||||||
lxc:
|
lxc:
|
||||||
cores: 1
|
cores: 1
|
||||||
memory: 512
|
memory: 512
|
||||||
swap: 512
|
swap: 512
|
||||||
disk: local-lvm:8
|
disk: local-lvm:8
|
||||||
startup: order=40
|
startup: order=40
|
||||||
features: "nesting=1,keyctl=1"
|
# fuse=1 — штатный флаг PVE вместо прежнего обхода сырыми lxc.* строками
|
||||||
|
# (lineinfile по /etc/pve/lxc/144.conf в pve-adguard.yml). Эффект тот же.
|
||||||
|
features: "fuse=1,keyctl=1,nesting=1"
|
||||||
unprivileged: true
|
unprivileged: true
|
||||||
ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
||||||
devices: ["/dev/fuse"]
|
devices: ["/dev/fuse"]
|
||||||
@@ -348,19 +371,22 @@ homelab_services:
|
|||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
docker-test:
|
docker-test:
|
||||||
vmid: 145
|
vmid: 152
|
||||||
node: cloud-pc
|
node: cloud-pc
|
||||||
ip: 192.168.1.29
|
ip: 192.168.1.29
|
||||||
hostname: docker-test
|
hostname: docker-test
|
||||||
role: Песочница для проверки Docker в непривилегированном LXC
|
role: Песочница для проверки Docker в непривилегированном LXC
|
||||||
provisioner: pct_ssh # playbooks/pve-docker-test.yml
|
provisioner: tofu # tofu/svc-docker-test.tf, переезд 2026-09-02 (migration-tofu.md)
|
||||||
lxc:
|
lxc:
|
||||||
cores: 1
|
cores: 1
|
||||||
memory: 512
|
memory: 512
|
||||||
swap: 512
|
swap: 512
|
||||||
disk: data:8
|
disk: data:8
|
||||||
startup: order=50
|
startup: order=50
|
||||||
features: "nesting=1,keyctl=1"
|
# fuse=1 появился после переезда на Tofu: раньше /dev/fuse пробрасывался
|
||||||
|
# сырыми lxc.* строками через lineinfile, теперь это штатный флаг PVE.
|
||||||
|
# Эффект тот же, запись в pct config другая.
|
||||||
|
features: "fuse=1,keyctl=1,nesting=1"
|
||||||
unprivileged: true
|
unprivileged: true
|
||||||
ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
||||||
devices: ["/dev/fuse"]
|
devices: ["/dev/fuse"]
|
||||||
@@ -378,23 +404,33 @@ homelab_services:
|
|||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
monitoring:
|
monitoring:
|
||||||
vmid: 146
|
vmid: 155
|
||||||
node: cloud-pc
|
node: cloud-pc
|
||||||
ip: 192.168.1.30
|
ip: 192.168.1.30
|
||||||
hostname: monitoring
|
hostname: monitoring
|
||||||
role: Prometheus + Alertmanager + Grafana + blackbox + pve-exporter + Uptime Kuma
|
# До переезда 2026-09-02 контейнер нёс ещё и замороженный стек
|
||||||
provisioner: pve_lxc # playbooks/pve-monitoring.yml
|
# Prometheus/Alertmanager/Grafana/blackbox/pve-exporter. В новый контейнер
|
||||||
|
# он сознательно не разворачивался (migration-tofu.md, п.5.5), поэтому
|
||||||
|
# фактически здесь работает только Uptime Kuma. Возврат стека — отдельное
|
||||||
|
# architecture decision, см. legacy-warning.md.
|
||||||
|
role: Uptime Kuma (замороженный стек Prometheus больше не развёрнут)
|
||||||
|
provisioner: tofu # tofu/svc-monitoring.tf, переезд 2026-09-02 (migration-tofu.md)
|
||||||
lxc:
|
lxc:
|
||||||
cores: 2
|
cores: 2
|
||||||
memory: 4096
|
memory: 4096
|
||||||
swap: 512
|
swap: 512
|
||||||
disk: data:24
|
disk: data:24
|
||||||
|
# Ресурсы оставлены как были, под замороженный стек. Фактическое
|
||||||
|
# потребление после переезда — 184 МБ RAM и 1.8 ГБ диска. Уменьшение —
|
||||||
|
# отдельное решение, не часть переезда.
|
||||||
startup: order=80
|
startup: order=80
|
||||||
# создаётся с nesting=1, keyctl=1 добавляется отдельной задачей `pct set`
|
features: "keyctl=1,nesting=1"
|
||||||
features: "nesting=1,keyctl=1"
|
|
||||||
unprivileged: true
|
unprivileged: true
|
||||||
ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
||||||
data_dir: /opt/monitoring
|
# Данные Uptime Kuma лежат в /opt/uptime-kuma, а НЕ в /opt/monitoring:
|
||||||
|
# последний принадлежал замороженному стеку и на новом контейнере
|
||||||
|
# отсутствует.
|
||||||
|
data_dir: /opt/uptime-kuma
|
||||||
ports:
|
ports:
|
||||||
- {name: grafana, port: 3000, proto: tcp, bind: 192.168.1.30}
|
- {name: grafana, port: 3000, proto: tcp, bind: 192.168.1.30}
|
||||||
- {name: pushgateway, port: 9091, proto: tcp, bind: 192.168.1.30}
|
- {name: pushgateway, port: 9091, proto: tcp, bind: 192.168.1.30}
|
||||||
@@ -454,12 +490,12 @@ homelab_services:
|
|||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
emergency-bot:
|
emergency-bot:
|
||||||
vmid: 148
|
vmid: 151
|
||||||
node: mini-pc
|
node: mini-pc
|
||||||
ip: 192.168.1.32
|
ip: 192.168.1.32
|
||||||
hostname: emergency-bot
|
hostname: emergency-bot
|
||||||
role: Telegram-бот аварийного доступа
|
role: Telegram-бот аварийного доступа
|
||||||
provisioner: pve_lxc # playbooks/pve-emergency-bot.yml
|
provisioner: tofu # tofu/services.tf, переезд 2026-09-02 (migration-tofu.md)
|
||||||
lxc:
|
lxc:
|
||||||
cores: 1
|
cores: 1
|
||||||
memory: 512
|
memory: 512
|
||||||
@@ -480,20 +516,20 @@ homelab_services:
|
|||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
grimmory:
|
grimmory:
|
||||||
vmid: 149
|
vmid: 157
|
||||||
node: cloud-pc
|
node: cloud-pc
|
||||||
ip: 192.168.1.34
|
ip: 192.168.1.34
|
||||||
hostname: grimmory
|
hostname: grimmory
|
||||||
role: Библиотека книг Grimmory (+ MariaDB), OPDS/KOReader
|
role: Библиотека книг Grimmory (+ MariaDB), OPDS/KOReader
|
||||||
provisioner: pve_lxc # playbooks/pve-grimmory.yml
|
provisioner: tofu # tofu/svc-grimmory.tf, переезд 2026-09-02 (migration-tofu.md)
|
||||||
lxc:
|
lxc:
|
||||||
cores: 2
|
cores: 2
|
||||||
memory: 4096
|
memory: 4096
|
||||||
swap: 1024
|
swap: 1024
|
||||||
disk: data:64
|
disk: data:64
|
||||||
startup: order=100
|
startup: order=100
|
||||||
# создаётся с nesting=1, keyctl=1 добавляется отдельной задачей `pct set`
|
# fuse=1 — штатный флаг PVE вместо прежнего обхода сырыми lxc.* строками.
|
||||||
features: "nesting=1,keyctl=1"
|
features: "fuse=1,keyctl=1,nesting=1"
|
||||||
unprivileged: true
|
unprivileged: true
|
||||||
ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
||||||
devices: ["/dev/fuse"]
|
devices: ["/dev/fuse"]
|
||||||
@@ -576,12 +612,12 @@ homelab_services:
|
|||||||
|
|
||||||
# --------------------------------------------------------------------------
|
# --------------------------------------------------------------------------
|
||||||
gyro:
|
gyro:
|
||||||
vmid: 150
|
vmid: 156
|
||||||
node: mini-pc
|
node: mini-pc
|
||||||
ip: 192.168.1.35
|
ip: 192.168.1.35
|
||||||
hostname: gyro
|
hostname: gyro
|
||||||
role: Изолированный контейнер под задачу gyro (доступ в сеть только через mihomo)
|
role: Изолированный контейнер под задачу gyro (доступ в сеть только через mihomo)
|
||||||
provisioner: pve_lxc # playbooks/pve-gyro.yml
|
provisioner: tofu # tofu/svc-gyro.tf
|
||||||
lxc:
|
lxc:
|
||||||
cores: 1
|
cores: 1
|
||||||
memory: 512
|
memory: 512
|
||||||
@@ -591,7 +627,7 @@ homelab_services:
|
|||||||
features: "" # pve_lxc_features: [] — nesting отключён намеренно
|
features: "" # pve_lxc_features: [] — nesting отключён намеренно
|
||||||
unprivileged: true
|
unprivileged: true
|
||||||
ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
||||||
firewall: proxmox # /etc/pve/firewall/150.fw, policy_in DROP
|
firewall: proxmox # /etc/pve/firewall/156.fw, policy_in DROP
|
||||||
ports: []
|
ports: []
|
||||||
images: [] # роль gyro образы не закрепляет
|
images: [] # роль gyro образы не закрепляет
|
||||||
backup:
|
backup:
|
||||||
|
|||||||
@@ -64,10 +64,6 @@ all:
|
|||||||
ansible_host: 192.168.1.25
|
ansible_host: 192.168.1.25
|
||||||
expected_lan_ip: 192.168.1.25
|
expected_lan_ip: 192.168.1.25
|
||||||
|
|
||||||
memoir-bot:
|
|
||||||
ansible_host: 192.168.1.26
|
|
||||||
expected_lan_ip: 192.168.1.26
|
|
||||||
|
|
||||||
mihomo:
|
mihomo:
|
||||||
ansible_host: 192.168.1.27
|
ansible_host: 192.168.1.27
|
||||||
expected_lan_ip: 192.168.1.27
|
expected_lan_ip: 192.168.1.27
|
||||||
@@ -103,7 +99,6 @@ all:
|
|||||||
ovpn-mini:
|
ovpn-mini:
|
||||||
vaultwarden:
|
vaultwarden:
|
||||||
gitea:
|
gitea:
|
||||||
memoir-bot:
|
|
||||||
mihomo:
|
mihomo:
|
||||||
adguard:
|
adguard:
|
||||||
monitoring:
|
monitoring:
|
||||||
|
|||||||
@@ -2,19 +2,23 @@
|
|||||||
- name: Create and verify current PBS audit before AdGuard update
|
- name: Create and verify current PBS audit before AdGuard update
|
||||||
hosts: mini-pc
|
hosts: mini-pc
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
|
vars:
|
||||||
|
adguard_vmid: "{{ homelab_services['adguard'].vmid }}"
|
||||||
tasks:
|
tasks:
|
||||||
- name: Verify AdGuard VMID ownership before backup
|
- name: Verify AdGuard VMID ownership before backup
|
||||||
ansible.builtin.command: pct config 144
|
ansible.builtin.command: "pct config {{ adguard_vmid }}"
|
||||||
register: adguard_pct_config
|
register: adguard_pct_config
|
||||||
changed_when: false
|
changed_when: false
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Refuse to back up a foreign VMID 144
|
- name: Refuse to back up a foreign AdGuard VMID
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- adguard_pct_config.rc == 0
|
- adguard_pct_config.rc == 0
|
||||||
- adguard_existing_hostname == 'adguard'
|
- adguard_existing_hostname == 'adguard'
|
||||||
fail_msg: VMID 144 is not the AdGuard container.
|
fail_msg: >-
|
||||||
|
VMID {{ adguard_vmid }} from the service registry is not the
|
||||||
|
AdGuard container.
|
||||||
vars:
|
vars:
|
||||||
adguard_existing_hostname: >-
|
adguard_existing_hostname: >-
|
||||||
{{ adguard_pct_config.stdout_lines
|
{{ adguard_pct_config.stdout_lines
|
||||||
@@ -41,13 +45,11 @@
|
|||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
argv:
|
argv:
|
||||||
- vzdump
|
- vzdump
|
||||||
- "144"
|
- "{{ adguard_vmid }}"
|
||||||
- --storage
|
- --storage
|
||||||
- pbs
|
- pbs
|
||||||
- --mode
|
- --mode
|
||||||
- snapshot
|
- snapshot
|
||||||
- --prune-backups
|
|
||||||
- keep-all=1
|
|
||||||
- --exclude-path
|
- --exclude-path
|
||||||
- /var/lib/docker/fuse-overlayfs/*/merged
|
- /var/lib/docker/fuse-overlayfs/*/merged
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
# ============================================================================
|
||||||
|
# Выпуск read-only Proxmox-токена для виджета Proxmox в Homepage-дашборде.
|
||||||
|
#
|
||||||
|
# Создаёт пользователя homepage@pve, роль PVEAuditor на / и privsep-токен
|
||||||
|
# homepage@pve!dashboard. Секрет пишется в КОРНЕВОЙ .env как
|
||||||
|
# DASHBOARD_PVE_API_USER / DASHBOARD_PVE_API_TOKEN_ID /
|
||||||
|
# DASHBOARD_PVE_API_TOKEN_SECRET — оттуда его читает playbooks/dashboard.yml
|
||||||
|
# через lookup('env', ...).
|
||||||
|
#
|
||||||
|
# Парный к playbooks/bootstrap-monitoring-pve-token.yml. Отдельный принципал,
|
||||||
|
# чтобы дашборд не зависел от кредов замороженного стека Prometheus.
|
||||||
|
#
|
||||||
|
# Запуск: make bootstrap-dashboard-token
|
||||||
|
# ============================================================================
|
||||||
|
- name: Create read-only Proxmox token for the Homepage dashboard
|
||||||
|
hosts: mini-pc
|
||||||
|
gather_facts: false
|
||||||
|
vars:
|
||||||
|
dashboard_pve_user: homepage@pve
|
||||||
|
dashboard_pve_token_id: dashboard
|
||||||
|
# .env лежит в корне репозитория, playbook_dir — это ansible/playbooks.
|
||||||
|
dashboard_pve_env_file: "{{ playbook_dir }}/../../.env"
|
||||||
|
dashboard_pve_rotate_existing_token: false
|
||||||
|
tasks:
|
||||||
|
- name: Read existing Proxmox users
|
||||||
|
ansible.builtin.command: pveum user list --output-format json
|
||||||
|
register: dashboard_pve_users_raw
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Create the dashboard Proxmox user
|
||||||
|
ansible.builtin.command: >-
|
||||||
|
pveum user add {{ dashboard_pve_user }}
|
||||||
|
--comment 'Read-only Homepage dashboard user'
|
||||||
|
when: dashboard_pve_user not in (dashboard_pve_users_raw.stdout | from_json | map(attribute='userid') | list)
|
||||||
|
|
||||||
|
- name: Grant PVEAuditor role to the dashboard user
|
||||||
|
ansible.builtin.command: >-
|
||||||
|
pveum acl modify / -user {{ dashboard_pve_user }} -role PVEAuditor
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Read the dashboard user tokens
|
||||||
|
ansible.builtin.command: >-
|
||||||
|
pveum user token list {{ dashboard_pve_user }} --output-format json
|
||||||
|
register: dashboard_pve_tokens_raw
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Refuse to overwrite an existing dashboard token
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- dashboard_pve_token_id not in (dashboard_pve_tokens_raw.stdout | from_json | map(attribute='tokenid') | list)
|
||||||
|
fail_msg: >-
|
||||||
|
Existing dashboard token secret cannot be recovered safely. Rotate it
|
||||||
|
explicitly (-e dashboard_pve_rotate_existing_token=true) before rerunning.
|
||||||
|
when: not dashboard_pve_rotate_existing_token | bool
|
||||||
|
|
||||||
|
- name: Rotate the existing dashboard token explicitly
|
||||||
|
ansible.builtin.command: >-
|
||||||
|
pveum user token remove {{ dashboard_pve_user }} {{ dashboard_pve_token_id }}
|
||||||
|
when:
|
||||||
|
- dashboard_pve_rotate_existing_token | bool
|
||||||
|
- dashboard_pve_token_id in (dashboard_pve_tokens_raw.stdout | from_json | map(attribute='tokenid') | list)
|
||||||
|
|
||||||
|
- name: Create the separated dashboard token
|
||||||
|
ansible.builtin.command: >-
|
||||||
|
pveum user token add {{ dashboard_pve_user }} {{ dashboard_pve_token_id }}
|
||||||
|
--privsep 1 --comment 'Homepage Proxmox widget' --output-format json
|
||||||
|
register: dashboard_pve_token_created
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Grant PVEAuditor role to the separated dashboard token
|
||||||
|
ansible.builtin.command: >-
|
||||||
|
pveum acl modify / -token {{ dashboard_pve_user }}!{{ dashboard_pve_token_id }} -role PVEAuditor
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Store the dashboard token variables in the root .env
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: "{{ dashboard_pve_env_file }}"
|
||||||
|
regexp: "^export {{ item.name }}="
|
||||||
|
line: "export {{ item.name }}='{{ item.value }}'"
|
||||||
|
create: false
|
||||||
|
loop:
|
||||||
|
- name: DASHBOARD_PVE_API_USER
|
||||||
|
value: "{{ dashboard_pve_user }}"
|
||||||
|
- name: DASHBOARD_PVE_API_TOKEN_ID
|
||||||
|
value: "{{ dashboard_pve_token_id }}"
|
||||||
|
- name: DASHBOARD_PVE_API_TOKEN_SECRET
|
||||||
|
value: "{{ (dashboard_pve_token_created.stdout | from_json).value }}"
|
||||||
|
delegate_to: localhost
|
||||||
|
vars:
|
||||||
|
ansible_connection: local
|
||||||
|
ansible_become: false
|
||||||
|
no_log: true
|
||||||
@@ -5,7 +5,8 @@
|
|||||||
vars:
|
vars:
|
||||||
monitoring_pve_user: monitoring@pve
|
monitoring_pve_user: monitoring@pve
|
||||||
monitoring_pve_token_id: prometheus
|
monitoring_pve_token_id: prometheus
|
||||||
monitoring_pve_env_file: "{{ playbook_dir }}/../.env"
|
# .env лежит в корне репозитория, playbook_dir — это ansible/playbooks.
|
||||||
|
monitoring_pve_env_file: "{{ playbook_dir }}/../../.env"
|
||||||
monitoring_pve_rotate_existing_token: false
|
monitoring_pve_rotate_existing_token: false
|
||||||
tasks:
|
tasks:
|
||||||
- name: Read existing Proxmox users
|
- name: Read existing Proxmox users
|
||||||
|
|||||||
@@ -37,10 +37,17 @@
|
|||||||
pve_token_data: "{{ pve_token_add.stdout | from_json }}"
|
pve_token_data: "{{ pve_token_add.stdout | from_json }}"
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
- name: Write local ansible/.env
|
# ВНИМАНИЕ: задача переписывает файл ЦЕЛИКОМ, а не правит отдельные строки.
|
||||||
|
# Всё, что добавлено вручную или другими целями — MONITORING_*, EMERGENCY_*,
|
||||||
|
# PROXMOX_ROOT_PASSWORD — будет потеряно. Поэтому backup: true.
|
||||||
|
# Строки пишутся с префиксом `export`: по нему bootstrap-monitoring-pve-token.yml
|
||||||
|
# находит их своим lineinfile (regexp: "^export NAME="), и он же позволяет
|
||||||
|
# просто сделать `source .env` в обычном шелле.
|
||||||
|
- name: Write the local .env in the repository root
|
||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
dest: "{{ playbook_dir }}/../.env"
|
dest: "{{ playbook_dir }}/../../.env"
|
||||||
mode: "0600"
|
mode: "0600"
|
||||||
|
backup: true
|
||||||
content: |
|
content: |
|
||||||
export PROXMOX_HOST={{ ansible_host }}
|
export PROXMOX_HOST={{ ansible_host }}
|
||||||
export PROXMOX_USER='{{ pve_api_user }}'
|
export PROXMOX_USER='{{ pve_api_user }}'
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
---
|
||||||
|
# ============================================================================
|
||||||
|
# Дашборд-обзор всей инфраструктуры HomeLab (Homepage, gethomepage.dev).
|
||||||
|
#
|
||||||
|
# ОБЛАСТЬ ОТВЕТСТВЕННОСТИ
|
||||||
|
# Разворачивает compose-стек Homepage на LXC monitoring (рядом с Uptime
|
||||||
|
# Kuma) и ГЕНЕРИРУЕТ его конфиги из реестра homelab_services: одна плитка
|
||||||
|
# на сервис, ссылка на его UI, группировка по узлу Proxmox. Добавили
|
||||||
|
# сервис в реестр -> плитка появилась сама, второй список вести не нужно.
|
||||||
|
#
|
||||||
|
# Наружу дашборд НЕ публикуется: контейнер слушает только LAN-адрес CT 155,
|
||||||
|
# доступ из локальной сети или по OpenVPN. Внутренняя топология (IP, VMID,
|
||||||
|
# раскладка по нодам) на публичный периметр не выносится.
|
||||||
|
#
|
||||||
|
# ГРАНИЦА С reverse-proxy.yml
|
||||||
|
# Никакой общей области. Дашборд — отдельный стек на другом хосте, Caddyfile
|
||||||
|
# он не трогает.
|
||||||
|
#
|
||||||
|
# ЗАПУСК
|
||||||
|
# make dashboard (ansible-playbook playbooks/dashboard.yml)
|
||||||
|
# make dry-dashboard (--check --diff)
|
||||||
|
# Первый прогон, пока образ не закреплён по digest:
|
||||||
|
# ansible-playbook playbooks/dashboard.yml -e dashboard_allow_floating_tag=true
|
||||||
|
#
|
||||||
|
# СЕКРЕТЫ
|
||||||
|
# Виджет proxmox требует read-only токен homepage@pve!dashboard (роль
|
||||||
|
# PVEAuditor). Выпускается playbooks/bootstrap-dashboard-pve-token.yml,
|
||||||
|
# секрет кладётся в корневой .env как DASHBOARD_PVE_API_USER /
|
||||||
|
# DASHBOARD_PVE_API_TOKEN_ID / DASHBOARD_PVE_API_TOKEN_SECRET и подхватывается
|
||||||
|
# отсюда через lookup('env', ...). Без него дашборд работает — просто плитка
|
||||||
|
# "Proxmox кластер" показывает ошибку виджета, это не блокер.
|
||||||
|
#
|
||||||
|
# ТАРГЕТ
|
||||||
|
# По умолчанию homelab_dashboard_host (monitoring). Переопределяется ради
|
||||||
|
# blue-green: `-e dashboard_config_target=monitoring-new --limit monitoring-new`
|
||||||
|
# (голый --limit play не перенацеливает, а обнуляет — см. uptime-kuma.yml).
|
||||||
|
# ============================================================================
|
||||||
|
- name: Deploy the HomeLab infrastructure dashboard (Homepage) on the monitoring LXC
|
||||||
|
hosts: "{{ dashboard_config_target | default(homelab_dashboard_host | default('monitoring')) }}"
|
||||||
|
gather_facts: true
|
||||||
|
|
||||||
|
vars:
|
||||||
|
dash_root: "{{ homelab_dashboard_dir | default('/opt/homepage') }}"
|
||||||
|
dash_config_dir: "{{ (homelab_dashboard_dir | default('/opt/homepage')) ~ '/config' }}"
|
||||||
|
dash_bind: "{{ homelab_dashboard_bind_ip | default(expected_lan_ip) }}"
|
||||||
|
dash_port: "{{ homelab_dashboard_port | default(8082) }}"
|
||||||
|
dash_image: "{{ homelab_dashboard_image | default('') }}"
|
||||||
|
dash_allowed_hosts: >-
|
||||||
|
{{ homelab_dashboard_allowed_hosts | default(dash_bind ~ ':' ~ dash_port) }}
|
||||||
|
# Список узлов Proxmox, встречающихся в реестре, — по нему строятся группы.
|
||||||
|
dashboard_nodes: >-
|
||||||
|
{{ homelab_services | dict2items | map(attribute='value.node')
|
||||||
|
| unique | sort | list }}
|
||||||
|
# Секрет Proxmox-виджета из окружения (make load .env). Пусто -> плитка
|
||||||
|
# рендерится без данных. no_log на задаче, которая это пишет.
|
||||||
|
dash_pve_user: "{{ lookup('ansible.builtin.env', 'DASHBOARD_PVE_API_USER') }}"
|
||||||
|
dash_pve_token_id: "{{ lookup('ansible.builtin.env', 'DASHBOARD_PVE_API_TOKEN_ID') }}"
|
||||||
|
dash_pve_token_secret: "{{ lookup('ansible.builtin.env', 'DASHBOARD_PVE_API_TOKEN_SECRET') }}"
|
||||||
|
|
||||||
|
pre_tasks:
|
||||||
|
- name: Require the dashboard registry variables
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- dash_image | length > 0
|
||||||
|
- dash_bind | length > 0
|
||||||
|
fail_msg: >-
|
||||||
|
Не заданы homelab_dashboard_* в
|
||||||
|
inventory/group_vars/all/services.yml.
|
||||||
|
|
||||||
|
- name: Require the Homepage image to be pinned by digest
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- "'@sha256:' in dash_image or dashboard_allow_floating_tag | default(false) | bool"
|
||||||
|
fail_msg: >-
|
||||||
|
{{ dash_image }} не закреплён по digest. Выполни на
|
||||||
|
{{ inventory_hostname }}:
|
||||||
|
docker pull {{ dash_image }}
|
||||||
|
docker inspect --format '{{ '{{' }} index .RepoDigests 0 {{ '}}' }}' {{ dash_image }}
|
||||||
|
и пропиши tag@sha256 в homelab_dashboard_image. Разовый обход для
|
||||||
|
первого прогона: -e dashboard_allow_floating_tag=true
|
||||||
|
|
||||||
|
- name: Ensure the Homepage config directory exists
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ dash_config_dir }}"
|
||||||
|
state: directory
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
|
- name: Render Homepage configuration files from the registry
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: "{{ item.src }}"
|
||||||
|
dest: "{{ dash_config_dir }}/{{ item.dest }}"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
loop:
|
||||||
|
- {src: homepage-settings.yaml.j2, dest: settings.yaml}
|
||||||
|
- {src: homepage-services.yaml.j2, dest: services.yaml}
|
||||||
|
- {src: homepage-widgets.yaml.j2, dest: widgets.yaml}
|
||||||
|
- {src: homepage-bookmarks.yaml.j2, dest: bookmarks.yaml}
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.dest }}"
|
||||||
|
register: dash_config_files
|
||||||
|
|
||||||
|
- name: Render the Homepage widget secrets file
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: homepage.env.j2
|
||||||
|
dest: "{{ dash_root }}/homepage.env"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0600"
|
||||||
|
register: dash_env_file
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
roles:
|
||||||
|
- role: compose_service
|
||||||
|
compose_service_name: homelab-homepage
|
||||||
|
compose_service_description: HomeLab infrastructure dashboard (Homepage)
|
||||||
|
compose_service_root: "{{ dash_root }}"
|
||||||
|
compose_service_root_mode: "0755"
|
||||||
|
compose_service_compose_file: docker-compose.yml
|
||||||
|
compose_service_compose_template: homepage-compose.yml.j2
|
||||||
|
# | bool обязателен: роль фильтрует триггеры по truthiness, а строка
|
||||||
|
# "False" из "{{ ... is changed }}" тоже истинна.
|
||||||
|
compose_service_restart_triggers:
|
||||||
|
- "{{ (dash_config_files is changed) | bool }}"
|
||||||
|
- "{{ (dash_env_file is changed) | bool }}"
|
||||||
|
compose_service_health_url: "http://{{ dash_bind }}:{{ dash_port }}/"
|
||||||
|
compose_service_health_status: [200]
|
||||||
|
compose_service_health_follow_redirects: none
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
- name: Create and verify Gitea backup before update
|
- name: Create and verify Gitea backup before update
|
||||||
hosts: cloud-pc
|
hosts: gitea
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
tasks:
|
tasks:
|
||||||
- name: Create a fresh Gitea offsite backup
|
- name: Create a fresh Gitea offsite backup
|
||||||
@@ -12,6 +12,10 @@
|
|||||||
- homelab-restic-offsite-gitea.service
|
- homelab-restic-offsite-gitea.service
|
||||||
changed_when: true
|
changed_when: true
|
||||||
|
|
||||||
|
- name: Run Gitea backup audit
|
||||||
|
hosts: cloud-pc
|
||||||
|
gather_facts: false
|
||||||
|
tasks:
|
||||||
- name: Run Gitea offsite backup audit
|
- name: Run Gitea offsite backup audit
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
argv:
|
argv:
|
||||||
@@ -22,6 +26,8 @@
|
|||||||
changed_when: true
|
changed_when: true
|
||||||
|
|
||||||
- import_playbook: pve-gitea.yml
|
- import_playbook: pve-gitea.yml
|
||||||
|
vars:
|
||||||
|
pve_provisioning_enabled: false
|
||||||
|
|
||||||
- name: Verify Gitea public endpoints after update
|
- name: Verify Gitea public endpoints after update
|
||||||
hosts: ru-vps
|
hosts: ru-vps
|
||||||
|
|||||||
@@ -24,23 +24,26 @@
|
|||||||
- name: Create a fresh Grimmory PBS backup
|
- name: Create a fresh Grimmory PBS backup
|
||||||
hosts: cloud-pc
|
hosts: cloud-pc
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
|
vars:
|
||||||
|
grimmory_vmid: "{{ homelab_services['grimmory'].vmid }}"
|
||||||
tasks:
|
tasks:
|
||||||
- name: Read Grimmory LXC config
|
- name: Read Grimmory LXC config
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
argv:
|
argv:
|
||||||
- pct
|
- pct
|
||||||
- config
|
- config
|
||||||
- "149"
|
- "{{ grimmory_vmid }}"
|
||||||
register: grimmory_pct_config
|
register: grimmory_pct_config
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
|
||||||
- name: Assert VMID 149 belongs to Grimmory
|
- name: Assert registry VMID belongs to Grimmory
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- grimmory_pct_hostname_line != ""
|
- grimmory_pct_hostname_line != ""
|
||||||
- grimmory_pct_hostname == "grimmory"
|
- grimmory_pct_hostname == "grimmory"
|
||||||
fail_msg: >-
|
fail_msg: >-
|
||||||
Refusing to run vzdump 149 because pct config hostname is not grimmory:
|
Refusing to run vzdump {{ grimmory_vmid }} because pct config
|
||||||
|
hostname is not grimmory:
|
||||||
{{ grimmory_pct_hostname_line | default('missing hostname line') }}
|
{{ grimmory_pct_hostname_line | default('missing hostname line') }}
|
||||||
vars:
|
vars:
|
||||||
grimmory_pct_hostname_line: >-
|
grimmory_pct_hostname_line: >-
|
||||||
@@ -66,13 +69,11 @@
|
|||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
argv:
|
argv:
|
||||||
- vzdump
|
- vzdump
|
||||||
- "149"
|
- "{{ grimmory_vmid }}"
|
||||||
- --storage
|
- --storage
|
||||||
- pbs
|
- pbs
|
||||||
- --mode
|
- --mode
|
||||||
- snapshot
|
- snapshot
|
||||||
- --prune-backups
|
|
||||||
- keep-all=1
|
|
||||||
- --exclude-path
|
- --exclude-path
|
||||||
- /var/lib/docker/fuse-overlayfs/*/merged
|
- /var/lib/docker/fuse-overlayfs/*/merged
|
||||||
|
|
||||||
@@ -90,6 +91,8 @@
|
|||||||
changed_when: true
|
changed_when: true
|
||||||
|
|
||||||
- import_playbook: pve-grimmory.yml
|
- import_playbook: pve-grimmory.yml
|
||||||
|
vars:
|
||||||
|
pve_provisioning_enabled: false
|
||||||
|
|
||||||
- name: Verify Grimmory public endpoint after update
|
- name: Verify Grimmory public endpoint after update
|
||||||
hosts: ru-vps
|
hosts: ru-vps
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
---
|
---
|
||||||
- name: Configure Gyro investment allocator host
|
- name: Configure Gyro investment allocator host
|
||||||
hosts: gyro
|
# Таргет переопределяем ради blue-green переезда на OpenTofu: во время
|
||||||
|
# миграции этот play нужно прогнать против нового контейнера на временном
|
||||||
|
# адресе. Просто `--limit gyro-new` для этого НЕ годится — лимит
|
||||||
|
# пересекается с паттерном play и даёт ноль хостов, а не перенацеливание
|
||||||
|
# (проверено 2026-09-02 через --list-hosts на всех плейбуках).
|
||||||
|
# Использовать вместе с --limit, чтобы play создания контейнера отсеялся:
|
||||||
|
# ansible-playbook playbooks/gyro.yml \
|
||||||
|
# -e pve_config_target=gyro-new --limit gyro-new
|
||||||
|
# По умолчанию поведение не меняется.
|
||||||
|
hosts: "{{ pve_config_target | default('gyro') }}"
|
||||||
gather_facts: true
|
gather_facts: true
|
||||||
roles:
|
roles:
|
||||||
- role: gyro
|
- role: gyro
|
||||||
|
|||||||
@@ -2,19 +2,23 @@
|
|||||||
- name: Verify Mihomo PBS audit before update
|
- name: Verify Mihomo PBS audit before update
|
||||||
hosts: mini-pc
|
hosts: mini-pc
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
|
vars:
|
||||||
|
mihomo_vmid: "{{ homelab_services['mihomo'].vmid }}"
|
||||||
tasks:
|
tasks:
|
||||||
- name: Read VMID 143 configuration
|
- name: Read Mihomo VMID configuration
|
||||||
ansible.builtin.command: "pct config 143"
|
ansible.builtin.command: "pct config {{ mihomo_vmid }}"
|
||||||
register: mihomo_pct_config
|
register: mihomo_pct_config
|
||||||
changed_when: false
|
changed_when: false
|
||||||
failed_when: false
|
failed_when: false
|
||||||
|
|
||||||
- name: Refuse to run backup unless VMID 143 is Mihomo
|
- name: Refuse to run backup unless registry VMID is Mihomo
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- mihomo_pct_config.rc == 0
|
- mihomo_pct_config.rc == 0
|
||||||
- mihomo_update_hostname == 'mihomo'
|
- mihomo_update_hostname == 'mihomo'
|
||||||
fail_msg: VMID 143 must be the Mihomo container before backup.
|
fail_msg: >-
|
||||||
|
VMID {{ mihomo_vmid }} from the service registry must be the Mihomo
|
||||||
|
container before backup.
|
||||||
vars:
|
vars:
|
||||||
mihomo_update_hostname: >-
|
mihomo_update_hostname: >-
|
||||||
{{ mihomo_pct_config.stdout_lines
|
{{ mihomo_pct_config.stdout_lines
|
||||||
@@ -41,13 +45,11 @@
|
|||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
argv:
|
argv:
|
||||||
- vzdump
|
- vzdump
|
||||||
- "143"
|
- "{{ mihomo_vmid }}"
|
||||||
- --storage
|
- --storage
|
||||||
- pbs
|
- pbs
|
||||||
- --mode
|
- --mode
|
||||||
- snapshot
|
- snapshot
|
||||||
- --prune-backups
|
|
||||||
- keep-all=1
|
|
||||||
- --exclude-path
|
- --exclude-path
|
||||||
- /var/lib/docker/fuse-overlayfs/*/merged
|
- /var/lib/docker/fuse-overlayfs/*/merged
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,36 @@
|
|||||||
|
# Профиль выполняется ВНУТРИ контейнера gitea, а не на ноде cloud-pc.
|
||||||
|
# До переезда на OpenTofu (2026-09-02) данные жили в bind mount каталога ноды
|
||||||
|
# /opt/data/gitea, и профиль работал на самой ноде. После переезда данные —
|
||||||
|
# это volume контейнера (mp0 на datastore), снаружи LXC такого пути больше нет,
|
||||||
|
# поэтому профиль переехал внутрь по образцу vaultwarden. Репозиторий restic
|
||||||
|
# тот же, цепочка снапшотов продолжается; изменились только пути внутри них.
|
||||||
- name: Configure Gitea offsite backup to Yandex Disk
|
- name: Configure Gitea offsite backup to Yandex Disk
|
||||||
hosts: cloud-pc
|
hosts: gitea
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
vars:
|
vars:
|
||||||
|
ansible_become: false
|
||||||
offsite_profile: gitea
|
offsite_profile: gitea
|
||||||
offsite_repository: rclone:yadisk:System/Backups/HomeLab/restic/gitea
|
offsite_repository: rclone:yadisk:System/Backups/HomeLab/restic/gitea
|
||||||
offsite_source_path: /opt/data/gitea
|
offsite_source_path: /opt/gitea/data
|
||||||
offsite_sqlite_db: /opt/data/gitea/gitea/gitea.db
|
offsite_sqlite_db: /opt/gitea/data/gitea/gitea.db
|
||||||
offsite_backup_tag: gitea,cloud-pc,yadisk
|
offsite_backup_tag: gitea,cloud-pc,yadisk
|
||||||
offsite_timer_oncalendar: "*-*-* 04:15:00"
|
offsite_timer_oncalendar: "*-*-* 04:15:00"
|
||||||
offsite_rclone_config_local: ~/.config/rclone/rclone.conf
|
offsite_rclone_config_local: ~/.config/rclone/rclone.conf
|
||||||
offsite_restic_password_local: "{{ playbook_dir }}/../generated/restic-offsite-password"
|
offsite_restic_password_local: "{{ playbook_dir }}/../generated/restic-offsite-password"
|
||||||
offsite_excludes:
|
offsite_excludes:
|
||||||
- /opt/data/gitea/gitea/gitea.db
|
- /opt/gitea/data/gitea/gitea.db
|
||||||
- /opt/data/gitea/gitea/gitea.db-shm
|
- /opt/gitea/data/gitea/gitea.db-shm
|
||||||
- /opt/data/gitea/gitea/gitea.db-wal
|
- /opt/gitea/data/gitea/gitea.db-wal
|
||||||
- /opt/data/gitea/gitea/log/**
|
- /opt/gitea/data/gitea/log/**
|
||||||
- /opt/data/gitea/gitea/sessions/**
|
- /opt/gitea/data/gitea/sessions/**
|
||||||
- /opt/data/gitea/gitea/queues/**
|
- /opt/gitea/data/gitea/queues/**
|
||||||
- /opt/data/gitea/gitea/tmp/**
|
- /opt/gitea/data/gitea/tmp/**
|
||||||
|
# lost+found появился вместе с переходом на volume: это свежая ext4, и
|
||||||
|
# каталог принадлежит uid 0 хоста, что внутри unprivileged LXC видно как
|
||||||
|
# nobody:nogroup и нечитаемо. Без исключения restic отдаёт exit 3
|
||||||
|
# ("at least one source file could not be read") и юнит падает каждую
|
||||||
|
# ночь, хотя снапшот при этом сохраняется. Найдено 2026-09-02.
|
||||||
|
- /opt/gitea/data/lost+found/**
|
||||||
tasks:
|
tasks:
|
||||||
- name: Configure restic offsite profile
|
- name: Configure restic offsite profile
|
||||||
ansible.builtin.include_tasks: ../tasks/offsite-restic-profile.yml
|
ansible.builtin.include_tasks: ../tasks/offsite-restic-profile.yml
|
||||||
|
|||||||
@@ -3,6 +3,13 @@
|
|||||||
hosts: mini-pc
|
hosts: mini-pc
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
vars:
|
vars:
|
||||||
|
# После переезда на OpenTofu (tofu/svc-adguard.tf, VMID 158) этот play —
|
||||||
|
# legacy: он таргетит СТАРЫЙ VMID 144 и его handler сделал бы pct start 144,
|
||||||
|
# подняв остановленный откат на боевом адресе. Пропускаем, пока реестр
|
||||||
|
# говорит provisioner: tofu. Для намеренного legacy rollback:
|
||||||
|
# -e pve_adguard_legacy_provisioning_enabled=true
|
||||||
|
pve_adguard_legacy_provisioning_enabled: >-
|
||||||
|
{{ homelab_services['adguard'].provisioner != 'tofu' }}
|
||||||
adguard_vmid: 144
|
adguard_vmid: 144
|
||||||
adguard_hostname: adguard
|
adguard_hostname: adguard
|
||||||
adguard_ip: 192.168.1.28/24
|
adguard_ip: 192.168.1.28/24
|
||||||
@@ -15,6 +22,10 @@
|
|||||||
- name: restart adguard lxc
|
- name: restart adguard lxc
|
||||||
ansible.builtin.shell: "pct stop {{ adguard_vmid }} || true; pct start {{ adguard_vmid }}"
|
ansible.builtin.shell: "pct stop {{ adguard_vmid }} || true; pct start {{ adguard_vmid }}"
|
||||||
changed_when: true
|
changed_when: true
|
||||||
|
pre_tasks:
|
||||||
|
- name: Skip legacy AdGuard provisioning after cutover
|
||||||
|
ansible.builtin.meta: end_play
|
||||||
|
when: not (pve_adguard_legacy_provisioning_enabled | bool)
|
||||||
tasks:
|
tasks:
|
||||||
- name: Check if AdGuard LXC exists
|
- name: Check if AdGuard LXC exists
|
||||||
ansible.builtin.command: "pct config {{ adguard_vmid }}"
|
ansible.builtin.command: "pct config {{ adguard_vmid }}"
|
||||||
@@ -93,7 +104,16 @@
|
|||||||
ansible_become: false
|
ansible_become: false
|
||||||
|
|
||||||
- name: Configure AdGuard Home inside LXC
|
- name: Configure AdGuard Home inside LXC
|
||||||
hosts: adguard
|
# Таргет переопределяем ради blue-green переезда на OpenTofu: во время
|
||||||
|
# миграции этот play нужно прогнать против нового контейнера на временном
|
||||||
|
# адресе. Просто `--limit adguard-new` для этого НЕ годится — лимит
|
||||||
|
# пересекается с паттерном play и даёт ноль хостов, а не перенацеливание
|
||||||
|
# (проверено 2026-09-02 через --list-hosts на всех плейбуках).
|
||||||
|
# Использовать вместе с --limit, чтобы play создания контейнера отсеялся:
|
||||||
|
# ansible-playbook playbooks/pve-adguard.yml \
|
||||||
|
# -e pve_config_target=adguard-new --limit adguard-new
|
||||||
|
# По умолчанию поведение не меняется.
|
||||||
|
hosts: "{{ pve_config_target | default('adguard') }}"
|
||||||
gather_facts: true
|
gather_facts: true
|
||||||
vars:
|
vars:
|
||||||
ansible_become: false
|
ansible_become: false
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
# Списки VMID выводятся из реестра homelab_services по полю backup.job, а не
|
||||||
|
# перечисляются вручную. Раньше добавление сервиса требовало отдельной правки
|
||||||
|
# здесь, и о ней легко было забыть — CT 148 emergency-bot до сих пор без бэкапа
|
||||||
|
# именно по этой причине.
|
||||||
- name: Configure Proxmox backup jobs
|
- name: Configure Proxmox backup jobs
|
||||||
hosts: mini-pc
|
hosts: mini-pc
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
@@ -6,7 +10,10 @@
|
|||||||
- id: homelab-pbs-daily-cloud
|
- id: homelab-pbs-daily-cloud
|
||||||
comment: Daily PBS backup for cloud-pc service containers
|
comment: Daily PBS backup for cloud-pc service containers
|
||||||
node: cloud-pc
|
node: cloud-pc
|
||||||
vmid: 141,145,146,147,149
|
vmid: >-
|
||||||
|
{{ homelab_services.values() | selectattr('backup.job', 'defined')
|
||||||
|
| selectattr('backup.job', 'eq', 'homelab-pbs-daily-cloud')
|
||||||
|
| map(attribute='vmid') | sort | join(',') }}
|
||||||
storage: pbs
|
storage: pbs
|
||||||
schedule: "02:10"
|
schedule: "02:10"
|
||||||
mode: snapshot
|
mode: snapshot
|
||||||
@@ -14,7 +21,10 @@
|
|||||||
- id: homelab-pbs-daily-mini
|
- id: homelab-pbs-daily-mini
|
||||||
comment: Daily PBS backup for mini-pc service containers
|
comment: Daily PBS backup for mini-pc service containers
|
||||||
node: mini-pc
|
node: mini-pc
|
||||||
vmid: 132,140,142,143,144,150
|
vmid: >-
|
||||||
|
{{ homelab_services.values() | selectattr('backup.job', 'defined')
|
||||||
|
| selectattr('backup.job', 'eq', 'homelab-pbs-daily-mini')
|
||||||
|
| map(attribute='vmid') | sort | join(',') }}
|
||||||
storage: pbs
|
storage: pbs
|
||||||
schedule: "02:40"
|
schedule: "02:40"
|
||||||
mode: snapshot
|
mode: snapshot
|
||||||
@@ -22,7 +32,10 @@
|
|||||||
- id: homelab-local-weekly-pbs
|
- id: homelab-local-weekly-pbs
|
||||||
comment: Weekly local backup for PBS container rootfs/config
|
comment: Weekly local backup for PBS container rootfs/config
|
||||||
node: cloud-pc
|
node: cloud-pc
|
||||||
vmid: 120
|
vmid: >-
|
||||||
|
{{ homelab_services.values() | selectattr('backup.job', 'defined')
|
||||||
|
| selectattr('backup.job', 'eq', 'homelab-local-weekly-pbs')
|
||||||
|
| map(attribute='vmid') | sort | join(',') }}
|
||||||
storage: backup
|
storage: backup
|
||||||
schedule: "Sun 03:30"
|
schedule: "Sun 03:30"
|
||||||
mode: snapshot
|
mode: snapshot
|
||||||
|
|||||||
@@ -3,6 +3,13 @@
|
|||||||
hosts: cloud-pc
|
hosts: cloud-pc
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
vars:
|
vars:
|
||||||
|
# После переезда на OpenTofu (tofu/svc-docker-test.tf, VMID 152) этот play —
|
||||||
|
# legacy: он таргетит СТАРЫЙ VMID 145 и его handler/"Start" сделали бы
|
||||||
|
# pct start 145, подняв остановленный откат на боевом адресе 192.168.1.29.
|
||||||
|
# Пропускаем, пока реестр говорит provisioner: tofu. Для намеренного
|
||||||
|
# legacy rollback: -e pve_docker_test_legacy_provisioning_enabled=true
|
||||||
|
pve_docker_test_legacy_provisioning_enabled: >-
|
||||||
|
{{ homelab_services['docker-test'].provisioner != 'tofu' }}
|
||||||
docker_test_vmid: 145
|
docker_test_vmid: 145
|
||||||
docker_test_hostname: docker-test
|
docker_test_hostname: docker-test
|
||||||
docker_test_ip: 192.168.1.29/24
|
docker_test_ip: 192.168.1.29/24
|
||||||
@@ -14,6 +21,10 @@
|
|||||||
- name: restart docker test lxc
|
- name: restart docker test lxc
|
||||||
ansible.builtin.shell: "pct stop {{ docker_test_vmid }} || true; pct start {{ docker_test_vmid }}"
|
ansible.builtin.shell: "pct stop {{ docker_test_vmid }} || true; pct start {{ docker_test_vmid }}"
|
||||||
changed_when: true
|
changed_when: true
|
||||||
|
pre_tasks:
|
||||||
|
- name: Skip legacy docker-test provisioning after cutover
|
||||||
|
ansible.builtin.meta: end_play
|
||||||
|
when: not (pve_docker_test_legacy_provisioning_enabled | bool)
|
||||||
tasks:
|
tasks:
|
||||||
- name: Check if Docker test LXC exists
|
- name: Check if Docker test LXC exists
|
||||||
ansible.builtin.command: "pct config {{ docker_test_vmid }}"
|
ansible.builtin.command: "pct config {{ docker_test_vmid }}"
|
||||||
@@ -79,7 +90,16 @@
|
|||||||
ansible_become: false
|
ansible_become: false
|
||||||
|
|
||||||
- name: Configure Docker test host
|
- name: Configure Docker test host
|
||||||
hosts: docker-test
|
# Таргет переопределяем ради blue-green переезда на OpenTofu: во время
|
||||||
|
# миграции этот play нужно прогнать против нового контейнера на временном
|
||||||
|
# адресе. Просто `--limit docker-test-new` для этого НЕ годится — лимит
|
||||||
|
# пересекается с паттерном play и даёт ноль хостов, а не перенацеливание
|
||||||
|
# (проверено 2026-09-02 через --list-hosts на всех плейбуках).
|
||||||
|
# Использовать вместе с --limit, чтобы play создания контейнера отсеялся:
|
||||||
|
# ansible-playbook playbooks/pve-docker-test.yml \
|
||||||
|
# -e pve_config_target=docker-test-new --limit docker-test-new
|
||||||
|
# По умолчанию поведение не меняется.
|
||||||
|
hosts: "{{ pve_config_target | default('docker-test') }}"
|
||||||
gather_facts: true
|
gather_facts: true
|
||||||
vars:
|
vars:
|
||||||
ansible_become: false
|
ansible_become: false
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
---
|
---
|
||||||
|
# После переезда на OpenTofu (tofu/services.tf, VMID 151) обе play здесь —
|
||||||
|
# legacy: они таргетят СТАРЫЙ VMID 148 на боевом адресе 192.168.1.32.
|
||||||
|
# Пропускаем, пока реестр говорит provisioner: tofu. Конфигурация emergency-bot
|
||||||
|
# в этот плейбук не входит — она в playbooks/emergency-access.yml. Для
|
||||||
|
# намеренного legacy rollback: -e pve_emergency_bot_legacy_provisioning_enabled=true
|
||||||
|
|
||||||
- name: Create emergency-bot LXC on mini-pc
|
- name: Create emergency-bot LXC on mini-pc
|
||||||
hosts: localhost
|
hosts: localhost
|
||||||
connection: local
|
connection: local
|
||||||
@@ -7,6 +13,8 @@
|
|||||||
vars:
|
vars:
|
||||||
ansible_become: false
|
ansible_become: false
|
||||||
ansible_python_interpreter: "{{ ansible_playbook_python }}"
|
ansible_python_interpreter: "{{ ansible_playbook_python }}"
|
||||||
|
pve_emergency_bot_legacy_provisioning_enabled: >-
|
||||||
|
{{ homelab_services['emergency-bot'].provisioner != 'tofu' }}
|
||||||
pve_lxc_vmid: 148
|
pve_lxc_vmid: 148
|
||||||
pve_lxc_node: mini-pc
|
pve_lxc_node: mini-pc
|
||||||
pve_lxc_hostname: emergency-bot
|
pve_lxc_hostname: emergency-bot
|
||||||
@@ -18,12 +26,23 @@
|
|||||||
pve_lxc_swap: 256
|
pve_lxc_swap: 256
|
||||||
pve_lxc_startup: order=70
|
pve_lxc_startup: order=70
|
||||||
pve_lxc_ostemplate: "{{ lookup('env', 'PVE_LXC_OSTEMPLATE') | default('local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst', true) }}"
|
pve_lxc_ostemplate: "{{ lookup('env', 'PVE_LXC_OSTEMPLATE') | default('local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst', true) }}"
|
||||||
|
pre_tasks:
|
||||||
|
- name: Skip legacy emergency-bot provisioning after cutover
|
||||||
|
ansible.builtin.meta: end_play
|
||||||
|
when: not (pve_emergency_bot_legacy_provisioning_enabled | bool)
|
||||||
roles:
|
roles:
|
||||||
- role: pve_lxc
|
- role: pve_lxc
|
||||||
|
|
||||||
- name: Wait for emergency-bot SSH
|
- name: Wait for emergency-bot SSH
|
||||||
hosts: emergency-bot
|
hosts: emergency-bot
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
|
vars:
|
||||||
|
pve_emergency_bot_legacy_provisioning_enabled: >-
|
||||||
|
{{ homelab_services['emergency-bot'].provisioner != 'tofu' }}
|
||||||
|
pre_tasks:
|
||||||
|
- name: Skip legacy emergency-bot provisioning after cutover
|
||||||
|
ansible.builtin.meta: end_play
|
||||||
|
when: not (pve_emergency_bot_legacy_provisioning_enabled | bool)
|
||||||
tasks:
|
tasks:
|
||||||
- name: Wait for emergency-bot to accept SSH connections
|
- name: Wait for emergency-bot to accept SSH connections
|
||||||
ansible.builtin.wait_for_connection:
|
ansible.builtin.wait_for_connection:
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
- name: Create Gitea LXC on cloud-pc
|
- name: Create Gitea LXC on cloud-pc
|
||||||
hosts: cloud-pc
|
hosts: cloud-pc
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
|
pre_tasks:
|
||||||
|
- name: Skip legacy Gitea provisioning for runtime-only calls
|
||||||
|
ansible.builtin.meta: end_play
|
||||||
|
when: not (pve_provisioning_enabled | default(true) | bool)
|
||||||
vars:
|
vars:
|
||||||
gitea_vmid: 141
|
gitea_vmid: 141
|
||||||
gitea_hostname: gitea
|
gitea_hostname: gitea
|
||||||
@@ -111,7 +115,16 @@
|
|||||||
ansible_become: false
|
ansible_become: false
|
||||||
|
|
||||||
- name: Configure Docker and Gitea inside LXC
|
- name: Configure Docker and Gitea inside LXC
|
||||||
hosts: gitea
|
# Таргет переопределяем ради blue-green переезда на OpenTofu: во время
|
||||||
|
# миграции этот play нужно прогнать против нового контейнера на временном
|
||||||
|
# адресе. Просто `--limit gitea-new` для этого НЕ годится — лимит
|
||||||
|
# пересекается с паттерном play и даёт ноль хостов, а не перенацеливание
|
||||||
|
# (проверено 2026-09-02 через --list-hosts на всех плейбуках).
|
||||||
|
# Использовать вместе с --limit, чтобы play создания контейнера отсеялся:
|
||||||
|
# ansible-playbook playbooks/pve-gitea.yml \
|
||||||
|
# -e pve_config_target=gitea-new --limit gitea-new
|
||||||
|
# По умолчанию поведение не меняется.
|
||||||
|
hosts: "{{ pve_config_target | default('gitea') }}"
|
||||||
gather_facts: true
|
gather_facts: true
|
||||||
vars:
|
vars:
|
||||||
ansible_become: false
|
ansible_become: false
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
- name: Guard Grimmory VMID before API updates
|
- name: Guard Grimmory VMID before API updates
|
||||||
hosts: cloud-pc
|
hosts: cloud-pc
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
|
pre_tasks:
|
||||||
|
- name: Skip legacy Grimmory provisioning for runtime-only calls
|
||||||
|
ansible.builtin.meta: end_play
|
||||||
|
when: not (pve_provisioning_enabled | default(true) | bool)
|
||||||
tasks:
|
tasks:
|
||||||
- name: Read existing VMID 149 configuration
|
- name: Read existing VMID 149 configuration
|
||||||
ansible.builtin.command: pct config 149
|
ansible.builtin.command: pct config 149
|
||||||
@@ -28,6 +32,10 @@
|
|||||||
connection: local
|
connection: local
|
||||||
become: false
|
become: false
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
|
pre_tasks:
|
||||||
|
- name: Skip legacy Grimmory provisioning for runtime-only calls
|
||||||
|
ansible.builtin.meta: end_play
|
||||||
|
when: not (pve_provisioning_enabled | default(true) | bool)
|
||||||
vars:
|
vars:
|
||||||
ansible_become: false
|
ansible_become: false
|
||||||
ansible_python_interpreter: "{{ ansible_playbook_python }}"
|
ansible_python_interpreter: "{{ ansible_playbook_python }}"
|
||||||
@@ -52,6 +60,10 @@
|
|||||||
- name: Configure Grimmory LXC devices
|
- name: Configure Grimmory LXC devices
|
||||||
hosts: cloud-pc
|
hosts: cloud-pc
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
|
pre_tasks:
|
||||||
|
- name: Skip legacy Grimmory provisioning for runtime-only calls
|
||||||
|
ansible.builtin.meta: end_play
|
||||||
|
when: not (pve_provisioning_enabled | default(true) | bool)
|
||||||
vars:
|
vars:
|
||||||
grimmory_vmid: 149
|
grimmory_vmid: 149
|
||||||
handlers:
|
handlers:
|
||||||
@@ -119,13 +131,32 @@
|
|||||||
ansible_become: false
|
ansible_become: false
|
||||||
|
|
||||||
- name: Configure Grimmory runtime
|
- name: Configure Grimmory runtime
|
||||||
hosts: grimmory
|
# Таргет переопределяем ради blue-green переезда на OpenTofu: во время
|
||||||
|
# миграции этот play нужно прогнать против нового контейнера на временном
|
||||||
|
# адресе. Просто `--limit grimmory-new` для этого НЕ годится — лимит
|
||||||
|
# пересекается с паттерном play и даёт ноль хостов, а не перенацеливание
|
||||||
|
# (проверено 2026-09-02 через --list-hosts на всех плейбуках).
|
||||||
|
# Использовать вместе с --limit, чтобы play создания контейнера отсеялся:
|
||||||
|
# ansible-playbook playbooks/pve-grimmory.yml \
|
||||||
|
# -e pve_config_target=grimmory-new --limit grimmory-new
|
||||||
|
# По умолчанию поведение не меняется.
|
||||||
|
hosts: "{{ pve_config_target | default('grimmory') }}"
|
||||||
gather_facts: true
|
gather_facts: true
|
||||||
vars:
|
vars:
|
||||||
ansible_become: false
|
ansible_become: false
|
||||||
grimmory_root: /opt/grimmory
|
grimmory_root: /opt/grimmory
|
||||||
grimmory_image: grimmory/grimmory:v3.2.4@sha256:dfa7afdfcf25d649fd664497a62385dd00cd9678c37546e182c172e41c8e80cb
|
grimmory_image: grimmory/grimmory:v3.2.4@sha256:dfa7afdfcf25d649fd664497a62385dd00cd9678c37546e182c172e41c8e80cb
|
||||||
grimmory_mariadb_image: lscr.io/linuxserver/mariadb:11.4.8@sha256:91de7f701bc7fc3a424b81beafca7a7c6c4c5b7c8be6afd2ae148698695c0b0c
|
grimmory_mariadb_image: lscr.io/linuxserver/mariadb:11.4.8@sha256:91de7f701bc7fc3a424b81beafca7a7c6c4c5b7c8be6afd2ae148698695c0b0c
|
||||||
|
# Адрес, на который биндится порт, на который смотрят правила DOCKER-USER и
|
||||||
|
# по которому проверяется health. Раньше во всех трёх местах был зашит
|
||||||
|
# боевой 192.168.1.34, из-за чего play нельзя было прогнать против другого
|
||||||
|
# контейнера: Docker не биндит чужой адрес и роняет grimmory.service, а
|
||||||
|
# health-check уходил по сети в БОЕВОЙ сервис и давал ложный успех.
|
||||||
|
# expected_lan_ip — уже принятая в репозитории идиома для "адрес этого
|
||||||
|
# хоста в LAN" (её же использует roles/uptime_kuma). На боевом grimmory она
|
||||||
|
# равна 192.168.1.34, поэтому рендер там не меняется. Найдено 2026-09-02
|
||||||
|
# при blue-green переезде на OpenTofu.
|
||||||
|
grimmory_bind_ip: "{{ expected_lan_ip }}"
|
||||||
tasks:
|
tasks:
|
||||||
- name: Install Grimmory runtime packages
|
- name: Install Grimmory runtime packages
|
||||||
ansible.builtin.apt:
|
ansible.builtin.apt:
|
||||||
@@ -298,7 +329,7 @@
|
|||||||
mariadb:
|
mariadb:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- "192.168.1.34:6060:6060"
|
- "{{ grimmory_bind_ip }}:6060:6060"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
- ./books:/books
|
- ./books:/books
|
||||||
@@ -338,9 +369,9 @@
|
|||||||
iptables -N GRIMMORY-FILTER 2>/dev/null || true
|
iptables -N GRIMMORY-FILTER 2>/dev/null || true
|
||||||
iptables -F GRIMMORY-FILTER
|
iptables -F GRIMMORY-FILTER
|
||||||
iptables -A GRIMMORY-FILTER -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
iptables -A GRIMMORY-FILTER -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
||||||
iptables -A GRIMMORY-FILTER -s {{ homelab_lan_cidr }} -p tcp -m conntrack --ctorigdst 192.168.1.34 --ctorigdstport 6060 -j ACCEPT
|
iptables -A GRIMMORY-FILTER -s {{ homelab_lan_cidr }} -p tcp -m conntrack --ctorigdst {{ grimmory_bind_ip }} --ctorigdstport 6060 -j ACCEPT
|
||||||
iptables -A GRIMMORY-FILTER -s {{ openvpn_network_cidr }} -p tcp -m conntrack --ctorigdst 192.168.1.34 --ctorigdstport 6060 -j ACCEPT
|
iptables -A GRIMMORY-FILTER -s {{ openvpn_network_cidr }} -p tcp -m conntrack --ctorigdst {{ grimmory_bind_ip }} --ctorigdstport 6060 -j ACCEPT
|
||||||
iptables -A GRIMMORY-FILTER -p tcp -m conntrack --ctorigdst 192.168.1.34 --ctorigdstport 6060 -j DROP
|
iptables -A GRIMMORY-FILTER -p tcp -m conntrack --ctorigdst {{ grimmory_bind_ip }} --ctorigdstport 6060 -j DROP
|
||||||
iptables -A GRIMMORY-FILTER -j RETURN
|
iptables -A GRIMMORY-FILTER -j RETURN
|
||||||
iptables -C DOCKER-USER -j GRIMMORY-FILTER 2>/dev/null || iptables -I DOCKER-USER 1 -j GRIMMORY-FILTER
|
iptables -C DOCKER-USER -j GRIMMORY-FILTER 2>/dev/null || iptables -I DOCKER-USER 1 -j GRIMMORY-FILTER
|
||||||
register: grimmory_firewall_script
|
register: grimmory_firewall_script
|
||||||
@@ -417,7 +448,7 @@
|
|||||||
|
|
||||||
- name: Wait for Grimmory health endpoint
|
- name: Wait for Grimmory health endpoint
|
||||||
ansible.builtin.uri:
|
ansible.builtin.uri:
|
||||||
url: http://192.168.1.34:6060/api/v1/healthcheck
|
url: http://{{ grimmory_bind_ip }}:6060/api/v1/healthcheck
|
||||||
status_code: 200
|
status_code: 200
|
||||||
register: grimmory_health
|
register: grimmory_health
|
||||||
retries: 120
|
retries: 120
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
- name: Guard Gyro VMID before API updates
|
- name: Guard Gyro VMID before API updates
|
||||||
hosts: mini-pc
|
hosts: mini-pc
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
|
vars:
|
||||||
|
pve_gyro_legacy_provisioning_enabled: "{{ homelab_services['gyro'].provisioner != 'tofu' }}"
|
||||||
|
pre_tasks:
|
||||||
|
- name: Skip legacy Gyro provisioning after cutover
|
||||||
|
ansible.builtin.meta: end_play
|
||||||
|
when: not (pve_gyro_legacy_provisioning_enabled | bool)
|
||||||
tasks:
|
tasks:
|
||||||
- name: Read existing VMID 150 configuration
|
- name: Read existing VMID 150 configuration
|
||||||
ansible.builtin.command: pct config 150
|
ansible.builtin.command: pct config 150
|
||||||
@@ -29,6 +35,7 @@
|
|||||||
become: false
|
become: false
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
vars:
|
vars:
|
||||||
|
pve_gyro_legacy_provisioning_enabled: "{{ homelab_services['gyro'].provisioner != 'tofu' }}"
|
||||||
ansible_become: false
|
ansible_become: false
|
||||||
ansible_python_interpreter: "{{ ansible_playbook_python }}"
|
ansible_python_interpreter: "{{ ansible_playbook_python }}"
|
||||||
pve_lxc_vmid: 150
|
pve_lxc_vmid: 150
|
||||||
@@ -45,6 +52,10 @@
|
|||||||
pve_lxc_update: false
|
pve_lxc_update: false
|
||||||
pve_lxc_features: []
|
pve_lxc_features: []
|
||||||
pve_lxc_ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
pve_lxc_ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
||||||
|
pre_tasks:
|
||||||
|
- name: Skip legacy Gyro provisioning after cutover
|
||||||
|
ansible.builtin.meta: end_play
|
||||||
|
when: not (pve_gyro_legacy_provisioning_enabled | bool)
|
||||||
roles:
|
roles:
|
||||||
- role: pve_lxc
|
- role: pve_lxc
|
||||||
|
|
||||||
@@ -52,7 +63,12 @@
|
|||||||
hosts: mini-pc
|
hosts: mini-pc
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
vars:
|
vars:
|
||||||
|
pve_gyro_legacy_provisioning_enabled: "{{ homelab_services['gyro'].provisioner != 'tofu' }}"
|
||||||
gyro_vmid: 150
|
gyro_vmid: 150
|
||||||
|
pre_tasks:
|
||||||
|
- name: Skip legacy Gyro provisioning after cutover
|
||||||
|
ansible.builtin.meta: end_play
|
||||||
|
when: not (pve_gyro_legacy_provisioning_enabled | bool)
|
||||||
tasks:
|
tasks:
|
||||||
- name: Read Gyro LXC configuration
|
- name: Read Gyro LXC configuration
|
||||||
ansible.builtin.command: "pct config {{ gyro_vmid }}"
|
ansible.builtin.command: "pct config {{ gyro_vmid }}"
|
||||||
|
|||||||
@@ -1,305 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Create memoir-bot LXC on mini-pc
|
|
||||||
hosts: mini-pc
|
|
||||||
gather_facts: false
|
|
||||||
vars:
|
|
||||||
memoir_bot_vmid: 142
|
|
||||||
memoir_bot_hostname: memoir-bot
|
|
||||||
memoir_bot_ip: 192.168.1.26/24
|
|
||||||
memoir_bot_gateway: 192.168.1.1
|
|
||||||
memoir_bot_bridge: vmbr0
|
|
||||||
memoir_bot_rootfs: local-lvm:16
|
|
||||||
memoir_bot_ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
|
||||||
memoir_bot_pubkey_file: ~/.ssh/id_ed25519_homelab.pub
|
|
||||||
handlers:
|
|
||||||
- name: restart memoir-bot lxc
|
|
||||||
ansible.builtin.shell: "pct stop {{ memoir_bot_vmid }} || true; pct start {{ memoir_bot_vmid }}"
|
|
||||||
changed_when: true
|
|
||||||
tasks:
|
|
||||||
- name: Check if memoir-bot LXC exists
|
|
||||||
ansible.builtin.command: "pct config {{ memoir_bot_vmid }}"
|
|
||||||
register: memoir_bot_pct_config
|
|
||||||
changed_when: false
|
|
||||||
failed_when: false
|
|
||||||
|
|
||||||
- name: Install memoir-bot LXC SSH public key on PVE host
|
|
||||||
ansible.builtin.copy:
|
|
||||||
dest: /tmp/memoir-bot-lxc.pub
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0600"
|
|
||||||
content: "{{ lookup('file', memoir_bot_pubkey_file) }}\n"
|
|
||||||
when: memoir_bot_pct_config.rc != 0
|
|
||||||
|
|
||||||
- name: Create memoir-bot LXC
|
|
||||||
ansible.builtin.command: >-
|
|
||||||
pct create {{ memoir_bot_vmid }} {{ memoir_bot_ostemplate }}
|
|
||||||
--hostname {{ memoir_bot_hostname }}
|
|
||||||
--rootfs {{ memoir_bot_rootfs }}
|
|
||||||
--cores 1
|
|
||||||
--memory 512
|
|
||||||
--swap 512
|
|
||||||
--net0 name=eth0,bridge={{ memoir_bot_bridge }},gw={{ memoir_bot_gateway }},ip={{ memoir_bot_ip }},firewall=1
|
|
||||||
--nameserver 1.1.1.1
|
|
||||||
--unprivileged 1
|
|
||||||
--features nesting=1,keyctl=1
|
|
||||||
--onboot 1
|
|
||||||
--startup order=60
|
|
||||||
--cmode shell
|
|
||||||
--ssh-public-keys /tmp/memoir-bot-lxc.pub
|
|
||||||
when: memoir_bot_pct_config.rc != 0
|
|
||||||
|
|
||||||
- name: Start memoir-bot LXC
|
|
||||||
ansible.builtin.command: "pct start {{ memoir_bot_vmid }}"
|
|
||||||
register: memoir_bot_pct_start
|
|
||||||
changed_when: memoir_bot_pct_start.rc == 0
|
|
||||||
failed_when: memoir_bot_pct_start.rc not in [0, 255]
|
|
||||||
|
|
||||||
- name: Allow FUSE device in memoir-bot LXC config
|
|
||||||
ansible.builtin.lineinfile:
|
|
||||||
path: "/etc/pve/lxc/{{ memoir_bot_vmid }}.conf"
|
|
||||||
line: "lxc.cgroup2.devices.allow: c 10:229 rwm"
|
|
||||||
state: present
|
|
||||||
notify: restart memoir-bot lxc
|
|
||||||
|
|
||||||
- name: Bind mount FUSE device in memoir-bot LXC config
|
|
||||||
ansible.builtin.lineinfile:
|
|
||||||
path: "/etc/pve/lxc/{{ memoir_bot_vmid }}.conf"
|
|
||||||
line: "lxc.mount.entry: /dev/fuse dev/fuse none bind,create=file"
|
|
||||||
state: present
|
|
||||||
notify: restart memoir-bot lxc
|
|
||||||
|
|
||||||
- name: Apply pending LXC config changes
|
|
||||||
ansible.builtin.meta: flush_handlers
|
|
||||||
|
|
||||||
- name: Wait for memoir-bot SSH through ru-vps
|
|
||||||
ansible.builtin.wait_for_connection:
|
|
||||||
timeout: 120
|
|
||||||
delegate_to: memoir-bot
|
|
||||||
vars:
|
|
||||||
ansible_become: false
|
|
||||||
|
|
||||||
- name: Configure Docker and memoir-bot inside LXC
|
|
||||||
hosts: memoir-bot
|
|
||||||
gather_facts: true
|
|
||||||
vars:
|
|
||||||
ansible_become: false
|
|
||||||
memoir_bot_source_dir: /home/ada/Documents/Projects/Other/memoir_bot/
|
|
||||||
memoir_bot_app_dir: /opt/memoir-bot/app
|
|
||||||
memoir_bot_state_dir: /srv/memoir-bot
|
|
||||||
memoir_bot_env_file: /srv/memoir-bot/.env
|
|
||||||
memoir_bot_env_source: /home/ada/Documents/Projects/Other/memoir_bot/.env
|
|
||||||
memoir_bot_ssh_private_key_file: ~/.ssh/id_ed25519
|
|
||||||
memoir_bot_ssh_public_key_file: ~/.ssh/id_ed25519.pub
|
|
||||||
memoir_bot_vault_repo: git@github.com:ada-dmitry/SecondBrain.git
|
|
||||||
memoir_bot_vault_dir: /srv/memoir-bot/vault
|
|
||||||
memoir_bot_image: memoir-bot:local
|
|
||||||
memoir_bot_container_name: memoir-bot
|
|
||||||
tasks:
|
|
||||||
- name: Install Docker and deploy dependencies
|
|
||||||
ansible.builtin.apt:
|
|
||||||
name:
|
|
||||||
- docker.io
|
|
||||||
- fuse-overlayfs
|
|
||||||
- git
|
|
||||||
- openssh-client
|
|
||||||
- rsync
|
|
||||||
- ca-certificates
|
|
||||||
- curl
|
|
||||||
state: present
|
|
||||||
update_cache: true
|
|
||||||
|
|
||||||
- name: Ensure Docker config directory exists
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: /etc/docker
|
|
||||||
state: directory
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0755"
|
|
||||||
|
|
||||||
- name: Configure Docker storage driver for unprivileged LXC
|
|
||||||
ansible.builtin.copy:
|
|
||||||
dest: /etc/docker/daemon.json
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0644"
|
|
||||||
content: |
|
|
||||||
{
|
|
||||||
"storage-driver": "fuse-overlayfs"
|
|
||||||
}
|
|
||||||
register: docker_daemon_config
|
|
||||||
|
|
||||||
- name: Ensure Docker service is enabled and running
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: docker
|
|
||||||
state: "{{ 'restarted' if docker_daemon_config.changed else 'started' }}"
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
- name: Ensure memoir-bot directories exist
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: "{{ item }}"
|
|
||||||
state: directory
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0750"
|
|
||||||
loop:
|
|
||||||
- "{{ memoir_bot_app_dir }}"
|
|
||||||
- "{{ memoir_bot_state_dir }}"
|
|
||||||
- "{{ memoir_bot_state_dir }}/ssh"
|
|
||||||
|
|
||||||
- name: Copy memoir-bot env example
|
|
||||||
ansible.builtin.copy:
|
|
||||||
dest: "{{ memoir_bot_state_dir }}/.env.example"
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0640"
|
|
||||||
content: |
|
|
||||||
BOT_TOKEN=123456:telegram-token
|
|
||||||
ALLOWED_USER_IDS=123456789
|
|
||||||
OBSIDIAN_VAULT_PATH=/srv/obsidian/vault
|
|
||||||
DAILY_NOTES_DIR=03 Journal
|
|
||||||
NOTE_PATH_FORMAT=%Y/%m/%d.%m.%y.md
|
|
||||||
MESSAGE_TIME_FORMAT=%H:%M
|
|
||||||
TELEGRAM_PROXY=
|
|
||||||
REMINDER_MIN_HOURS=2
|
|
||||||
REMINDER_MAX_HOURS=3
|
|
||||||
REMINDER_START_HOUR=7
|
|
||||||
REMINDER_END_HOUR=23
|
|
||||||
REMINDER_TIMEZONE=Europe/Moscow
|
|
||||||
|
|
||||||
- name: Copy memoir-bot env file when present locally
|
|
||||||
ansible.builtin.copy:
|
|
||||||
src: "{{ memoir_bot_env_source }}"
|
|
||||||
dest: "{{ memoir_bot_env_file }}"
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0600"
|
|
||||||
when: lookup('ansible.builtin.fileglob', memoir_bot_env_source) | length > 0
|
|
||||||
no_log: true
|
|
||||||
|
|
||||||
- name: Install GitHub SSH private key for memoir-bot
|
|
||||||
ansible.builtin.copy:
|
|
||||||
src: "{{ memoir_bot_ssh_private_key_file }}"
|
|
||||||
dest: "{{ memoir_bot_state_dir }}/ssh/id_ed25519"
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0600"
|
|
||||||
no_log: true
|
|
||||||
|
|
||||||
- name: Install GitHub SSH public key for memoir-bot when present locally
|
|
||||||
ansible.builtin.copy:
|
|
||||||
src: "{{ memoir_bot_ssh_public_key_file }}"
|
|
||||||
dest: "{{ memoir_bot_state_dir }}/ssh/id_ed25519.pub"
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0644"
|
|
||||||
when: lookup('ansible.builtin.fileglob', memoir_bot_ssh_public_key_file) | length > 0
|
|
||||||
|
|
||||||
- name: Scan GitHub SSH host key
|
|
||||||
ansible.builtin.command: ssh-keyscan github.com
|
|
||||||
register: memoir_bot_github_host_key
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Trust GitHub SSH host key for memoir-bot
|
|
||||||
ansible.builtin.known_hosts:
|
|
||||||
path: "{{ memoir_bot_state_dir }}/ssh/known_hosts"
|
|
||||||
name: github.com
|
|
||||||
key: "{{ memoir_bot_github_host_key.stdout }}"
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Clone SecondBrain vault
|
|
||||||
ansible.builtin.git:
|
|
||||||
repo: "{{ memoir_bot_vault_repo }}"
|
|
||||||
dest: "{{ memoir_bot_vault_dir }}"
|
|
||||||
key_file: "{{ memoir_bot_state_dir }}/ssh/id_ed25519"
|
|
||||||
accept_hostkey: true
|
|
||||||
update: true
|
|
||||||
version: main
|
|
||||||
register: memoir_bot_vault_checkout
|
|
||||||
|
|
||||||
- name: Configure memoir-bot vault Git author name
|
|
||||||
ansible.builtin.command: git config user.name memoir-bot
|
|
||||||
args:
|
|
||||||
chdir: "{{ memoir_bot_vault_dir }}"
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Configure memoir-bot vault Git author email
|
|
||||||
ansible.builtin.command: git config user.email memoir-bot@homelab.local
|
|
||||||
args:
|
|
||||||
chdir: "{{ memoir_bot_vault_dir }}"
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
- name: Sync memoir-bot source code
|
|
||||||
ansible.posix.synchronize:
|
|
||||||
src: "{{ memoir_bot_source_dir }}"
|
|
||||||
dest: "{{ memoir_bot_app_dir }}/"
|
|
||||||
delete: true
|
|
||||||
rsync_opts:
|
|
||||||
- "--exclude=.env"
|
|
||||||
- "--exclude=.git"
|
|
||||||
- "--exclude=.venv"
|
|
||||||
- "--exclude=__pycache__"
|
|
||||||
- "--exclude=*.pyc"
|
|
||||||
register: memoir_bot_source_sync
|
|
||||||
|
|
||||||
- name: Check if memoir-bot image exists
|
|
||||||
ansible.builtin.command: "docker image inspect {{ memoir_bot_image }}"
|
|
||||||
register: memoir_bot_image_inspect
|
|
||||||
changed_when: false
|
|
||||||
failed_when: false
|
|
||||||
|
|
||||||
- name: Build memoir-bot image
|
|
||||||
ansible.builtin.command: "docker build -t {{ memoir_bot_image }} {{ memoir_bot_app_dir }}"
|
|
||||||
when: memoir_bot_source_sync.changed or memoir_bot_image_inspect.rc != 0
|
|
||||||
register: memoir_bot_image_build
|
|
||||||
changed_when: memoir_bot_image_build.rc == 0
|
|
||||||
|
|
||||||
- name: Install memoir-bot systemd unit
|
|
||||||
ansible.builtin.copy:
|
|
||||||
dest: /etc/systemd/system/memoir-bot.service
|
|
||||||
owner: root
|
|
||||||
group: root
|
|
||||||
mode: "0644"
|
|
||||||
content: |
|
|
||||||
[Unit]
|
|
||||||
Description=Memoir Telegram bot container
|
|
||||||
After=docker.service
|
|
||||||
Requires=docker.service
|
|
||||||
ConditionPathExists={{ memoir_bot_env_file }}
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Restart=always
|
|
||||||
RestartSec=10
|
|
||||||
ExecStartPre=-/usr/bin/docker rm -f {{ memoir_bot_container_name }}
|
|
||||||
ExecStart=/usr/bin/docker run --rm \
|
|
||||||
--name {{ memoir_bot_container_name }} \
|
|
||||||
--env-file {{ memoir_bot_env_file }} \
|
|
||||||
-v {{ memoir_bot_state_dir }}/vault:/srv/obsidian/vault \
|
|
||||||
-v {{ memoir_bot_state_dir }}/ssh:/root/.ssh:ro \
|
|
||||||
{{ memoir_bot_image }}
|
|
||||||
ExecStop=/usr/bin/docker stop {{ memoir_bot_container_name }}
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
register: memoir_bot_unit
|
|
||||||
|
|
||||||
- name: Reload systemd when memoir-bot unit changes
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
daemon_reload: true
|
|
||||||
when: memoir_bot_unit.changed
|
|
||||||
|
|
||||||
- name: Check memoir-bot env file
|
|
||||||
ansible.builtin.stat:
|
|
||||||
path: "{{ memoir_bot_env_file }}"
|
|
||||||
register: memoir_bot_env
|
|
||||||
|
|
||||||
- name: Enable memoir-bot service
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: memoir-bot
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
- name: Start memoir-bot when env file exists
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: memoir-bot
|
|
||||||
state: "{{ 'restarted' if memoir_bot_source_sync.changed or memoir_bot_image_build.changed or memoir_bot_unit.changed or memoir_bot_vault_checkout.changed else 'started' }}"
|
|
||||||
when: memoir_bot_env.stat.exists
|
|
||||||
@@ -3,6 +3,13 @@
|
|||||||
hosts: mini-pc
|
hosts: mini-pc
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
vars:
|
vars:
|
||||||
|
# После переезда на OpenTofu (tofu/svc-mihomo.tf, VMID 159) этот play —
|
||||||
|
# legacy: он таргетит СТАРЫЙ VMID 143 и его handler сделал бы pct start 143,
|
||||||
|
# подняв остановленный откат на боевом адресе. Пропускаем, пока реестр
|
||||||
|
# говорит provisioner: tofu. Для намеренного legacy rollback:
|
||||||
|
# -e pve_mihomo_legacy_provisioning_enabled=true
|
||||||
|
pve_mihomo_legacy_provisioning_enabled: >-
|
||||||
|
{{ homelab_services['mihomo'].provisioner != 'tofu' }}
|
||||||
mihomo_vmid: 143
|
mihomo_vmid: 143
|
||||||
mihomo_hostname: mihomo
|
mihomo_hostname: mihomo
|
||||||
mihomo_ip: 192.168.1.27/24
|
mihomo_ip: 192.168.1.27/24
|
||||||
@@ -15,6 +22,10 @@
|
|||||||
- name: restart mihomo lxc
|
- name: restart mihomo lxc
|
||||||
ansible.builtin.shell: "pct stop {{ mihomo_vmid }} || true; pct start {{ mihomo_vmid }}"
|
ansible.builtin.shell: "pct stop {{ mihomo_vmid }} || true; pct start {{ mihomo_vmid }}"
|
||||||
changed_when: true
|
changed_when: true
|
||||||
|
pre_tasks:
|
||||||
|
- name: Skip legacy mihomo provisioning after cutover
|
||||||
|
ansible.builtin.meta: end_play
|
||||||
|
when: not (pve_mihomo_legacy_provisioning_enabled | bool)
|
||||||
tasks:
|
tasks:
|
||||||
- name: Check if mihomo LXC exists
|
- name: Check if mihomo LXC exists
|
||||||
ansible.builtin.command: "pct config {{ mihomo_vmid }}"
|
ansible.builtin.command: "pct config {{ mihomo_vmid }}"
|
||||||
@@ -107,7 +118,16 @@
|
|||||||
ansible_become: false
|
ansible_become: false
|
||||||
|
|
||||||
- name: Prepare mihomo runtime host
|
- name: Prepare mihomo runtime host
|
||||||
hosts: mihomo
|
# Таргет переопределяем ради blue-green переезда на OpenTofu: во время
|
||||||
|
# миграции этот play нужно прогнать против нового контейнера на временном
|
||||||
|
# адресе. Просто `--limit mihomo-new` для этого НЕ годится — лимит
|
||||||
|
# пересекается с паттерном play и даёт ноль хостов, а не перенацеливание
|
||||||
|
# (проверено 2026-09-02 через --list-hosts на всех плейбуках).
|
||||||
|
# Использовать вместе с --limit, чтобы play создания контейнера отсеялся:
|
||||||
|
# ansible-playbook playbooks/pve-mihomo.yml \
|
||||||
|
# -e pve_config_target=mihomo-new --limit mihomo-new
|
||||||
|
# По умолчанию поведение не меняется.
|
||||||
|
hosts: "{{ pve_config_target | default('mihomo') }}"
|
||||||
gather_facts: true
|
gather_facts: true
|
||||||
vars:
|
vars:
|
||||||
ansible_become: false
|
ansible_become: false
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
---
|
---
|
||||||
|
# После переезда на OpenTofu (tofu/svc-monitoring.tf, VMID 155) обе play здесь —
|
||||||
|
# legacy: они таргетят СТАРЫЙ VMID 146 на боевом адресе 192.168.1.30, а вторая
|
||||||
|
# делает pct reboot 146. Пропускаем, пока реестр говорит provisioner: tofu.
|
||||||
|
# Конфигурация (Uptime Kuma) в этот плейбук не входит — она в
|
||||||
|
# playbooks/uptime-kuma.yml. Замороженный Prometheus-стек в новый контейнер не
|
||||||
|
# разворачивался. Для намеренного legacy rollback:
|
||||||
|
# -e pve_monitoring_legacy_provisioning_enabled=true
|
||||||
|
|
||||||
- name: Create monitoring LXC on cloud-pc
|
- name: Create monitoring LXC on cloud-pc
|
||||||
hosts: localhost
|
hosts: localhost
|
||||||
connection: local
|
connection: local
|
||||||
@@ -7,6 +15,8 @@
|
|||||||
vars:
|
vars:
|
||||||
ansible_become: false
|
ansible_become: false
|
||||||
ansible_python_interpreter: "{{ ansible_playbook_python }}"
|
ansible_python_interpreter: "{{ ansible_playbook_python }}"
|
||||||
|
pve_monitoring_legacy_provisioning_enabled: >-
|
||||||
|
{{ homelab_services['monitoring'].provisioner != 'tofu' }}
|
||||||
pve_lxc_vmid: 146
|
pve_lxc_vmid: 146
|
||||||
pve_lxc_node: cloud-pc
|
pve_lxc_node: cloud-pc
|
||||||
pve_lxc_hostname: monitoring
|
pve_lxc_hostname: monitoring
|
||||||
@@ -20,12 +30,23 @@
|
|||||||
pve_lxc_features:
|
pve_lxc_features:
|
||||||
- nesting=1
|
- nesting=1
|
||||||
pve_lxc_ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
pve_lxc_ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst
|
||||||
|
pre_tasks:
|
||||||
|
- name: Skip legacy monitoring provisioning after cutover
|
||||||
|
ansible.builtin.meta: end_play
|
||||||
|
when: not (pve_monitoring_legacy_provisioning_enabled | bool)
|
||||||
roles:
|
roles:
|
||||||
- role: pve_lxc
|
- role: pve_lxc
|
||||||
|
|
||||||
- name: Enable Docker keyctl feature for monitoring LXC
|
- name: Enable Docker keyctl feature for monitoring LXC
|
||||||
hosts: cloud-pc
|
hosts: cloud-pc
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
|
vars:
|
||||||
|
pve_monitoring_legacy_provisioning_enabled: >-
|
||||||
|
{{ homelab_services['monitoring'].provisioner != 'tofu' }}
|
||||||
|
pre_tasks:
|
||||||
|
- name: Skip legacy monitoring provisioning after cutover
|
||||||
|
ansible.builtin.meta: end_play
|
||||||
|
when: not (pve_monitoring_legacy_provisioning_enabled | bool)
|
||||||
tasks:
|
tasks:
|
||||||
- name: Read monitoring LXC configuration
|
- name: Read monitoring LXC configuration
|
||||||
ansible.builtin.command: pct config 146
|
ansible.builtin.command: pct config 146
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
|
---
|
||||||
|
# После переезда на OpenTofu (tofu/svc-ovpn-mini.tf, VMID 160) обе play здесь —
|
||||||
|
# legacy: они таргетят СТАРЫЙ VMID 132, а их handlers сделали бы pct start 132,
|
||||||
|
# подняв остановленный откат на боевом адресе 192.168.1.23. Пропускаем, пока
|
||||||
|
# реестр говорит provisioner: tofu. Конфигурация OpenVPN-шлюза в этот плейбук
|
||||||
|
# никогда не входила — она в playbooks/openvpn-vps-mini.yml (роль
|
||||||
|
# openvpn_gateway, группа vpn_openvpn). Для намеренного legacy rollback:
|
||||||
|
# -e pve_ovpn_mini_legacy_provisioning_enabled=true
|
||||||
|
|
||||||
- name: Create ovpn-mini LXC on mini-pc
|
- name: Create ovpn-mini LXC on mini-pc
|
||||||
hosts: localhost
|
hosts: localhost
|
||||||
connection: local
|
connection: local
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
|
vars:
|
||||||
|
pve_ovpn_mini_legacy_provisioning_enabled: >-
|
||||||
|
{{ homelab_services['ovpn-mini'].provisioner != 'tofu' }}
|
||||||
|
pre_tasks:
|
||||||
|
- name: Skip legacy ovpn-mini provisioning after cutover
|
||||||
|
ansible.builtin.meta: end_play
|
||||||
|
when: not (pve_ovpn_mini_legacy_provisioning_enabled | bool)
|
||||||
roles:
|
roles:
|
||||||
- role: pve_lxc
|
- role: pve_lxc
|
||||||
vars:
|
vars:
|
||||||
@@ -18,10 +34,17 @@
|
|||||||
hosts: mini-pc
|
hosts: mini-pc
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
become: true
|
become: true
|
||||||
|
vars:
|
||||||
|
pve_ovpn_mini_legacy_provisioning_enabled: >-
|
||||||
|
{{ homelab_services['ovpn-mini'].provisioner != 'tofu' }}
|
||||||
handlers:
|
handlers:
|
||||||
- name: restart ovpn-mini lxc
|
- name: restart ovpn-mini lxc
|
||||||
ansible.builtin.shell: pct stop 132 || true; pct start 132
|
ansible.builtin.shell: pct stop 132 || true; pct start 132
|
||||||
changed_when: true
|
changed_when: true
|
||||||
|
pre_tasks:
|
||||||
|
- name: Skip legacy ovpn-mini provisioning after cutover
|
||||||
|
ansible.builtin.meta: end_play
|
||||||
|
when: not (pve_ovpn_mini_legacy_provisioning_enabled | bool)
|
||||||
tasks:
|
tasks:
|
||||||
- name: Allow /dev/net/tun device
|
- name: Allow /dev/net/tun device
|
||||||
ansible.builtin.lineinfile:
|
ansible.builtin.lineinfile:
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
- name: Remove storage-level PBS prune policy from pbs storage
|
||||||
|
hosts: mini-pc
|
||||||
|
gather_facts: false
|
||||||
|
tasks:
|
||||||
|
- name: Read current PBS storage configuration
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- pvesh
|
||||||
|
- get
|
||||||
|
- /storage/pbs
|
||||||
|
- --output-format
|
||||||
|
- json
|
||||||
|
register: pbs_storage_current_raw
|
||||||
|
changed_when: false
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Parse current PBS storage configuration
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
pbs_storage_current: "{{ pbs_storage_current_raw.stdout | from_json }}"
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Assert current PBS storage is safe to modify
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- pbs_storage_current.storage | default('') == 'pbs'
|
||||||
|
- pbs_storage_current.type | default('') == 'pbs'
|
||||||
|
- pbs_storage_current.digest | default('') | length > 0
|
||||||
|
fail_msg: Refusing to modify /storage/pbs because the returned storage metadata is unexpected or missing a digest.
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Remove storage-level prune policy from PBS storage
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- pvesh
|
||||||
|
- set
|
||||||
|
- /storage/pbs
|
||||||
|
- --delete
|
||||||
|
- prune-backups
|
||||||
|
- --digest
|
||||||
|
- "{{ pbs_storage_current.digest }}"
|
||||||
|
when: pbs_storage_current.get('prune-backups') is not none
|
||||||
|
changed_when: true
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Read back PBS storage configuration
|
||||||
|
ansible.builtin.command:
|
||||||
|
argv:
|
||||||
|
- pvesh
|
||||||
|
- get
|
||||||
|
- /storage/pbs
|
||||||
|
- --output-format
|
||||||
|
- json
|
||||||
|
register: pbs_storage_after_raw
|
||||||
|
changed_when: false
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Parse read-back PBS storage configuration
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
pbs_storage_after: "{{ pbs_storage_after_raw.stdout | from_json }}"
|
||||||
|
no_log: true
|
||||||
|
|
||||||
|
- name: Assert storage-level prune policy is absent
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- pbs_storage_after.storage | default('') == 'pbs'
|
||||||
|
- pbs_storage_after.type | default('') == 'pbs'
|
||||||
|
- pbs_storage_after.get('prune-backups') is none
|
||||||
|
fail_msg: Storage-level prune policy still exists on /storage/pbs after the declarative fix.
|
||||||
|
no_log: true
|
||||||
@@ -2,6 +2,10 @@
|
|||||||
- name: Guard Vaultwarden VMID before API updates
|
- name: Guard Vaultwarden VMID before API updates
|
||||||
hosts: mini-pc
|
hosts: mini-pc
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
|
pre_tasks:
|
||||||
|
- name: Skip legacy Vaultwarden provisioning for runtime-only calls
|
||||||
|
ansible.builtin.meta: end_play
|
||||||
|
when: not (pve_provisioning_enabled | default(true) | bool)
|
||||||
tasks:
|
tasks:
|
||||||
- name: Read existing VMID 140 configuration
|
- name: Read existing VMID 140 configuration
|
||||||
ansible.builtin.command: pct config 140
|
ansible.builtin.command: pct config 140
|
||||||
@@ -25,6 +29,10 @@
|
|||||||
- name: Create Vaultwarden LXC on mini-pc
|
- name: Create Vaultwarden LXC on mini-pc
|
||||||
hosts: mini-pc
|
hosts: mini-pc
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
|
pre_tasks:
|
||||||
|
- name: Skip legacy Vaultwarden provisioning for runtime-only calls
|
||||||
|
ansible.builtin.meta: end_play
|
||||||
|
when: not (pve_provisioning_enabled | default(true) | bool)
|
||||||
vars:
|
vars:
|
||||||
vaultwarden_vmid: 140
|
vaultwarden_vmid: 140
|
||||||
vaultwarden_hostname: vaultwarden
|
vaultwarden_hostname: vaultwarden
|
||||||
@@ -103,7 +111,16 @@
|
|||||||
ansible_become: false
|
ansible_become: false
|
||||||
|
|
||||||
- name: Configure Docker and Vaultwarden inside LXC
|
- name: Configure Docker and Vaultwarden inside LXC
|
||||||
hosts: vaultwarden
|
# Таргет переопределяем ради blue-green переезда на OpenTofu: во время
|
||||||
|
# миграции этот play нужно прогнать против нового контейнера на временном
|
||||||
|
# адресе. Просто `--limit vaultwarden-new` для этого НЕ годится — лимит
|
||||||
|
# пересекается с паттерном play и даёт ноль хостов, а не перенацеливание
|
||||||
|
# (проверено 2026-09-02 через --list-hosts на всех плейбуках).
|
||||||
|
# Использовать вместе с --limit, чтобы play создания контейнера отсеялся:
|
||||||
|
# ansible-playbook playbooks/pve-vaultwarden.yml \
|
||||||
|
# -e pve_config_target=vaultwarden-new --limit vaultwarden-new
|
||||||
|
# По умолчанию поведение не меняется.
|
||||||
|
hosts: "{{ pve_config_target | default('vaultwarden') }}"
|
||||||
gather_facts: true
|
gather_facts: true
|
||||||
vars:
|
vars:
|
||||||
ansible_become: false
|
ansible_become: false
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
---
|
||||||
|
# Базовое состояние публичной VPS и стек Caddy.
|
||||||
|
#
|
||||||
|
# ОБЛАСТЬ ОТВЕТСТВЕННОСТИ
|
||||||
|
# Этот плейбук ПРИНИМАЕТ существующий ru-vps под управление Ansible, а не
|
||||||
|
# разворачивает его с нуля. Docker и compose-плагин уже установлены вручную
|
||||||
|
# (Docker CE 29.x из upstream-репозитория, не Ubuntu'шный docker.io), поэтому
|
||||||
|
# плейбук их проверяет, но не ставит: `apt install docker.io` поверх Docker CE
|
||||||
|
# на боевой VPS сломал бы все стеки сразу.
|
||||||
|
#
|
||||||
|
# Управляется здесь ТОЛЬКО стек Caddy. Остальные каталоги в
|
||||||
|
# /opt/services/ru-vps (gitea-runner, mihomo, resticprofile, squid, zerotier)
|
||||||
|
# развёрнуты вручную и осознанно оставлены вне управления — решение по каждому
|
||||||
|
# принимается отдельно, см. docs/ai/plan.md.
|
||||||
|
#
|
||||||
|
# ГРАНИЦА С reverse-proxy.yml
|
||||||
|
# Содержимое Caddyfile принадлежит playbooks/reverse-proxy.yml. Здесь файл
|
||||||
|
# только проверяется на наличие: стартовать Caddy без конфига бессмысленно,
|
||||||
|
# а перезаписать его отсюда — значит потерять все маршруты.
|
||||||
|
#
|
||||||
|
# ЧТО МЕНЯЕТСЯ НА ЖИВОМ ХОСТЕ ПРИ ПЕРВОМ ПРОГОНЕ
|
||||||
|
# 1. Образ caddy:2-alpine закрепляется по digest -> контейнер пересоздаётся.
|
||||||
|
# 2. Появляется systemd-юнит homelab-caddy.service: до сих пор жизненным
|
||||||
|
# циклом стека управляла только политика restart=unless-stopped, то есть
|
||||||
|
# после `docker compose down` его никто не поднимал.
|
||||||
|
# Это кратковременный перерыв в публичном HTTPS. Прогонять осознанно.
|
||||||
|
#
|
||||||
|
# ЗАМЕЧАНИЕ ПРО --check
|
||||||
|
# До первого реального прогона `--check` заканчивается ошибкой
|
||||||
|
# "Could not find the requested service homelab-caddy": в check-режиме юнит
|
||||||
|
# на диск не пишется, поэтому systemd его не видит. Это артефакт проверки,
|
||||||
|
# а не дефект плейбука. Diff'ы задач выше при этом достоверны.
|
||||||
|
|
||||||
|
- name: Adopt the ru-vps Caddy stack into Ansible
|
||||||
|
hosts: ru-vps
|
||||||
|
gather_facts: true
|
||||||
|
|
||||||
|
# roles: выполняются РАНЬШЕ tasks:, поэтому все проверки идут в pre_tasks —
|
||||||
|
# иначе стек разложился бы до проверки Caddyfile.
|
||||||
|
pre_tasks:
|
||||||
|
- name: Verify the container runtime is present
|
||||||
|
vars:
|
||||||
|
ru_vps_docker_binary: /usr/bin/docker
|
||||||
|
block:
|
||||||
|
- name: Check the Docker binary
|
||||||
|
ansible.builtin.command: "{{ ru_vps_docker_binary }} version --format '{{ '{{' }}.Server.Version{{ '}}' }}'"
|
||||||
|
register: ru_vps_docker_version
|
||||||
|
changed_when: false
|
||||||
|
# Read-only проверка, поэтому выполняется и в --check.
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
|
- name: Check the Compose plugin
|
||||||
|
ansible.builtin.command: "{{ ru_vps_docker_binary }} compose version --short"
|
||||||
|
register: ru_vps_compose_version
|
||||||
|
changed_when: false
|
||||||
|
# Read-only проверка, поэтому выполняется и в --check.
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
|
- name: Report the detected runtime
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg: >-
|
||||||
|
docker={{ ru_vps_docker_version.stdout | trim }}
|
||||||
|
compose={{ ru_vps_compose_version.stdout | trim }}
|
||||||
|
|
||||||
|
# --- Арбитр кластера ---------------------------------------------------
|
||||||
|
# corosync-qnetd на ru-vps даёт двухнодовому кластеру третий голос.
|
||||||
|
# Ноды подключаются к нему по публичному адресу (см. corosync.conf,
|
||||||
|
# host: 157.22.231.198), а старое правило пускало 5403 только из
|
||||||
|
# 10.122.62.0/24 — сети ZeroTier, выведенной в июле. После этого qdevice
|
||||||
|
# молча перестал голосовать.
|
||||||
|
- name: Allow corosync-qnetd from the PVE nodes
|
||||||
|
community.general.ufw:
|
||||||
|
rule: allow
|
||||||
|
port: "5403"
|
||||||
|
proto: tcp
|
||||||
|
src: "{{ homelab_pve_egress_ip }}"
|
||||||
|
comment: corosync-qnetd for the HomeLab PVE cluster
|
||||||
|
|
||||||
|
- name: Drop the qnetd rule for the retired ZeroTier network
|
||||||
|
community.general.ufw:
|
||||||
|
rule: allow
|
||||||
|
port: "5403"
|
||||||
|
proto: tcp
|
||||||
|
src: 10.122.62.0/24
|
||||||
|
delete: true
|
||||||
|
|
||||||
|
# Caddyfile принадлежит reverse-proxy.yml. Без него стек стартует пустым,
|
||||||
|
# поэтому сначала проверяем наличие и только потом раскладываем стек.
|
||||||
|
- name: Check the Caddyfile managed by reverse-proxy.yml
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ homelab_reverse_proxy_caddyfile }}"
|
||||||
|
register: ru_vps_caddyfile
|
||||||
|
|
||||||
|
- name: Require the Caddyfile before touching the stack
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- ru_vps_caddyfile.stat.exists
|
||||||
|
- ru_vps_caddyfile.stat.size > 0
|
||||||
|
fail_msg: >-
|
||||||
|
{{ homelab_reverse_proxy_caddyfile }} отсутствует или пуст.
|
||||||
|
Сначала прогони playbooks/reverse-proxy.yml, иначе Caddy стартует
|
||||||
|
без маршрутов и публичные сервисы лягут.
|
||||||
|
|
||||||
|
roles:
|
||||||
|
- role: compose_service
|
||||||
|
compose_service_name: "{{ homelab_reverse_proxy_unit }}"
|
||||||
|
compose_service_description: Caddy reverse proxy for public HomeLab services
|
||||||
|
compose_service_root: "{{ homelab_reverse_proxy_dir }}"
|
||||||
|
# Каталог создан вручную с 0755 — сохраняем, чтобы прогон не давал
|
||||||
|
# косметического diff'а на боевом хосте.
|
||||||
|
compose_service_root_mode: "0755"
|
||||||
|
# Имя файла сохраняем: имя compose-проекта выводится из каталога, а сам
|
||||||
|
# файл должен остаться тем же, иначе стек станет orphan и поднимется
|
||||||
|
# второй контейнер на тех же 80/443.
|
||||||
|
compose_service_compose_file: docker-compose.yml
|
||||||
|
# /opt/data/caddy и /opt/configs/caddy принадлежат ada:ada и содержат
|
||||||
|
# выпущенные сертификаты. Роль их владельца не трогает намеренно.
|
||||||
|
compose_service_directories: []
|
||||||
|
compose_service_compose_content: |
|
||||||
|
services:
|
||||||
|
caddy:
|
||||||
|
image: {{ homelab_reverse_proxy_image | trim }}
|
||||||
|
container_name: {{ homelab_reverse_proxy_container }}
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
- "443:443/udp"
|
||||||
|
volumes:
|
||||||
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||||
|
- /opt/data/caddy:/data
|
||||||
|
- /opt/configs/caddy:/config
|
||||||
|
networks: {}
|
||||||
|
# Caddy на голый IP отвечает 308 (редирект http -> https), а не 200.
|
||||||
|
compose_service_health_url: http://127.0.0.1:80/
|
||||||
|
compose_service_health_status: [308]
|
||||||
|
compose_service_health_follow_redirects: none
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
---
|
||||||
|
# Вывод ZeroTier с ru-vps и чистка оставшихся от него правил UFW.
|
||||||
|
#
|
||||||
|
# ПОЧЕМУ ЭТО БЕЗОПАСНО
|
||||||
|
# Проверено 2026-09-02: ZT-интерфейса на хосте нет, маршрутов через него нет,
|
||||||
|
# на ZT-адресах никто не слушает. Контейнер `zerotier` работает, но подключён
|
||||||
|
# в никуда (потому и unhealthy). `ssh-zt22.service` в состоянии failed: его
|
||||||
|
# sshd настроен на ListenAddress 10.122.62.65, которого больше не существует.
|
||||||
|
#
|
||||||
|
# Из Ansible ZeroTier выведен ещё в июле 2026 — на хосте остался хвост.
|
||||||
|
#
|
||||||
|
# ЧТО НЕ УДАЛЯЕТСЯ
|
||||||
|
# Каталог /opt/services/ru-vps/zerotier, данные /opt/data/zerotier и файл
|
||||||
|
# /etc/ssh/sshd_config_zt22 остаются на месте. `docker compose down` идёт БЕЗ
|
||||||
|
# -v, то есть identity узла ZeroTier сохраняется. Удаление этих файлов —
|
||||||
|
# отдельный осознанный шаг оператора после того, как всё устоялось.
|
||||||
|
#
|
||||||
|
# make zerotier-decommission CONFIRM=1
|
||||||
|
|
||||||
|
- name: Decommission ZeroTier on ru-vps
|
||||||
|
hosts: ru-vps
|
||||||
|
gather_facts: false
|
||||||
|
|
||||||
|
vars:
|
||||||
|
zt_dir: /opt/services/ru-vps/zerotier
|
||||||
|
zt_interface: zt6q3dmi2d
|
||||||
|
zt_legacy_cidr: 10.122.62.0/24
|
||||||
|
# Правила UFW для сервисов, которые на хосте ничего не слушают (проверено
|
||||||
|
# через ss): squid не запущен, danted слушает 1081 а не 1080, на 993 и 7892
|
||||||
|
# слушателей нет. Это НЕ наследие ZeroTier, поэтому по умолчанию не трогаем.
|
||||||
|
zt_cleanup_unrelated_stale_rules: false
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
# --- Предполётные проверки ---------------------------------------------
|
||||||
|
- name: List the network interfaces
|
||||||
|
ansible.builtin.command: ip -br addr show
|
||||||
|
register: zt_ifaces
|
||||||
|
changed_when: false
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
|
- name: Read the current firewall rules
|
||||||
|
ansible.builtin.command: ufw status
|
||||||
|
register: zt_ufw
|
||||||
|
changed_when: false
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
|
- name: Refuse to run while a ZeroTier interface is up
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- zt_interface not in zt_ifaces.stdout
|
||||||
|
fail_msg: >-
|
||||||
|
Интерфейс {{ zt_interface }} присутствует на хосте — значит ZeroTier
|
||||||
|
снова используется. Плейбук рассчитан на вывод мёртвого стека и
|
||||||
|
отказывается работать: сначала подтверди, что сеть больше не нужна.
|
||||||
|
|
||||||
|
# Управляющая сессия идёт по 3422/tcp. Если глобального разрешения на этот
|
||||||
|
# порт нет, чистка правил может оборвать доступ — лучше не начинать.
|
||||||
|
- name: Require the management SSH rule to survive the cleanup
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- "'3422/tcp' in zt_ufw.stdout"
|
||||||
|
fail_msg: >-
|
||||||
|
В UFW нет правила для 3422/tcp. Прерываюсь, чтобы не остаться без
|
||||||
|
управляющего доступа.
|
||||||
|
|
||||||
|
# --- Контейнер ----------------------------------------------------------
|
||||||
|
- name: Check whether the ZeroTier stack is present
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ zt_dir }}/docker-compose.yml"
|
||||||
|
register: zt_compose
|
||||||
|
|
||||||
|
- name: Stop and remove the ZeroTier stack
|
||||||
|
ansible.builtin.command:
|
||||||
|
cmd: /usr/bin/docker compose -f {{ zt_dir }}/docker-compose.yml down
|
||||||
|
chdir: "{{ zt_dir }}"
|
||||||
|
register: zt_down
|
||||||
|
when: zt_compose.stat.exists
|
||||||
|
changed_when: "'Removing' in zt_down.stderr or 'Stopping' in zt_down.stderr"
|
||||||
|
|
||||||
|
# --- sshd на ZeroTier ---------------------------------------------------
|
||||||
|
- name: Disable the ZeroTier-only sshd unit
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: ssh-zt22.service
|
||||||
|
state: stopped
|
||||||
|
enabled: false
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
# `stop` не снимает состояние failed, и юнит навсегда остался бы в секции
|
||||||
|
# FAILED SYSTEMD UNITS отчёта `make status`. homelab-pve-routes.service —
|
||||||
|
# фантом той же эпохи: файла юнита уже нет, а запись о падении осталась.
|
||||||
|
- name: List the units currently in a failed state
|
||||||
|
ansible.builtin.command: systemctl list-units --state=failed --no-legend --plain --no-pager
|
||||||
|
register: zt_failed
|
||||||
|
changed_when: false
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
|
- name: Clear the leftover failed state of the ZeroTier-era units
|
||||||
|
ansible.builtin.command: "systemctl reset-failed {{ item }}"
|
||||||
|
loop:
|
||||||
|
- ssh-zt22.service
|
||||||
|
- homelab-pve-routes.service
|
||||||
|
# Без этого условия reset-failed рапортует changed на каждом прогоне:
|
||||||
|
# его код возврата нулевой и когда сбрасывать нечего.
|
||||||
|
when: item in zt_failed.stdout
|
||||||
|
register: zt_reset
|
||||||
|
changed_when: zt_reset.rc == 0
|
||||||
|
failed_when: false
|
||||||
|
|
||||||
|
# --- UFW ----------------------------------------------------------------
|
||||||
|
- name: Remove the ZeroTier interface rules
|
||||||
|
community.general.ufw:
|
||||||
|
rule: allow
|
||||||
|
direction: "{{ item }}"
|
||||||
|
interface: "{{ zt_interface }}"
|
||||||
|
delete: true
|
||||||
|
loop: [in, out]
|
||||||
|
|
||||||
|
- name: Remove the ZeroTier port rules
|
||||||
|
community.general.ufw:
|
||||||
|
rule: "{{ item.rule }}"
|
||||||
|
port: "{{ item.port }}"
|
||||||
|
proto: "{{ item.proto | default('any') }}"
|
||||||
|
interface: "{{ item.interface | default(omit) }}"
|
||||||
|
direction: "{{ 'in' if item.interface is defined else omit }}"
|
||||||
|
delete: true
|
||||||
|
loop:
|
||||||
|
- {rule: allow, port: "9993", proto: udp} # ZeroTier UDP
|
||||||
|
- {rule: deny, port: "9001"} # ZeroTier controller
|
||||||
|
- {rule: allow, port: "9001", interface: "zt+"} # он же, но с ZT-сетей
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.rule }} {{ item.port }}{{ '/' ~ item.proto if item.proto is defined else '' }}"
|
||||||
|
|
||||||
|
- name: Remove rules scoped to the retired ZeroTier subnet
|
||||||
|
community.general.ufw:
|
||||||
|
rule: allow
|
||||||
|
port: "{{ item }}"
|
||||||
|
proto: tcp
|
||||||
|
src: "{{ zt_legacy_cidr }}"
|
||||||
|
delete: true
|
||||||
|
loop: ["22", "3422"]
|
||||||
|
|
||||||
|
- name: Remove stale rules for services that no longer listen
|
||||||
|
community.general.ufw:
|
||||||
|
rule: allow
|
||||||
|
port: "{{ item.port }}"
|
||||||
|
proto: tcp
|
||||||
|
delete: true
|
||||||
|
loop:
|
||||||
|
- {port: "3128"} # squid: стек есть, контейнер не запущен
|
||||||
|
- {port: "1080"} # danted слушает 1081, не 1080
|
||||||
|
- {port: "993"} # слушателей нет
|
||||||
|
- {port: "7892"} # слушателей нет
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.port }}/tcp"
|
||||||
|
when: zt_cleanup_unrelated_stale_rules | bool
|
||||||
|
|
||||||
|
# --- Проверка -----------------------------------------------------------
|
||||||
|
- name: List the running containers after the cleanup
|
||||||
|
ansible.builtin.command: docker ps --format {{ '{{' }}.Names{{ '}}' }}
|
||||||
|
register: zt_after_ps
|
||||||
|
changed_when: false
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
|
- name: Read the firewall after the cleanup
|
||||||
|
ansible.builtin.command: ufw status
|
||||||
|
register: zt_after_ufw
|
||||||
|
changed_when: false
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
|
- name: Confirm ZeroTier is gone and management access survived
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- "'zerotier' not in zt_after_ps.stdout_lines"
|
||||||
|
- zt_interface not in zt_after_ufw.stdout
|
||||||
|
- "'3422/tcp' in zt_after_ufw.stdout"
|
||||||
|
success_msg: ZeroTier выведен, правило для 3422/tcp на месте.
|
||||||
|
# В --check контейнер не останавливается и правила не удаляются, поэтому
|
||||||
|
# проверка результата заведомо не прошла бы. Diff'ы задач выше достоверны.
|
||||||
|
when: not ansible_check_mode
|
||||||
|
|
||||||
|
- name: Show the resulting firewall
|
||||||
|
ansible.builtin.debug:
|
||||||
|
var: zt_after_ufw.stdout_lines
|
||||||
@@ -26,7 +26,6 @@
|
|||||||
# grimmory{,-docker-firewall} playbooks/pve-grimmory.yml
|
# grimmory{,-docker-firewall} playbooks/pve-grimmory.yml
|
||||||
# gyro.timer roles/gyro (gyro.service is oneshot -> see jobs)
|
# gyro.timer roles/gyro (gyro.service is oneshot -> see jobs)
|
||||||
# hermes-ai-tun-proxy.service playbooks/pve-hermes-ai.yml
|
# hermes-ai-tun-proxy.service playbooks/pve-hermes-ai.yml
|
||||||
# memoir-bot.service playbooks/pve-memoir-bot.yml
|
|
||||||
# mihomo{,-ui}.service playbooks/pve-mihomo.yml
|
# mihomo{,-ui}.service playbooks/pve-mihomo.yml
|
||||||
# uptime-kuma.service roles/uptime_kuma
|
# uptime-kuma.service roles/uptime_kuma
|
||||||
# vaultwarden.service playbooks/pve-vaultwarden.yml
|
# vaultwarden.service playbooks/pve-vaultwarden.yml
|
||||||
@@ -39,7 +38,6 @@
|
|||||||
grimmory: [docker.service, grimmory.service, grimmory-docker-firewall.service]
|
grimmory: [docker.service, grimmory.service, grimmory-docker-firewall.service]
|
||||||
gyro: [gyro.timer]
|
gyro: [gyro.timer]
|
||||||
hermes-ai: [docker.service, hermes-ai-tun-proxy.service]
|
hermes-ai: [docker.service, hermes-ai-tun-proxy.service]
|
||||||
memoir-bot: [docker.service, memoir-bot.service]
|
|
||||||
mihomo: [docker.service, mihomo.service, mihomo-ui.service]
|
mihomo: [docker.service, mihomo.service, mihomo-ui.service]
|
||||||
monitoring: [docker.service, uptime-kuma.service]
|
monitoring: [docker.service, uptime-kuma.service]
|
||||||
ru-vps: [docker.service]
|
ru-vps: [docker.service]
|
||||||
@@ -51,8 +49,9 @@
|
|||||||
# Yandex Disk over the network, so we only read what systemd already knows.
|
# Yandex Disk over the network, so we only read what systemd already knows.
|
||||||
status_job_units:
|
status_job_units:
|
||||||
cloud-pc:
|
cloud-pc:
|
||||||
- homelab-restic-offsite-gitea.service
|
|
||||||
- homelab-backup-audit-gitea.service
|
- homelab-backup-audit-gitea.service
|
||||||
|
gitea:
|
||||||
|
- homelab-restic-offsite-gitea.service
|
||||||
grimmory:
|
grimmory:
|
||||||
- homelab-restic-offsite-grimmory.service
|
- homelab-restic-offsite-grimmory.service
|
||||||
- homelab-backup-audit-grimmory.service
|
- homelab-backup-audit-grimmory.service
|
||||||
@@ -136,6 +135,18 @@
|
|||||||
failed_when: false
|
failed_when: false
|
||||||
when: status_units | length > 0
|
when: status_units | length > 0
|
||||||
|
|
||||||
|
# Двухнодовый кластер держится на арбитре corosync-qnetd (ru-vps:5403).
|
||||||
|
# Когда арбитр молчит, Total votes < Expected votes и падение ЛЮБОЙ ноды
|
||||||
|
# оставляет выжившую без кворума. Один раз это уже сломалось молча —
|
||||||
|
# после вывода ZeroTier правило UFW для 5403 осталось на мёртвой сети.
|
||||||
|
- name: Read cluster quorum state
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: LC_ALL=C pvecm status 2>/dev/null | grep -E 'Quorate:|Expected votes|Total votes' | tr -s ' ' | tr '\n' ' '
|
||||||
|
register: status_quorum
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
when: inventory_hostname in (groups['pve_nodes'] | default([]))
|
||||||
|
|
||||||
- name: List failed systemd units
|
- name: List failed systemd units
|
||||||
ansible.builtin.shell:
|
ansible.builtin.shell:
|
||||||
cmd: >-
|
cmd: >-
|
||||||
@@ -232,6 +243,7 @@
|
|||||||
if inventory_hostname in (groups['vpn_openvpn'] | default([])) else '-' }}
|
if inventory_hostname in (groups['vpn_openvpn'] | default([])) else '-' }}
|
||||||
vpn_peer_ip: "{{ openvpn_peer_ip | default('-') }}"
|
vpn_peer_ip: "{{ openvpn_peer_ip | default('-') }}"
|
||||||
vpn_probes: "{{ status_vpn_probe.stdout_lines | default([]) }}"
|
vpn_probes: "{{ status_vpn_probe.stdout_lines | default([]) }}"
|
||||||
|
quorum: "{{ status_quorum.stdout | default('') | trim }}"
|
||||||
|
|
||||||
|
|
||||||
- name: Print HomeLab status summary
|
- name: Print HomeLab status summary
|
||||||
@@ -331,6 +343,19 @@
|
|||||||
loop: "{{ status_hosts }}"
|
loop: "{{ status_hosts }}"
|
||||||
when: hostvars[item].status_record.vpn_peer != '-'
|
when: hostvars[item].status_record.vpn_peer != '-'
|
||||||
|
|
||||||
|
- name: Add cluster quorum section header
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
status_report: >-
|
||||||
|
{{ status_report + ['', 'CLUSTER QUORUM (qdevice = третий голос)', '-' * 80] }}
|
||||||
|
|
||||||
|
- name: Add quorum line per PVE node
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
status_report: >-
|
||||||
|
{{ status_report + [' ' ~ item ~ ': '
|
||||||
|
~ (hostvars[item].status_record.quorum | default('') | trim | default('нет данных', true))] }}
|
||||||
|
loop: "{{ status_hosts }}"
|
||||||
|
when: hostvars[item].status_record.quorum | default('') | length > 0
|
||||||
|
|
||||||
- name: Add backup and job section header
|
- name: Add backup and job section header
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
status_report: >-
|
status_report: >-
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#jinja2: trim_blocks: True, lstrip_blocks: True
|
||||||
|
# {{ ansible_managed }}
|
||||||
|
# Статические админ-ссылки (не из реестра). Рендерится playbooks/dashboard.yml.
|
||||||
|
- Управление:
|
||||||
|
- Proxmox cloud-pc:
|
||||||
|
- abbr: PVE
|
||||||
|
href: https://192.168.1.5:8006
|
||||||
|
- Proxmox mini-pc:
|
||||||
|
- abbr: PVE
|
||||||
|
href: https://192.168.1.10:8006
|
||||||
|
- Proxmox Backup Server:
|
||||||
|
- abbr: PBS
|
||||||
|
href: https://192.168.1.20:8007
|
||||||
|
- Uptime Kuma:
|
||||||
|
- abbr: UK
|
||||||
|
href: http://{{ dash_bind }}:3001
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#jinja2: trim_blocks: True, lstrip_blocks: True
|
||||||
|
# {{ ansible_managed }}
|
||||||
|
# Рендерится playbooks/dashboard.yml (роль compose_service). Правь шаблон,
|
||||||
|
# а не файл на хосте: следующий прогон перезапишет.
|
||||||
|
services:
|
||||||
|
homepage:
|
||||||
|
image: {{ dash_image }}
|
||||||
|
container_name: homelab-homepage
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- ./homepage.env
|
||||||
|
environment:
|
||||||
|
HOMEPAGE_ALLOWED_HOSTS: "{{ dash_allowed_hosts | trim }}"
|
||||||
|
ports:
|
||||||
|
# Только LAN-адрес CT 155 -> контейнерный 3000. Наружу не публикуется.
|
||||||
|
- "{{ dash_bind }}:{{ dash_port }}:3000"
|
||||||
|
volumes:
|
||||||
|
- ./config:/app/config
|
||||||
|
# Docker-сокет намеренно НЕ монтируется: сервисы живут на других хостах,
|
||||||
|
# а лишний доступ к сокету на LAN-видимом контейнере не нужен.
|
||||||
|
networks: {}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#jinja2: trim_blocks: True, lstrip_blocks: True
|
||||||
|
# {{ ansible_managed }}
|
||||||
|
# Рендерится playbooks/dashboard.yml ИЗ реестра homelab_services. НЕ править
|
||||||
|
# на хосте: меняй сервисы в ansible/inventory/group_vars/all/services.yml,
|
||||||
|
# затем `make dashboard`.
|
||||||
|
#
|
||||||
|
# Правило ссылки на сервис:
|
||||||
|
# * есть proxy.domain -> https://<domain> (+ siteMonitor)
|
||||||
|
# * иначе первый веб-порт -> http(s)://<ip>:<port> (+ siteMonitor для http)
|
||||||
|
# приоритет имён портов: http > ui > setup > uptime-kuma > grafana >
|
||||||
|
# controller > pbs-api
|
||||||
|
# * ни того, ни другого -> группа Headless, только ICMP ping по ip
|
||||||
|
{% set port_priority = ['http', 'ui', 'setup', 'uptime-kuma', 'grafana', 'controller', 'pbs-api'] %}
|
||||||
|
{% if homelab_dashboard_proxmox_url | default('') | length > 0 %}
|
||||||
|
- Infrastructure:
|
||||||
|
- Proxmox кластер:
|
||||||
|
description: cloud-pc + mini-pc
|
||||||
|
widget:
|
||||||
|
type: proxmox
|
||||||
|
url: {{ homelab_dashboard_proxmox_url }}
|
||||||
|
username: "{{ '{{HOMEPAGE_VAR_PROXMOX_USER}}' }}"
|
||||||
|
password: "{{ '{{HOMEPAGE_VAR_PROXMOX_TOKEN}}' }}"
|
||||||
|
{% endif %}
|
||||||
|
{% for node in dashboard_nodes %}
|
||||||
|
- {{ node }}:
|
||||||
|
{% for name, svc in homelab_services | dictsort %}
|
||||||
|
{% if svc.node == node %}
|
||||||
|
{% set proxy = svc.proxy | default({}) %}
|
||||||
|
{% set ns = namespace(scheme='', port=0) %}
|
||||||
|
{% for pref in port_priority %}
|
||||||
|
{% for p in svc.ports | default([]) %}
|
||||||
|
{% if ns.port == 0 and p.name == pref %}
|
||||||
|
{% set ns.port = p.port %}
|
||||||
|
{% set ns.scheme = 'https' if pref == 'pbs-api' else 'http' %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if proxy.domain is defined %}
|
||||||
|
- {{ name }}:
|
||||||
|
href: https://{{ proxy.domain }}
|
||||||
|
description: {{ svc.role }}
|
||||||
|
siteMonitor: https://{{ proxy.domain }}
|
||||||
|
{% elif ns.port > 0 %}
|
||||||
|
- {{ name }}:
|
||||||
|
href: {{ ns.scheme }}://{{ svc.ip }}:{{ ns.port }}
|
||||||
|
description: {{ svc.role }}
|
||||||
|
{% if ns.scheme == 'http' %}
|
||||||
|
siteMonitor: http://{{ svc.ip }}:{{ ns.port }}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
- Headless:
|
||||||
|
{% for name, svc in homelab_services | dictsort %}
|
||||||
|
{% set proxy = svc.proxy | default({}) %}
|
||||||
|
{% set ns = namespace(web=false) %}
|
||||||
|
{% for pref in port_priority %}
|
||||||
|
{% for p in svc.ports | default([]) %}
|
||||||
|
{% if p.name == pref %}
|
||||||
|
{% set ns.web = true %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if proxy.domain is not defined and not ns.web %}
|
||||||
|
- {{ name }}:
|
||||||
|
description: {{ svc.role }}
|
||||||
|
ping: {{ svc.ip }}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#jinja2: trim_blocks: True, lstrip_blocks: True
|
||||||
|
# {{ ansible_managed }}
|
||||||
|
# Рендерится playbooks/dashboard.yml. Порядок и имена групп ОБЯЗАНЫ совпадать
|
||||||
|
# с группами в services.yaml — иначе Homepage покажет их в алфавитном порядке.
|
||||||
|
title: HomeLab
|
||||||
|
headerStyle: boxed
|
||||||
|
color: slate
|
||||||
|
layout:
|
||||||
|
{% if homelab_dashboard_proxmox_url | default('') | length > 0 %}
|
||||||
|
Infrastructure:
|
||||||
|
style: row
|
||||||
|
columns: 3
|
||||||
|
{% endif %}
|
||||||
|
{% for node in dashboard_nodes %}
|
||||||
|
{{ node }}:
|
||||||
|
style: row
|
||||||
|
columns: 4
|
||||||
|
{% endfor %}
|
||||||
|
Headless:
|
||||||
|
style: row
|
||||||
|
columns: 4
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#jinja2: trim_blocks: True, lstrip_blocks: True
|
||||||
|
# {{ ansible_managed }}
|
||||||
|
# Верхняя панель Homepage. Рендерится playbooks/dashboard.yml.
|
||||||
|
- datetime:
|
||||||
|
text_size: xl
|
||||||
|
format:
|
||||||
|
timeStyle: short
|
||||||
|
dateStyle: short
|
||||||
|
- resources:
|
||||||
|
label: monitoring CT
|
||||||
|
cpu: true
|
||||||
|
memory: true
|
||||||
|
disk: /
|
||||||
|
- uptimekuma:
|
||||||
|
# Требует опубликованной Status Page в Uptime Kuma с этим slug
|
||||||
|
# (мониторы и статус-страницы Kuma живут только в её UI).
|
||||||
|
url: http://{{ dash_bind }}:3001
|
||||||
|
slug: {{ homelab_dashboard_kuma_slug | default('homelab') }}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#jinja2: trim_blocks: True, lstrip_blocks: True
|
||||||
|
# {{ ansible_managed }}
|
||||||
|
# Секреты виджетов Homepage. mode 0600, в Git не попадает. Значения берутся
|
||||||
|
# из корневого .env через lookup('env', ...) в playbooks/dashboard.yml.
|
||||||
|
# Homepage подставляет их в widgets/services по имени {{ '{{HOMEPAGE_VAR_*}}' }}.
|
||||||
|
HOMEPAGE_VAR_PROXMOX_URL={{ homelab_dashboard_proxmox_url | default('') }}
|
||||||
|
{% if dash_pve_user | length > 0 and dash_pve_token_id | length > 0 %}
|
||||||
|
HOMEPAGE_VAR_PROXMOX_USER={{ dash_pve_user }}!{{ dash_pve_token_id }}
|
||||||
|
{% else %}
|
||||||
|
HOMEPAGE_VAR_PROXMOX_USER=
|
||||||
|
{% endif %}
|
||||||
|
HOMEPAGE_VAR_PROXMOX_TOKEN={{ dash_pve_token_secret | default('') }}
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
---
|
---
|
||||||
- name: Freeze Prometheus monitoring and configure Uptime Kuma
|
- name: Freeze Prometheus monitoring and configure Uptime Kuma
|
||||||
hosts: monitoring_server
|
# Таргет переопределяем ради blue-green переезда на OpenTofu: конфигурацию
|
||||||
|
# нужно прогнать против нового контейнера на временном адресе. `--limit`
|
||||||
|
# сам по себе не перенацеливает play, а обнуляет его (проверено 2026-09-02).
|
||||||
|
# Использовать вместе с --limit:
|
||||||
|
# ansible-playbook playbooks/uptime-kuma.yml \
|
||||||
|
# -e pve_config_target=monitoring-new --limit monitoring-new
|
||||||
|
# По умолчанию поведение не меняется.
|
||||||
|
hosts: "{{ pve_config_target | default('monitoring_server') }}"
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
roles:
|
roles:
|
||||||
- role: uptime_kuma
|
- role: uptime_kuma
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
---
|
||||||
|
# Дрейф между реестром homelab_services и фактическим состоянием Proxmox.
|
||||||
|
#
|
||||||
|
# ЗАЧЕМ
|
||||||
|
# Реестр — источник правды, но программно его потребляют лишь несколько мест.
|
||||||
|
# VMID, адрес и ресурсы легко разъезжаются с реальностью, и заметить это
|
||||||
|
# раньше было нечем: `make status` показывает состояние, но не сверяет его
|
||||||
|
# с задекларированным, и его exit code намеренно не является gate'ом.
|
||||||
|
#
|
||||||
|
# ОТЛИЧИЕ ОТ status.yml
|
||||||
|
# Этот плейбук — именно gate: при любом расхождении он завершается ошибкой.
|
||||||
|
# Годится для cron и CI. Строго read-only: только `pct config`.
|
||||||
|
#
|
||||||
|
# make validate
|
||||||
|
# make validate EXTRA="--limit cloud-pc"
|
||||||
|
|
||||||
|
- name: Read actual LXC configuration from the Proxmox nodes
|
||||||
|
hosts: pve_nodes
|
||||||
|
gather_facts: false
|
||||||
|
tasks:
|
||||||
|
- name: Read pct config for the services declared on this node
|
||||||
|
ansible.builtin.command: "pct config {{ item.value.vmid }}"
|
||||||
|
loop: >-
|
||||||
|
{{ homelab_services | dict2items
|
||||||
|
| selectattr('value.node', 'eq', inventory_hostname) | list }}
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.key }} ({{ item.value.vmid }})"
|
||||||
|
register: validate_pct
|
||||||
|
changed_when: false
|
||||||
|
failed_when: false
|
||||||
|
check_mode: false
|
||||||
|
|
||||||
|
# Сравниваются только однозначные поля. rootfs намеренно пропущен: в реестре
|
||||||
|
# он записан как "data:32", а pct отдаёт "data:vm-141-disk-0,size=32G" —
|
||||||
|
# это разные представления, и их сверка требует отдельного парсера.
|
||||||
|
- name: Collect drift for this node
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
validate_drift: "{{ validate_drift | default([]) + item_drift }}"
|
||||||
|
loop: "{{ validate_pct.results }}"
|
||||||
|
loop_control:
|
||||||
|
label: "{{ item.item.key }}"
|
||||||
|
vars:
|
||||||
|
svc: "{{ item.item.value }}"
|
||||||
|
sname: "{{ item.item.key }}"
|
||||||
|
gone: "{{ item.rc | default(1) != 0 }}"
|
||||||
|
out: "{{ item.stdout | default('') }}"
|
||||||
|
a_host: "{{ out | regex_search('(?m)^hostname: (\\S+)', '\\1') | default([''], true) | first }}"
|
||||||
|
a_cores: "{{ out | regex_search('(?m)^cores: (\\d+)', '\\1') | default([''], true) | first }}"
|
||||||
|
a_mem: "{{ out | regex_search('(?m)^memory: (\\d+)', '\\1') | default([''], true) | first }}"
|
||||||
|
a_swap: "{{ out | regex_search('(?m)^swap: (\\d+)', '\\1') | default([''], true) | first }}"
|
||||||
|
a_ip: "{{ out | regex_search('ip=([0-9.]+)', '\\1') | default([''], true) | first }}"
|
||||||
|
item_drift: >-
|
||||||
|
{{ ([sname ~ ': VMID ' ~ svc.vmid ~ ' отсутствует на узле ' ~ inventory_hostname] if gone else [])
|
||||||
|
+ ([sname ~ ': hostname=' ~ a_host ~ ', в реестре ' ~ svc.hostname]
|
||||||
|
if (not gone and a_host != svc.hostname) else [])
|
||||||
|
+ ([sname ~ ': ip=' ~ a_ip ~ ', в реестре ' ~ svc.ip]
|
||||||
|
if (not gone and a_ip != svc.ip) else [])
|
||||||
|
+ ([sname ~ ': cores=' ~ a_cores ~ ', в реестре ' ~ svc.lxc.cores]
|
||||||
|
if (not gone and svc.lxc.cores is not none and a_cores | string != svc.lxc.cores | string) else [])
|
||||||
|
+ ([sname ~ ': memory=' ~ a_mem ~ ', в реестре ' ~ svc.lxc.memory]
|
||||||
|
if (not gone and svc.lxc.memory is not none and a_mem | string != svc.lxc.memory | string) else [])
|
||||||
|
+ ([sname ~ ': swap=' ~ a_swap ~ ', в реестре ' ~ svc.lxc.swap]
|
||||||
|
if (not gone and svc.lxc.swap is not none and a_swap | string != svc.lxc.swap | string) else []) }}
|
||||||
|
|
||||||
|
- name: Report registry drift
|
||||||
|
hosts: pve_nodes
|
||||||
|
gather_facts: false
|
||||||
|
run_once: true
|
||||||
|
tasks:
|
||||||
|
- name: Fail when the registry disagrees with Proxmox
|
||||||
|
ansible.builtin.assert:
|
||||||
|
that:
|
||||||
|
- all_drift | length == 0
|
||||||
|
success_msg: >-
|
||||||
|
Реестр совпадает с Proxmox: проверено сервисов —
|
||||||
|
{{ homelab_services | dict2items
|
||||||
|
| selectattr('value.node', 'in', groups['pve_nodes']) | list | length }}.
|
||||||
|
fail_msg: >-
|
||||||
|
{{ ['Реестр разошёлся с Proxmox:']
|
||||||
|
+ (all_drift | map('regex_replace', '^', ' - ') | list) }}
|
||||||
|
vars:
|
||||||
|
all_drift: >-
|
||||||
|
{{ groups['pve_nodes']
|
||||||
|
| map('extract', hostvars, 'validate_drift')
|
||||||
|
| select('defined') | flatten }}
|
||||||
@@ -24,18 +24,22 @@
|
|||||||
- name: Create and verify Vaultwarden PBS backup before update
|
- name: Create and verify Vaultwarden PBS backup before update
|
||||||
hosts: mini-pc
|
hosts: mini-pc
|
||||||
gather_facts: false
|
gather_facts: false
|
||||||
|
vars:
|
||||||
|
vaultwarden_vmid: "{{ homelab_services['vaultwarden'].vmid }}"
|
||||||
tasks:
|
tasks:
|
||||||
- name: Read existing VMID 140 configuration
|
- name: Read existing Vaultwarden VMID configuration
|
||||||
ansible.builtin.command: pct config 140
|
ansible.builtin.command: "pct config {{ vaultwarden_vmid }}"
|
||||||
register: vaultwarden_existing_vmid
|
register: vaultwarden_existing_vmid
|
||||||
changed_when: false
|
changed_when: false
|
||||||
|
|
||||||
- name: Refuse to modify a foreign VMID 140
|
- name: Refuse to modify a foreign Vaultwarden VMID
|
||||||
ansible.builtin.assert:
|
ansible.builtin.assert:
|
||||||
that:
|
that:
|
||||||
- vaultwarden_existing_vmid.rc == 0
|
- vaultwarden_existing_vmid.rc == 0
|
||||||
- vaultwarden_existing_hostname == 'vaultwarden'
|
- vaultwarden_existing_hostname == 'vaultwarden'
|
||||||
fail_msg: VMID 140 already exists and is not the Vaultwarden container.
|
fail_msg: >-
|
||||||
|
VMID {{ vaultwarden_vmid }} from the service registry is not the
|
||||||
|
Vaultwarden container.
|
||||||
vars:
|
vars:
|
||||||
vaultwarden_existing_hostname: >-
|
vaultwarden_existing_hostname: >-
|
||||||
{{ vaultwarden_existing_vmid.stdout_lines
|
{{ vaultwarden_existing_vmid.stdout_lines
|
||||||
@@ -62,13 +66,11 @@
|
|||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
argv:
|
argv:
|
||||||
- vzdump
|
- vzdump
|
||||||
- "140"
|
- "{{ vaultwarden_vmid }}"
|
||||||
- --storage
|
- --storage
|
||||||
- pbs
|
- pbs
|
||||||
- --mode
|
- --mode
|
||||||
- snapshot
|
- snapshot
|
||||||
- --prune-backups
|
|
||||||
- keep-all=1
|
|
||||||
- --exclude-path
|
- --exclude-path
|
||||||
- /var/lib/docker/fuse-overlayfs/*/merged
|
- /var/lib/docker/fuse-overlayfs/*/merged
|
||||||
|
|
||||||
@@ -82,6 +84,8 @@
|
|||||||
changed_when: true
|
changed_when: true
|
||||||
|
|
||||||
- import_playbook: pve-vaultwarden.yml
|
- import_playbook: pve-vaultwarden.yml
|
||||||
|
vars:
|
||||||
|
pve_provisioning_enabled: false
|
||||||
|
|
||||||
- name: Verify Vaultwarden public endpoint after update
|
- name: Verify Vaultwarden public endpoint after update
|
||||||
hosts: ru-vps
|
hosts: ru-vps
|
||||||
|
|||||||
+14
-3
@@ -36,12 +36,23 @@ systemd-юнит `Type=oneshot` с `docker compose up -d --remove-orphans`,
|
|||||||
Подробности и пример плейбука Gitea на новых ролях:
|
Подробности и пример плейбука Gitea на новых ролях:
|
||||||
[`compose_service/README.md`](compose_service/README.md).
|
[`compose_service/README.md`](compose_service/README.md).
|
||||||
|
|
||||||
**Статус:** роли созданы и проверены синтаксически, но пока не подключены ни
|
**Статус:** `compose_service` подключён в `playbooks/ru-vps-base.yml` (стек
|
||||||
к одному живому сервису. Перевод `pve-*.yml` на них — отдельный этап.
|
Caddy на ru-vps) — это его первый и пока единственный потребитель.
|
||||||
|
`lxc_docker_host` не подключён нигде. Перевод `pve-*.yml` на обе роли —
|
||||||
|
отдельный этап.
|
||||||
|
|
||||||
## Источник данных
|
## Источник данных
|
||||||
|
|
||||||
Факты о сервисах (vmid, узел, адрес, порты, домен, образы с digest, ресурсы,
|
Факты о сервисах (vmid, узел, адрес, порты, домен, образы с digest, ресурсы,
|
||||||
бэкап, мониторинг, порядок автозапуска) собраны в реестре
|
бэкап, мониторинг, порядок автозапуска) собраны в реестре
|
||||||
`ansible/inventory/group_vars/all/services.yml` (`homelab_services`).
|
`ansible/inventory/group_vars/all/services.yml` (`homelab_services`).
|
||||||
Его уже потребляет `playbooks/reverse-proxy.yml`.
|
Программные потребители реестра:
|
||||||
|
|
||||||
|
- `playbooks/reverse-proxy.yml` — сборка Caddyfile;
|
||||||
|
- `playbooks/ru-vps-base.yml` — стек Caddy и его закреплённый образ;
|
||||||
|
- `playbooks/pve-backup-jobs.yml` — списки VMID заданий PBS (поле `backup.job`);
|
||||||
|
- `roles/backup_audit` — VMID для аудита (флаг `monitoring.backup_audit_vmid`);
|
||||||
|
- `playbooks/validate.yml` — сверка реестра с фактическим состоянием Proxmox.
|
||||||
|
|
||||||
|
Остальное (`pve-*.yml`, `status.yml`, monitoring, SSH config) по-прежнему
|
||||||
|
дублирует значения и должно меняться согласованно.
|
||||||
|
|||||||
@@ -3,29 +3,22 @@ backup_audit_log_file: /var/log/homelab-backup-audit.log
|
|||||||
backup_audit_timer_oncalendar: "*-*-* 06:00:00"
|
backup_audit_timer_oncalendar: "*-*-* 06:00:00"
|
||||||
backup_audit_timer_randomized_delay: 10m
|
backup_audit_timer_randomized_delay: 10m
|
||||||
|
|
||||||
backup_audit_pbs_vmids:
|
# Единый порог свежести для всех PBS-снапшотов. Вынесен отдельно, потому что
|
||||||
- vmid: 132
|
# сам список VMID теперь выводится из реестра и per-vmid значения в нём нет.
|
||||||
max_age_hours: 48
|
backup_audit_pbs_max_age_hours: 48
|
||||||
- vmid: 140
|
|
||||||
max_age_hours: 48
|
# VMID берутся из homelab_services по флагу monitoring.backup_audit_vmid, а не
|
||||||
- vmid: 141
|
# перечисляются вручную. Форма записи ({vmid, max_age_hours}) сохранена, чтобы
|
||||||
max_age_hours: 48
|
# вызывающий playbook при необходимости мог передать свой список с иными
|
||||||
- vmid: 142
|
# порогами — шаблон audit-pbs.sh.j2 читает именно эти два поля.
|
||||||
max_age_hours: 48
|
backup_audit_pbs_vmids: >-
|
||||||
- vmid: 143
|
{{ homelab_services.values()
|
||||||
max_age_hours: 48
|
| selectattr('monitoring.backup_audit_vmid', 'defined')
|
||||||
- vmid: 144
|
| selectattr('monitoring.backup_audit_vmid')
|
||||||
max_age_hours: 48
|
| map(attribute='vmid') | sort
|
||||||
- vmid: 145
|
| map('community.general.dict_kv', 'vmid')
|
||||||
max_age_hours: 48
|
| map('combine', {'max_age_hours': backup_audit_pbs_max_age_hours})
|
||||||
- vmid: 146
|
| list }}
|
||||||
max_age_hours: 48
|
|
||||||
- vmid: 147
|
|
||||||
max_age_hours: 48
|
|
||||||
- vmid: 149
|
|
||||||
max_age_hours: 48
|
|
||||||
- vmid: 150
|
|
||||||
max_age_hours: 48
|
|
||||||
|
|
||||||
backup_audit_restic_profiles: []
|
backup_audit_restic_profiles: []
|
||||||
backup_audit_metrics_dir: /var/lib/node_exporter/textfile_collector
|
backup_audit_metrics_dir: /var/lib/node_exporter/textfile_collector
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ Docker»: пакеты, проверка `/dev/fuse`, `storage-driver: fuse-over
|
|||||||
запуск демона и базовый UFW.
|
запуск демона и базовый UFW.
|
||||||
|
|
||||||
Роль вынесена из повторяющихся блоков `playbooks/pve-*.yml`
|
Роль вынесена из повторяющихся блоков `playbooks/pve-*.yml`
|
||||||
(gitea, vaultwarden, mihomo, adguard, memoir-bot, docker-test, grimmory,
|
(gitea, vaultwarden, mihomo, adguard, docker-test, grimmory, hermes-ai) —
|
||||||
hermes-ai) — суммарно около 350 строк копипасты.
|
суммарно около 350 строк копипасты.
|
||||||
|
|
||||||
## Что делает
|
## Что делает
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ lxc_docker_host_packages:
|
|||||||
# gitea: [sqlite3, rsync]
|
# gitea: [sqlite3, rsync]
|
||||||
# adguard: [dnsutils]
|
# adguard: [dnsutils]
|
||||||
# mihomo: [git]
|
# mihomo: [git]
|
||||||
# memoir-bot: [git, openssh-client, rsync]
|
|
||||||
# grimmory: [mariadb-client, openssl]
|
# grimmory: [mariadb-client, openssl]
|
||||||
lxc_docker_host_extra_packages: []
|
lxc_docker_host_extra_packages: []
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
- monitoring_pve_api_token_secret != 'replace-me'
|
- monitoring_pve_api_token_secret != 'replace-me'
|
||||||
- monitoring_grafana_admin_password | length > 0
|
- monitoring_grafana_admin_password | length > 0
|
||||||
- monitoring_grafana_admin_password != 'change-me'
|
- monitoring_grafana_admin_password != 'change-me'
|
||||||
fail_msg: Load monitoring secrets from ignored ansible/.env or Ansible Vault before running this playbook.
|
fail_msg: Load monitoring secrets from the ignored repository-root .env or Ansible Vault before running this playbook.
|
||||||
no_log: true
|
no_log: true
|
||||||
|
|
||||||
- name: Install monitoring runtime packages
|
- name: Install monitoring runtime packages
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ scrape_configs:
|
|||||||
- "192.168.1.23:9100"
|
- "192.168.1.23:9100"
|
||||||
- "192.168.1.24:9100"
|
- "192.168.1.24:9100"
|
||||||
- "192.168.1.25:9100"
|
- "192.168.1.25:9100"
|
||||||
- "192.168.1.26:9100"
|
|
||||||
- "192.168.1.27:9100"
|
- "192.168.1.27:9100"
|
||||||
- "192.168.1.28:9100"
|
- "192.168.1.28:9100"
|
||||||
- "192.168.1.30:9100"
|
- "192.168.1.30:9100"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
- lookup('env', 'PROXMOX_USER') | length > 0
|
- lookup('env', 'PROXMOX_USER') | length > 0
|
||||||
- lookup('env', 'PROXMOX_TOKEN_ID') | length > 0
|
- lookup('env', 'PROXMOX_TOKEN_ID') | length > 0
|
||||||
- lookup('env', 'PROXMOX_TOKEN_SECRET') | length > 0
|
- lookup('env', 'PROXMOX_TOKEN_SECRET') | length > 0
|
||||||
fail_msg: Load ansible/.env with PROXMOX_HOST, PROXMOX_USER, PROXMOX_TOKEN_ID and PROXMOX_TOKEN_SECRET.
|
fail_msg: Load the repository-root .env with PROXMOX_HOST, PROXMOX_USER, PROXMOX_TOKEN_ID and PROXMOX_TOKEN_SECRET.
|
||||||
|
|
||||||
- name: Create or update LXC container
|
- name: Create or update LXC container
|
||||||
community.proxmox.proxmox:
|
community.proxmox.proxmox:
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ uptime_kuma_lan_cidr: 192.168.1.0/24
|
|||||||
uptime_kuma_listen_address: "{{ expected_lan_ip }}"
|
uptime_kuma_listen_address: "{{ expected_lan_ip }}"
|
||||||
uptime_kuma_openvpn_gateway_ip: 192.168.1.23
|
uptime_kuma_openvpn_gateway_ip: 192.168.1.23
|
||||||
uptime_kuma_http_proxy: http://192.168.1.27:7890
|
uptime_kuma_http_proxy: http://192.168.1.27:7890
|
||||||
uptime_kuma_no_proxy: "localhost,127.0.0.1,::1,ada-dev.ru,.ada-dev.ru,192.168.1.5,192.168.1.10,192.168.1.20,192.168.1.23,192.168.1.24,192.168.1.25,192.168.1.26,192.168.1.27,192.168.1.28,192.168.1.29,192.168.1.30,192.168.1.31,192.168.1.32,192.168.1.34,192.168.1.100"
|
uptime_kuma_no_proxy: "localhost,127.0.0.1,::1,ada-dev.ru,.ada-dev.ru,192.168.1.5,192.168.1.10,192.168.1.20,192.168.1.23,192.168.1.24,192.168.1.25,192.168.1.27,192.168.1.28,192.168.1.29,192.168.1.30,192.168.1.31,192.168.1.32,192.168.1.34,192.168.1.100"
|
||||||
|
|||||||
@@ -137,11 +137,21 @@
|
|||||||
delay: 5
|
delay: 5
|
||||||
until: uptime_kuma_health.status == 200
|
until: uptime_kuma_health.status == 200
|
||||||
|
|
||||||
|
# На хосте, где замороженный стек никогда не разворачивался (например, новый
|
||||||
|
# контейнер blue-green переезда), юнита homelab-monitoring нет, и systemd-модуль
|
||||||
|
# упал бы с "Could not find the requested service". Смысл шага — "legacy-стек не
|
||||||
|
# должен работать", а на чистом хосте это уже так. Проверено 2026-09-02.
|
||||||
|
- name: Detect the legacy monitoring unit
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: /etc/systemd/system/homelab-monitoring.service
|
||||||
|
register: uptime_kuma_legacy_unit
|
||||||
|
|
||||||
- name: Freeze the legacy Prometheus monitoring stack after Uptime Kuma is healthy
|
- name: Freeze the legacy Prometheus monitoring stack after Uptime Kuma is healthy
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.systemd:
|
||||||
name: homelab-monitoring
|
name: homelab-monitoring
|
||||||
enabled: false
|
enabled: false
|
||||||
state: stopped
|
state: stopped
|
||||||
|
when: uptime_kuma_legacy_unit.stat.exists
|
||||||
|
|
||||||
- name: Remove legacy monitoring firewall rules
|
- name: Remove legacy monitoring firewall rules
|
||||||
community.general.ufw:
|
community.general.ufw:
|
||||||
|
|||||||
+17
-9
@@ -39,12 +39,21 @@ Host cloud-pc mini-pc 192.168.1.5 192.168.1.10
|
|||||||
IdentityFile ~/.ssh/id_ed25519_homelab_ansible
|
IdentityFile ~/.ssh/id_ed25519_homelab_ansible
|
||||||
ProxyJump ru-vps
|
ProxyJump ru-vps
|
||||||
|
|
||||||
# ── LXC, доступные напрямую по LAN (без ProxyJump) ──────────────────────
|
# ── LXC pbs и ovpn-mini ─────────────────────────────────────────────────
|
||||||
|
# pbs идёт через ProxyJump ru-vps (агрегированная строка ниже): вне LAN это
|
||||||
|
# его единственный путь, а `make check`/`status`/`backup-audit` иначе падали.
|
||||||
Host pbs 192.168.1.20
|
Host pbs 192.168.1.20
|
||||||
HostName 192.168.1.20
|
HostName 192.168.1.20
|
||||||
|
|
||||||
|
# ovpn-mini — ProxyJump none НАМЕРЕННО: путь через ru-vps шёл бы по туннелю,
|
||||||
|
# который сам ovpn-mini и терминирует. Пока туннель жив это работало, но при
|
||||||
|
# его реконфигурации (или переезде контейнера) транспорт закольцовывается на
|
||||||
|
# мёртвый туннель. Из LAN хост доступен напрямую; вне LAN управление —
|
||||||
|
# через консоль ноды (pct exec) или при заранее поднятом туннеле.
|
||||||
|
# Первая строка блока побеждает по правилу "первое значение опции".
|
||||||
Host ovpn-mini 192.168.1.23
|
Host ovpn-mini 192.168.1.23
|
||||||
HostName 192.168.1.23
|
HostName 192.168.1.23
|
||||||
|
ProxyJump none
|
||||||
|
|
||||||
# ── LXC за jump-хостом ──────────────────────────────────────────────────
|
# ── LXC за jump-хостом ──────────────────────────────────────────────────
|
||||||
Host vaultwarden 192.168.1.24
|
Host vaultwarden 192.168.1.24
|
||||||
@@ -53,9 +62,6 @@ Host vaultwarden 192.168.1.24
|
|||||||
Host gitea 192.168.1.25
|
Host gitea 192.168.1.25
|
||||||
HostName 192.168.1.25
|
HostName 192.168.1.25
|
||||||
|
|
||||||
Host memoir-bot 192.168.1.26
|
|
||||||
HostName 192.168.1.26
|
|
||||||
|
|
||||||
Host mihomo 192.168.1.27
|
Host mihomo 192.168.1.27
|
||||||
HostName 192.168.1.27
|
HostName 192.168.1.27
|
||||||
|
|
||||||
@@ -80,23 +86,25 @@ Host grimmory 192.168.1.34
|
|||||||
Host gyro 192.168.1.35
|
Host gyro 192.168.1.35
|
||||||
HostName 192.168.1.35
|
HostName 192.168.1.35
|
||||||
|
|
||||||
|
|
||||||
# ── Групповые опции. ssh_config не поддерживает перенос строк, поэтому
|
# ── Групповые опции. ssh_config не поддерживает перенос строк, поэтому
|
||||||
# списки паттернов длинные: имя хоста (для `ssh gitea`) + IP (Ansible
|
# списки паттернов длинные: имя хоста (для `ssh gitea`) + IP (Ansible
|
||||||
# подключается по ansible_host). ─────────────────────────────────────
|
# подключается по ansible_host). ─────────────────────────────────────
|
||||||
|
|
||||||
# ProxyJump только для LXC, до которых нет прямого доступа
|
# ProxyJump для всех LXC. ОСТОРОЖНО: путь до ovpn-mini идёт через туннель,
|
||||||
Host vaultwarden gitea memoir-bot mihomo adguard docker-test monitoring hermes-ai emergency-bot grimmory gyro 192.168.1.24 192.168.1.25 192.168.1.26 192.168.1.27 192.168.1.28 192.168.1.29 192.168.1.30 192.168.1.31 192.168.1.32 192.168.1.34 192.168.1.35
|
# который сам ovpn-mini и терминирует. Для чтения это безопасно, но
|
||||||
|
# playbooks/openvpn-vps-mini.yml вне LAN может оборвать себе транспорт на
|
||||||
|
# середине прогона — переконфигурацию OpenVPN выполняй из локальной сети.
|
||||||
|
Host pbs ovpn-mini vaultwarden gitea mihomo adguard docker-test monitoring hermes-ai emergency-bot grimmory gyro 192.168.1.20 192.168.1.23 192.168.1.24 192.168.1.25 192.168.1.27 192.168.1.28 192.168.1.29 192.168.1.30 192.168.1.31 192.168.1.32 192.168.1.34 192.168.1.35
|
||||||
ProxyJump ru-vps
|
ProxyJump ru-vps
|
||||||
|
|
||||||
# Все LXC — под root с ключом id_ed25519_homelab
|
# Все LXC — под root с ключом id_ed25519_homelab
|
||||||
Host pbs ovpn-mini vaultwarden gitea memoir-bot mihomo adguard docker-test monitoring hermes-ai emergency-bot grimmory gyro 192.168.1.20 192.168.1.23 192.168.1.24 192.168.1.25 192.168.1.26 192.168.1.27 192.168.1.28 192.168.1.29 192.168.1.30 192.168.1.31 192.168.1.32 192.168.1.34 192.168.1.35
|
Host pbs ovpn-mini vaultwarden gitea mihomo adguard docker-test monitoring hermes-ai emergency-bot grimmory gyro 192.168.1.20 192.168.1.23 192.168.1.24 192.168.1.25 192.168.1.27 192.168.1.28 192.168.1.29 192.168.1.30 192.168.1.31 192.168.1.32 192.168.1.34 192.168.1.35
|
||||||
User root
|
User root
|
||||||
IdentityFile ~/.ssh/id_ed25519_homelab
|
IdentityFile ~/.ssh/id_ed25519_homelab
|
||||||
|
|
||||||
# Общие опции для хостов HomeLab (намеренно не `Host *`, чтобы не влиять
|
# Общие опции для хостов HomeLab (намеренно не `Host *`, чтобы не влиять
|
||||||
# на остальные записи ~/.ssh/config пользователя)
|
# на остальные записи ~/.ssh/config пользователя)
|
||||||
Host ru-vps vps cloud-pc mini-pc pbs ovpn-mini vaultwarden gitea memoir-bot mihomo adguard docker-test monitoring hermes-ai emergency-bot grimmory gyro 157.22.231.198 192.168.1.5 192.168.1.10 192.168.1.20 192.168.1.23 192.168.1.24 192.168.1.25 192.168.1.26 192.168.1.27 192.168.1.28 192.168.1.29 192.168.1.30 192.168.1.31 192.168.1.32 192.168.1.34 192.168.1.35
|
Host ru-vps vps cloud-pc mini-pc pbs ovpn-mini vaultwarden gitea mihomo adguard docker-test monitoring hermes-ai emergency-bot grimmory gyro 157.22.231.198 192.168.1.5 192.168.1.10 192.168.1.20 192.168.1.23 192.168.1.24 192.168.1.25 192.168.1.27 192.168.1.28 192.168.1.29 192.168.1.30 192.168.1.31 192.168.1.32 192.168.1.34 192.168.1.35
|
||||||
IdentitiesOnly yes
|
IdentitiesOnly yes
|
||||||
StrictHostKeyChecking accept-new
|
StrictHostKeyChecking accept-new
|
||||||
ControlMaster auto
|
ControlMaster auto
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# AI Context
|
||||||
|
|
||||||
|
## Назначение
|
||||||
|
|
||||||
|
Этот набор документов фиксирует стабильный, подтвержденный репозиторием контекст
|
||||||
|
HomeLab. Он дополняет краткий рабочий контракт в [`AGENTS.md`](../../AGENTS.md) и
|
||||||
|
не заменяет канонические Ansible inventory/vars или операционные заметки Obsidian.
|
||||||
|
|
||||||
|
HomeLab управляется как Ansible-first control plane: Proxmox VE/LXC, сетевой
|
||||||
|
транспорт, сервисы, резервное копирование, обновления и мониторинг описываются в
|
||||||
|
`ansible/`. Прямые изменения на серверах допустимы только для read-only диагностики
|
||||||
|
или break-glass восстановления с последующим переносом желаемого состояния в Ansible.
|
||||||
|
|
||||||
|
## Возможности
|
||||||
|
|
||||||
|
- создание и настройка LXC через Proxmox API или `pct` по SSH;
|
||||||
|
- управление сервисами через Docker/systemd и Docker Compose/systemd;
|
||||||
|
- OpenVPN-транспорт и SSH ProxyJump через `ru-vps`;
|
||||||
|
- Caddy reverse proxy для публичных сервисов;
|
||||||
|
- PBS и offsite restic backups с аудитом свежести;
|
||||||
|
- управляемые обновления по схеме backup/audit -> update -> health check;
|
||||||
|
- активный Uptime Kuma и сохраненный, но замороженный Prometheus stack;
|
||||||
|
- локальный Grimmory MCP с ручной синхронизацией книг в Obsidian.
|
||||||
|
|
||||||
|
## Форма системы
|
||||||
|
|
||||||
|
- `ansible/Makefile` является основной ручной точкой входа.
|
||||||
|
- `ansible/inventory/hosts.yml` задает хосты, группы и индивидуальные адреса.
|
||||||
|
- `ansible/inventory/group_vars/all/services.yml` содержит сводный реестр сервисов,
|
||||||
|
но пока программно управляет только генерацией reverse proxy.
|
||||||
|
- `ansible/playbooks/` содержит операционные entry points, `ansible/roles/` - роли.
|
||||||
|
- `tools/grimmory-mcp/` является отдельным Node.js stdio MCP-процессом.
|
||||||
|
- Obsidian vault хранит решения, текущую эксплуатационную картину и журнал работ.
|
||||||
|
|
||||||
|
## Карта репозитория
|
||||||
|
|
||||||
|
| Путь | Назначение |
|
||||||
|
|---|---|
|
||||||
|
| `ansible/Makefile` | Проверки, deploy, update и защищенные операции |
|
||||||
|
| `ansible/inventory/` | Канонические хосты, группы, общие и host-specific vars |
|
||||||
|
| `ansible/playbooks/` | Операционные Ansible entry points |
|
||||||
|
| `ansible/roles/` | Переиспользуемые роли и сохраненный monitoring stack |
|
||||||
|
| `ansible/ssh_config` | SSH users, keys, ports и ProxyJump |
|
||||||
|
| `.opencode/agents/` | Read-only специализированные агенты HomeLab |
|
||||||
|
| `tools/grimmory-mcp/` | Grimmory API и Obsidian sync integration |
|
||||||
|
| `archive/2026-07-proxmox-migration/` | История до Ansible control plane, не active source |
|
||||||
|
|
||||||
|
## Ключевые ограничения
|
||||||
|
|
||||||
|
- Не хранить secrets в Git или Obsidian.
|
||||||
|
- Не считать `--check --diff` полной симуляцией Proxmox provisioning.
|
||||||
|
- Не запускать замороженный Prometheus stack без отдельного решения.
|
||||||
|
- Не считать generic roles `lxc_docker_host` и `compose_service` подключенными к
|
||||||
|
production: активные service playbooks пока остаются источником поведения.
|
||||||
|
- Не исправлять обнаруженный технический долг в рамках несвязанной задачи.
|
||||||
|
- Не редактировать archive, generated files, installed Galaxy collections или
|
||||||
|
`node_modules` как active implementation.
|
||||||
|
|
||||||
|
## Документы
|
||||||
|
|
||||||
|
- [`architecture.md`](architecture.md) - observed architecture и data/control flows.
|
||||||
|
- [`tech-stack.md`](tech-stack.md) - runtimes, dependencies и команды.
|
||||||
|
- [`edge-cases.md`](edge-cases.md) - failure modes, safety gaps и coverage.
|
||||||
|
- [`plan.md`](plan.md) - только подтвержденная активная работа.
|
||||||
|
- [`migration-tofu.md`](migration-tofu.md) - пошаговый план перехода provisioning
|
||||||
|
LXC на OpenTofu по схеме blue-green.
|
||||||
|
- [`legacy-warning.md`](legacy-warning.md) - границы legacy/frozen/prototype кода.
|
||||||
|
- [`links.md`](links.md) - официальные version-relevant references.
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
# Архитектура
|
||||||
|
|
||||||
|
## Контекст и точки входа
|
||||||
|
|
||||||
|
Репозиторий является control plane, а не приложением с единым runtime. Оператор
|
||||||
|
запускает цели `ansible/Makefile`; Make загружает локальные secrets, применяет safety
|
||||||
|
gates и вызывает playbooks. `ansible.cfg` выбирает inventory, roles и локально
|
||||||
|
установленные Galaxy collections.
|
||||||
|
|
||||||
|
Канонические источники:
|
||||||
|
|
||||||
|
- хосты и группы: `ansible/inventory/hosts.yml`;
|
||||||
|
- общий доступ и сеть: `ansible/inventory/group_vars/all/main.yml`;
|
||||||
|
- сводные сведения о сервисах: `ansible/inventory/group_vars/all/services.yml`;
|
||||||
|
- SSH transport: `ansible/ssh_config`;
|
||||||
|
- ручные операции: `ansible/Makefile`.
|
||||||
|
|
||||||
|
Программные потребители реестра:
|
||||||
|
|
||||||
|
- `playbooks/reverse-proxy.yml` — сборка Caddyfile;
|
||||||
|
- `playbooks/ru-vps-base.yml` — стек Caddy и его закреплённый образ;
|
||||||
|
- `playbooks/pve-backup-jobs.yml` — списки VMID заданий PBS (поле `backup.job`);
|
||||||
|
- `roles/backup_audit` — VMID для аудита (флаг `monitoring.backup_audit_vmid`);
|
||||||
|
- `playbooks/validate.yml` — сверка реестра с фактическим состоянием Proxmox;
|
||||||
|
- `playbooks/dashboard.yml` — `services.yaml` для Homepage-обзора инфраструктуры.
|
||||||
|
|
||||||
|
Остальное (`pve-*.yml`, `status.yml`, monitoring, SSH config) по-прежнему
|
||||||
|
дублирует значения и должно меняться согласованно.
|
||||||
|
|
||||||
|
## Топология
|
||||||
|
|
||||||
|
- `ru-vps`: public VPS, qdevice, Caddy, OpenVPN server и SSH JumpHost.
|
||||||
|
- `cloud-pc`, `mini-pc`: Proxmox VE nodes.
|
||||||
|
- `lxc_infra`: PBS, OpenVPN gateway и service LXC.
|
||||||
|
- `monitoring`: CT 146 на `cloud-pc`; Uptime Kuma активен, Prometheus stack заморожен.
|
||||||
|
|
||||||
|
Актуальные VMID, placement, IP, ports, domains и backup policy не копируются сюда:
|
||||||
|
их нужно читать из `homelab_services` и сверять с `hosts.yml`.
|
||||||
|
|
||||||
|
## Provisioning Flow
|
||||||
|
|
||||||
|
1. `make deploy-<service>` загружает ignored `.env` из корня репозитория.
|
||||||
|
2. `playbooks/pve-<service>.yml` создает или сверяет LXC.
|
||||||
|
3. Часть playbooks вызывает `roles/pve_lxc` через Proxmox API.
|
||||||
|
4. Остальные подключаются по SSH к PVE node и выполняют `pct create/set/start`.
|
||||||
|
5. Следующий play настраивает LXC, runtime, systemd и health checks.
|
||||||
|
|
||||||
|
Два provisioner-пути имеют разные check-mode и ownership guards. Нельзя переносить
|
||||||
|
service между ними как косметический рефакторинг.
|
||||||
|
|
||||||
|
## Service Runtime
|
||||||
|
|
||||||
|
- Большинство сервисов используют systemd units вокруг `docker run`.
|
||||||
|
- Grimmory использует Compose и отдельный `DOCKER-USER` firewall unit.
|
||||||
|
- Uptime Kuma и сохраненный monitoring stack используют legacy `docker-compose`.
|
||||||
|
- `lxc_docker_host` и `compose_service` описывают будущий общий паттерн, но ни один
|
||||||
|
production playbook их пока не вызывает.
|
||||||
|
|
||||||
|
## Network Flow
|
||||||
|
|
||||||
|
`ansible_ssh_common_args` подключает `ansible/ssh_config`. Обычный путь управления:
|
||||||
|
|
||||||
|
```text
|
||||||
|
controller -> ru-vps:3422 -> 192.168.1.x target
|
||||||
|
```
|
||||||
|
|
||||||
|
`pbs` и `ovpn-mini` являются direct-LAN исключениями. LXC обычно управляются как
|
||||||
|
`root`, а PVE nodes и `ru-vps` - как service account `ansible`.
|
||||||
|
|
||||||
|
OpenVPN site tunnel:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ru-vps 10.78.0.1:8443/tcp <-> ovpn-mini 10.78.0.2 -> 192.168.1.0/24
|
||||||
|
```
|
||||||
|
|
||||||
|
Публичный service flow:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Internet -> Caddy on ru-vps -> OpenVPN -> ovpn-mini -> LAN service
|
||||||
|
```
|
||||||
|
|
||||||
|
Потеря `ru-vps` или site tunnel одновременно влияет на public upstreams и обычный
|
||||||
|
SSH management path.
|
||||||
|
|
||||||
|
## Reverse Proxy
|
||||||
|
|
||||||
|
`playbooks/reverse-proxy.yml` выбирает записи `homelab_services` с блоком `proxy`,
|
||||||
|
валидирует metadata и Caddyfile, обновляет marked blocks и проверяет upstreams.
|
||||||
|
Ansible управляет Caddyfile, но не установкой и lifecycle контейнера Caddy на `ru-vps`.
|
||||||
|
Grimmory OPDS/KOReader routes имеют специальные headers и отключение compression;
|
||||||
|
их нельзя упрощать без device compatibility tests.
|
||||||
|
|
||||||
|
## Backup и Update
|
||||||
|
|
||||||
|
`playbooks/pve-backup-jobs.yml` задает cluster-level PBS schedules. Offsite restic
|
||||||
|
profiles выполняются systemd timers и используют application-aware SQLite backup или
|
||||||
|
MariaDB dump. `roles/backup_audit` проверяет freshness, `restic check` и выборочные
|
||||||
|
restores, после чего атомарно пишет textfile metrics.
|
||||||
|
|
||||||
|
Update playbooks выполняют:
|
||||||
|
|
||||||
|
```text
|
||||||
|
fresh backup/audit -> reapply service declaration -> health verification
|
||||||
|
```
|
||||||
|
|
||||||
|
Backup является prerequisite для ручного recovery, а не автоматическим rollback.
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
Uptime Kuma в CT 146 является активным monitoring UI. Его роль останавливает и
|
||||||
|
отключает `homelab-monitoring`, сохраняя конфигурацию и данные Prometheus stack.
|
||||||
|
`make monitoring` защищен `CONFIRM=1` и предназначен только для отдельно принятого
|
||||||
|
решения о восстановлении старого stack.
|
||||||
|
|
||||||
|
Exporter roles, groups и backup metrics остаются в репозитории. Hardcoded Prometheus
|
||||||
|
targets могут расходиться с inventory, пока stack заморожен.
|
||||||
|
|
||||||
|
`playbooks/dashboard.yml` (`make dashboard`) поднимает Homepage вторым
|
||||||
|
compose-стеком на том же LXC `monitoring`: обзорная стартовая страница со
|
||||||
|
списком всех сервисов, ссылками на их UI и виджетами Proxmox/Uptime Kuma.
|
||||||
|
`services.yaml`, `settings.yaml`, `widgets.yaml`, `bookmarks.yaml` рендерятся
|
||||||
|
из `homelab_services` шаблонами `playbooks/templates/homepage-*.j2` — отдельный
|
||||||
|
список сервисов не ведётся. Контейнер слушает только LAN-адрес CT 155
|
||||||
|
(`homelab_dashboard_bind_ip`), наружу через Caddy не публикуется. Виджет
|
||||||
|
Proxmox использует read-only токен `homepage@pve!dashboard` (роль `PVEAuditor`,
|
||||||
|
`make bootstrap-dashboard-token`), секрет — в корневом `.env` как
|
||||||
|
`DASHBOARD_PVE_*`. Образ Homepage закрепляется по digest как остальные active
|
||||||
|
images; до первого закрепления плейбук падает на assert'е (обход —
|
||||||
|
`-e dashboard_allow_floating_tag=true`).
|
||||||
|
|
||||||
|
## Grimmory MCP
|
||||||
|
|
||||||
|
`tools/grimmory-mcp/src/server.js` запускает локальный stdio MCP. Grimmory API
|
||||||
|
используется read-only; authentication POST не меняет library data. Явно вызванные
|
||||||
|
sync tools читают API, опционально загружают cover, атомарно обновляют managed sections
|
||||||
|
Obsidian notes и запускают внешний vault index script.
|
||||||
|
|
||||||
|
Sync не является общей транзакцией: full sync может закончиться после частичного
|
||||||
|
набора успешных book updates. Vault path и credential file защищены отдельными
|
||||||
|
проверками, описанными в [`edge-cases.md`](edge-cases.md).
|
||||||
|
|
||||||
|
## Testing Boundaries
|
||||||
|
|
||||||
|
- Ansible имеет static lint и syntax checks, но не имеет Molecule/idempotence harness.
|
||||||
|
- `check.yml` проверяет reachability и expected IP, а не service health.
|
||||||
|
- `status.yml` формирует наблюдательный отчет и не является failing health gate.
|
||||||
|
- Grimmory MCP имеет Node test suite.
|
||||||
|
- Gitea Actions workflow существует, но runner и Actions не активированы.
|
||||||
|
|
||||||
|
## Неизвестно
|
||||||
|
|
||||||
|
- Текущие runtime versions Proxmox VE, OpenVPN, Caddy и restic не закреплены repo manifests.
|
||||||
|
- UI-only состояние AdGuard, Uptime Kuma monitors и часть service credentials не может
|
||||||
|
быть установлена из репозитория.
|
||||||
|
- Наличие DHCP reservation для Grimmory и намеренность отсутствия backup CT 148
|
||||||
|
требуют проверки оператором.
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# Edge Cases и риски
|
||||||
|
|
||||||
|
## Proxmox Provisioning
|
||||||
|
|
||||||
|
- Handled: ряд service playbooks проверяет hostname существующего VMID перед
|
||||||
|
изменением контейнера.
|
||||||
|
- Gap: generic `roles/pve_lxc` не содержит общего foreign-VMID guard; часть callers
|
||||||
|
и direct `pct` playbooks также не выполняет ownership assertion.
|
||||||
|
- Gap: некоторые `pct start` tasks считают любой return code `255` допустимым, что
|
||||||
|
может скрыть ошибку, не связанную с already-running state.
|
||||||
|
- Unknown: `--check --diff` не моделирует Proxmox module calls и command-heavy
|
||||||
|
playbooks полностью. `make dry-*` является preview, а не isolated simulation.
|
||||||
|
|
||||||
|
## Validation Semantics
|
||||||
|
|
||||||
|
- `playbooks/check.yml` проверяет Ansible reachability и `expected_lan_ip`.
|
||||||
|
- `playbooks/status.yml` подавляет многие command errors и отображает DOWN/failed
|
||||||
|
состояния в отчете; успешный exit code не означает healthy infrastructure.
|
||||||
|
- `make lint` запускает ansible-lint и yamllint из корня репозитория и включает
|
||||||
|
all-playbook syntax loop, то есть воспроизводит CI.
|
||||||
|
- CI workflow не исполняется без включенных Gitea Actions и runner.
|
||||||
|
- Ansible roles не имеют Molecule, idempotence или integration test harness.
|
||||||
|
|
||||||
|
## Network Failure Domains
|
||||||
|
|
||||||
|
- Потеря `ru-vps` нарушает обычный ProxyJump к большинству hosts и public Caddy.
|
||||||
|
- Потеря OpenVPN site tunnel нарушает private upstream path public services.
|
||||||
|
- Потеря Mihomo одновременно затрагивает egress Hermes, внешние Uptime Kuma probes,
|
||||||
|
emergency Telegram и Gyro notifications.
|
||||||
|
- Normal SSH использует `StrictHostKeyChecking=accept-new`; emergency и Gyro paths
|
||||||
|
требуют заранее проверенных pinned host keys.
|
||||||
|
- Proxmox API TLS validation по шаблону `.env.example` отключен по умолчанию.
|
||||||
|
|
||||||
|
## Secrets и Privilege
|
||||||
|
|
||||||
|
- Handled: ignored `.env`, Vault, `0600`, `no_log` и external password files не
|
||||||
|
позволяют хранить ожидаемые secrets в tracked config.
|
||||||
|
- Constraint: никогда не переносить значения из ignored/archived `.env` в docs,
|
||||||
|
output или commits.
|
||||||
|
- Risk: `bootstrap-pve-api-token.yml` вращает privileged token и переписывает весь
|
||||||
|
корневой `.env` целиком, а не построчно. Теряются `MONITORING_*`, `EMERGENCY_*` и
|
||||||
|
`PROXMOX_ROOT_PASSWORD`. С 2026-09-02 у задачи `backup: true`, но восстанавливать
|
||||||
|
придётся вручную. Запускать только как отдельную осознанную операцию.
|
||||||
|
- Constraint: строки в `.env` пишутся с префиксом `export`, и это не стиль:
|
||||||
|
`bootstrap-monitoring-pve-token.yml` ищет их через `regexp: "^export NAME="`.
|
||||||
|
Убрать префикс — значит получить дубликаты строк вместо обновления.
|
||||||
|
- Risk: LXC управляются как root, shell hosts используют `ansible` с passwordless sudo.
|
||||||
|
|
||||||
|
## Backups и Updates
|
||||||
|
|
||||||
|
- Handled: SQLite backup API + integrity check, atomic MariaDB dump, per-profile
|
||||||
|
`flock`, PBS/restic freshness audit и selective restore.
|
||||||
|
- Gap: update failures не запускают automatic rollback; backup только обеспечивает
|
||||||
|
возможность ручного recovery.
|
||||||
|
- Gap: PBS audit проверяет freshness, но не выполняет restore или PBS data integrity
|
||||||
|
drill. Обычный `restic check` не читает все data packs.
|
||||||
|
- Gap: recurring Grimmory audit проверяет non-empty SQL dump, но не импортирует его
|
||||||
|
во временную MariaDB.
|
||||||
|
- Gap: CT 148 `emergency-bot` не имеет backup/monitoring в service registry.
|
||||||
|
- Constraint: code-only downgrade Grimmory после Flyway migration запрещен; нужен
|
||||||
|
previous image вместе с pre-upgrade database backup.
|
||||||
|
- Concurrency: нет repository-wide lock от одновременных operator/update runs;
|
||||||
|
preflight через `pgrep vzdump` имеет race до запуска нового backup.
|
||||||
|
- Handled: storage-level `prune-backups` on PVE storage `pbs` removed
|
||||||
|
declaratively; retention now runs centrally in PBS `prune-pbs`, so the PBS
|
||||||
|
side is the only authority for PBS-backed retention. The weekly PBS-container
|
||||||
|
backup on storage `backup` with `keep-last=2` remains an intentional
|
||||||
|
exception.
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
- Active Uptime Kuma и frozen Prometheus stack не должны запускаться как две
|
||||||
|
параллельные monitoring architectures без отдельного решения.
|
||||||
|
- Hardcoded Prometheus targets могут расходиться с inventory и service registry.
|
||||||
|
- Central monitoring на `cloud-pc` не может независимо сообщить о полном отказе
|
||||||
|
своего node без внешнего наблюдателя.
|
||||||
|
|
||||||
|
## Reverse Proxy и Firewall
|
||||||
|
|
||||||
|
- Handled: reverse proxy metadata, Caddyfile, container config и upstreams проходят
|
||||||
|
validation до/после restart.
|
||||||
|
- Constraint: Ansible не управляет lifecycle Caddy container на `ru-vps`.
|
||||||
|
- Constraint: не упрощать Grimmory OPDS/KOReader handlers и не удалять его
|
||||||
|
`DOCKER-USER` protection без эквивалентных compatibility/security checks.
|
||||||
|
- Constraint: не включать cluster-wide PVE firewall без аудита всех guests с
|
||||||
|
`firewall=1`.
|
||||||
|
|
||||||
|
## Grimmory MCP
|
||||||
|
|
||||||
|
- Handled и tested: pagination guards, optional 204/404 resources, concurrent auth
|
||||||
|
promises, credential ownership/mode/no-follow checks, vault path containment,
|
||||||
|
cover size/type validation, atomic note writes и preservation of user markers.
|
||||||
|
- Gap: full sync не транзакционен и может завершиться с частично обновленным vault.
|
||||||
|
- Gap: нет mutex для concurrent sync/index rebuild.
|
||||||
|
- Gap: поиск existing note для каждой книги повторно сканирует весь notes directory.
|
||||||
|
- External dependency: Python и vault-owned index script должны существовать и
|
||||||
|
завершиться в timeout; они не управляются этим репозиторием.
|
||||||
|
|
||||||
|
## Требующие проверки состояния
|
||||||
|
|
||||||
|
- Завершена ли initial UI configuration AdGuard.
|
||||||
|
- Создана ли DHCP reservation/exclusion для Grimmory `192.168.1.34`.
|
||||||
|
- Настроены ли Gyro Vault secrets; tracked configuration не подтверждает их наличие.
|
||||||
|
- Является ли отсутствие backup для CT 148 намеренным решением.
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# Legacy и fragile boundaries
|
||||||
|
|
||||||
|
Этот файл не является backlog. Он предотвращает случайную замену active behavior
|
||||||
|
более новым, старым или внешне похожим кодом без отдельного решения.
|
||||||
|
|
||||||
|
## Исторический Archive
|
||||||
|
|
||||||
|
- Path: `archive/2026-07-proxmox-migration/`.
|
||||||
|
- Evidence: каталог содержит прежние NixOS, Docker Compose, GitOps, ZeroTier и другие
|
||||||
|
pre-Ansible материалы.
|
||||||
|
- Constraint: не редактировать и не возвращать файлы из archive как active config.
|
||||||
|
- Decision: accepted historical reference; active implementation создается в `ansible/`.
|
||||||
|
|
||||||
|
ZeroTier удален из active infrastructure 11 июля 2026 года. Active inventory, roles
|
||||||
|
и playbooks его не содержат.
|
||||||
|
|
||||||
|
## Frozen Monitoring Stack
|
||||||
|
|
||||||
|
- Paths: `ansible/roles/monitoring_server/`, `monitoring_exporter/`,
|
||||||
|
`monitoring_blackbox/`, `ansible/playbooks/monitoring.yml`.
|
||||||
|
- Evidence: `ansible/Makefile` помечает target `monitoring` как frozen и требует
|
||||||
|
`CONFIRM=1`; role `uptime_kuma` останавливает `homelab-monitoring`.
|
||||||
|
- Constraint: наличие кода не означает, что Prometheus stack активен.
|
||||||
|
- Decision: defer; Uptime Kuma является active monitoring до нового решения.
|
||||||
|
|
||||||
|
## Prototype Roles
|
||||||
|
|
||||||
|
- Paths: `ansible/roles/lxc_docker_host/`, `ansible/roles/compose_service/`.
|
||||||
|
- Evidence: `ansible/roles/README.md`. `compose_service` с 2026-09-02 вызывается из
|
||||||
|
`playbooks/ru-vps-base.yml` (стек Caddy); `lxc_docker_host` по-прежнему не вызывается
|
||||||
|
ни одним playbook.
|
||||||
|
- Constraint: не считать direct `pve-*.yml` dead code и не мигрировать service как
|
||||||
|
opportunistic cleanup. Миграция меняет runtime, pull, firewall и recreation semantics.
|
||||||
|
- Decision: defer; выполнять отдельно по одному service с backup и health validation.
|
||||||
|
|
||||||
|
Empty `ansible/roles/base`, `docker` и `ufw` являются остатками ранней структуры, а
|
||||||
|
не active reusable roles.
|
||||||
|
|
||||||
|
## Duplicated Service Facts
|
||||||
|
|
||||||
|
- Paths: service registry, `pve-*.yml`, `pve-backup-jobs.yml`, backup audit defaults,
|
||||||
|
monitoring templates, status playbook и SSH config.
|
||||||
|
- Evidence: реестр потребляют reverse proxy, `ru-vps-base.yml`, backup jobs, backup
|
||||||
|
audit и `validate.yml`; `pve-*.yml`, `status.yml`, monitoring и SSH config всё ещё
|
||||||
|
дублируют значения.
|
||||||
|
- Constraint: изменение VMID/IP/image/backup/monitoring требует сверки оставшихся
|
||||||
|
consumers. `make validate` ловит расхождение реестра с Proxmox по hostname, IP,
|
||||||
|
cores, memory и swap, но не по образам, бэкапам и SSH config.
|
||||||
|
- Decision: accepted risk до отдельной migration/validator задачи.
|
||||||
|
|
||||||
|
## Partial Ownership
|
||||||
|
|
||||||
|
- Caddy installation/container lifecycle на `ru-vps` и provisioning PBS CT 120 не
|
||||||
|
управляются репозиторием.
|
||||||
|
- Hermes playbook подготавливает runtime/proxy, но не deploy самого Hermes application.
|
||||||
|
- UI state Uptime Kuma и AdGuard не полностью декларативен.
|
||||||
|
- Constraint: не заявлять полную reproducibility этих компонентов без проверки
|
||||||
|
внешнего состояния.
|
||||||
|
|
||||||
|
## Compatibility Layers
|
||||||
|
|
||||||
|
- Gitea/Vaultwarden `caddy_legacy_regexp` удаляет старые Caddy sections. Не удалять
|
||||||
|
поля до подтвержденного успешного reverse-proxy migration run.
|
||||||
|
- Grimmory OPDS/KOReader headers и compression behavior являются device compatibility
|
||||||
|
contract, а dual v1/v2 API handling MCP соответствует deployed Grimmory v3.2.4.
|
||||||
|
- OpenVPN использует static-key configuration. OpenVPN 2.6 считает этот режим
|
||||||
|
deprecated; миграция на TLS требует отдельного network change plan.
|
||||||
|
|
||||||
|
## Superseded Planning Documents
|
||||||
|
|
||||||
|
- `current-task.md` - исторический Prometheus plan, не active task.
|
||||||
|
- `tasks/grimmory-deployment-plan.md` - смешивает план и deployment record; текущее
|
||||||
|
состояние проверять по Ansible.
|
||||||
|
- Constraint: не выполнять оставшиеся пункты этих документов автоматически.
|
||||||
|
- Decision: preserve as history; новые approved tasks записывать в `plan.md`.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Официальные ссылки
|
||||||
|
|
||||||
|
Ссылки подобраны только для контрактов, которые нельзя надежно вывести из локального
|
||||||
|
кода. Если runtime version не закреплена, это указано явно.
|
||||||
|
|
||||||
|
| Тема | Официальная ссылка | Применимость |
|
||||||
|
|---|---|---|
|
||||||
|
| Ansible check/diff | [Check and diff mode](https://docs.ansible.com/projects/ansible-core/devel/playbook_guide/playbooks_checkmode.html) | Объясняет partial simulation и modules без check-mode; rolling docs, repository lower bound `>=2.19` |
|
||||||
|
| Proxmox Ansible module | [`community.proxmox.proxmox`](https://docs.ansible.com/ansible/latest/collections/community/proxmox/proxmox_module.html) | API provisioning; installed Galaxy version не закреплена |
|
||||||
|
| Proxmox LXC API | [LXC API endpoint](https://pve.proxmox.com/pve-docs/api-viewer/index.html#/nodes/{node}/lxc) | Контракт API role; live PVE version требует проверки |
|
||||||
|
| `pct` | [`pct(1)`](https://pve.proxmox.com/pve-docs/pct.1.html) | Direct-SSH provisioners и diagnostics; rolling PVE manual |
|
||||||
|
| OpenVPN | [OpenVPN 2.6 manual](https://build.openvpn.net/man/openvpn-2.6/openvpn.8.html) | Site/laptop tunnel directives; peer versions неизвестны, static-key mode deprecated в 2.6 |
|
||||||
|
| Caddy validation | [`caddy validate`](https://caddyserver.com/docs/command-line#caddy-validate) | Используется reverse proxy playbook; Caddy version не закреплена |
|
||||||
|
| Caddy reverse proxy | [`reverse_proxy`](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy) | Public upstream и Grimmory handlers |
|
||||||
|
| restic integrity | [Checking integrity and consistency](https://restic.readthedocs.io/en/stable/045_working_with_repos.html#checking-integrity-and-consistency) | Различает metadata check и чтение data packs; runtime version неизвестна |
|
||||||
|
| restic restore | [Restore](https://restic.readthedocs.io/en/stable/050_restore.html) | Selective L2 restore в backup audit |
|
||||||
|
| Uptime Kuma | [README 1.23.16](https://github.com/louislam/uptime-kuma/blob/1.23.16/README.md) | Совпадает с pinned active image version |
|
||||||
|
| MCP TypeScript SDK | [Server guide 1.30.0](https://github.com/modelcontextprotocol/typescript-sdk/blob/1.30.0/docs/server.md) | Совпадает с exact package version и stdio server implementation |
|
||||||
|
| Grimmory API | [OpenAPI v3.2.4](https://github.com/grimmory-tools/grimmory/releases/download/v3.2.4/openapi.json) | Совпадает с deployed version; live docs newer и API объявлен unstable |
|
||||||
|
| Grimmory release | [Release v3.2.4](https://github.com/grimmory-tools/grimmory/releases/tag/v3.2.4) | Release/migration review перед изменением app image |
|
||||||
@@ -0,0 +1,788 @@
|
|||||||
|
# Переход provisioning LXC на OpenTofu
|
||||||
|
|
||||||
|
Пошаговый план миграции. Рассчитан на исполнителя, который этот разговор не
|
||||||
|
видел: всё нужное либо здесь, либо по ссылкам на файлы репозитория.
|
||||||
|
|
||||||
|
Стратегия — **blue-green**: боевые контейнеры не импортируются в Tofu и не
|
||||||
|
переконфигурируются на месте. Вместо этого рядом создаётся новый контейнер,
|
||||||
|
туда переносятся данные, затем переключается адрес, а старый контейнер
|
||||||
|
некоторое время стоит остановленным как откат.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Как начать сессию по этому плану
|
||||||
|
|
||||||
|
Открыть Claude Code в корне репозитория и дать примерно такой промпт,
|
||||||
|
подставив нужный сервис:
|
||||||
|
|
||||||
|
Работаем по docs/ai/migration-tofu.md — переход provisioning LXC на OpenTofu
|
||||||
|
по схеме blue-green. Прочитай его целиком, а также tofu/README.md и
|
||||||
|
AGENTS.md.
|
||||||
|
|
||||||
|
Делаем сервис №1 из раздела 5 (emergency-bot, CT 148). Идём строго по
|
||||||
|
процедуре раздела 4, по шагам, не забегая вперёд.
|
||||||
|
|
||||||
|
Правила:
|
||||||
|
- разрушающие шаги (pct stop, pct destroy, tofu-destroy, смена адреса)
|
||||||
|
выполняешь только после моего явного подтверждения;
|
||||||
|
- старый контейнер не удаляем, он остаётся откатом;
|
||||||
|
- make validate должен быть зелёным до и после;
|
||||||
|
- если реальность разошлась с планом — останавливайся и говори, не
|
||||||
|
придумывай обход.
|
||||||
|
|
||||||
|
Между сервисами сессию имеет смысл начинать заново: контекст одного переезда
|
||||||
|
следующему не нужен, а свежий контекст надёжнее.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Что уже сделано — не переделывать
|
||||||
|
|
||||||
|
- `tofu/` с провайдером `bpg/proxmox`, цели `make tofu-init | tofu-plan |
|
||||||
|
tofu-apply CONFIRM=1 | tofu-destroy CONFIRM=1`.
|
||||||
|
- Аутентификация: `root@pam` по паролю из корневого `.env`
|
||||||
|
(`PROXMOX_ROOT_USER`, `PROXMOX_ROOT_PASSWORD`). Выбор режима автоматический,
|
||||||
|
печатается в stderr. Подробности и матрица возможностей — `tofu/README.md`.
|
||||||
|
- Цели `tofu-*` сами поднимают SSH-туннель через `ru-vps`: провайдер ходит в
|
||||||
|
API по HTTPS и ProxyJump не умеет.
|
||||||
|
- Реестр `homelab_services` программно потребляют: `reverse-proxy.yml`,
|
||||||
|
`ru-vps-base.yml`, `pve-backup-jobs.yml` (списки VMID по `backup.job`),
|
||||||
|
`roles/backup_audit` (VMID по `monitoring.backup_audit_vmid`), `validate.yml`.
|
||||||
|
- `make validate` — read-only gate, сверяет реестр с `pct config` по hostname,
|
||||||
|
IP, cores, memory, swap. Падает при расхождении.
|
||||||
|
- `make lint` воспроизводит CI (линтеры из корня + syntax-check всех плейбуков).
|
||||||
|
|
||||||
|
### Что проверено на пилоте (VMID 199, снесён)
|
||||||
|
|
||||||
|
В режиме `root@pam` Tofu выставляет декларативно всё, что нужно этой
|
||||||
|
инфраструктуре:
|
||||||
|
|
||||||
|
dev0: deny-write=0,path=/dev/net/tun,uid=0,gid=0,mode=0660
|
||||||
|
features: fuse=1,keyctl=1,nesting=1
|
||||||
|
mp0: data:199/vm-199-disk-1.raw,mp=/opt/pilot-data,size=4G
|
||||||
|
|
||||||
|
Повторный `plan` даёт `No changes`. Следствия:
|
||||||
|
|
||||||
|
- правка `/etc/pve/lxc/<vmid>.conf` через `lineinfile` (девять контейнеров)
|
||||||
|
заменяется декларацией в Tofu, но КАКОЙ именно — зависит от устройства:
|
||||||
|
`/dev/fuse` — штатным флагом `features { fuse = true }`, `/dev/net/tun` —
|
||||||
|
блоком `device_passthrough`. Оба варианта видны в выводе пилота выше:
|
||||||
|
`features: fuse=1,...` и `dev0: ...path=/dev/net/tun`;
|
||||||
|
- шаг `pct set <vmid> --features nesting=1,keyctl=1` больше не нужен;
|
||||||
|
- `lifecycle { ignore_changes = [features] }` не нужен;
|
||||||
|
- apply одной фазой.
|
||||||
|
|
||||||
|
**Важно:** всё это работает ТОЛЬКО под `root@pam` по паролю. API-токен, даже
|
||||||
|
принадлежащий root, проверку не проходит — в Proxmox она буквальная
|
||||||
|
(`$authuser eq 'root@pam'`, `PVE/LXC.pm:1658`), а при токенной аутентификации
|
||||||
|
`$authuser` равен полному `user@realm!tokenname` (`PVE/HTTPServer.pm:86`).
|
||||||
|
|
||||||
|
### Ключевое упрощение
|
||||||
|
|
||||||
|
**Если при переезде сохранить IP, меняется только VMID.** А все потребители
|
||||||
|
VMID уже выводят его из реестра автоматически (backup jobs, backup audit).
|
||||||
|
Поэтому cutover — это правка одного поля `vmid` в `services.yml`, а не
|
||||||
|
синхронная правка шести файлов. Ради этого и делалась генерация.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Инварианты
|
||||||
|
|
||||||
|
1. **Последовательным обязан быть только cutover.** Инвариант изначально
|
||||||
|
звучал как «один сервис за раз»; 2026-09-02 он уточнён после параллельного
|
||||||
|
прогона шести сервисов. Разделение такое:
|
||||||
|
- **Параллелится безопасно:** подготовка (снятие эталона, описание в Tofu),
|
||||||
|
создание новых контейнеров (один пакетный `apply`, Tofu сам разводит
|
||||||
|
ресурсы) и конфигурация на временных адресах (шаг 4.3). Всё это не
|
||||||
|
трогает боевые контейнеры и полностью откатывается точечным
|
||||||
|
`tofu destroy` нужного ресурса.
|
||||||
|
- **Остаётся строго последовательным:** перенос данных (4.4), переключение
|
||||||
|
адреса (4.5) и правка реестра (4.6) — по одному сервису, с явным
|
||||||
|
подтверждением человека на каждый разрушающий шаг.
|
||||||
|
Причины, по которым `apply` физически не параллелится: состояние Tofu одно
|
||||||
|
и локальное, а цели `make tofu-*` поднимают SSH-туннель на фиксированный
|
||||||
|
порт 18006 с фиксированным control-сокетом. Два одновременных прогона
|
||||||
|
столкнутся.
|
||||||
|
2. **Старый контейнер останавливается, но не удаляется** — минимум неделю. Это
|
||||||
|
единственный быстрый откат.
|
||||||
|
3. `make validate` зелёный до и после каждого переезда.
|
||||||
|
4. Перед стартом каждого сервиса — свежий бэкап PBS этого VMID.
|
||||||
|
5. VMID и IP выведенных контейнеров не переиспользовать сразу: в PBS остаются
|
||||||
|
цепочки по VMID, в кэшах — адреса.
|
||||||
|
6. **CT 120 `pbs` не мигрировать.** Это цель бэкапов, на которую опирается план
|
||||||
|
отката всех остальных. Он `unmanaged` и таким остаётся.
|
||||||
|
7. Не менять одновременно provisioning и рантайм сервиса без необходимости.
|
||||||
|
Если переводишь `docker run` на `compose_service` — это отдельное
|
||||||
|
осознанное решение по конкретному сервису, а не часть переезда по умолчанию.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Предпосылки перед первым переездом
|
||||||
|
|
||||||
|
- [x] Вывод `memoir-bot` — live-шаги оператора выполнены 2026-09-02: deploy key
|
||||||
|
`SecondBrain` отозван, монитор в Uptime Kuma снят, `pct stop 142`
|
||||||
|
выполнен (подтверждено `pct list` на mini-pc). Это была репетиция
|
||||||
|
удаления старого контейнера. `pct destroy 142` сознательно отложен —
|
||||||
|
см. [`plan.md`](plan.md) и общий инвариант о паузе перед удалением.
|
||||||
|
- [x] `make validate` — зелёный (проверено 2026-09-02, 12 сервисов, drift нет).
|
||||||
|
- [x] `make backup-audit` — прогнан 2026-09-02, все хосты `ok`, без ошибок.
|
||||||
|
- [x] `tofu/terraform.tfstate` — решено оставить только локальным на время
|
||||||
|
переезда сервиса №1. Осознанный риск: state небольшой, при потере можно
|
||||||
|
re-import всё созданное заново. Постоянное решение (например, offsite
|
||||||
|
restic-профиль) — отдельная задача, не блокирует старт.
|
||||||
|
- [x] Свободное место проверено. На 2026-09-02: cloud-pc `data` — 816 ГиБ
|
||||||
|
свободно, mini-pc `local-lvm` — 116 ГиБ. Запаса хватает на любой сервис,
|
||||||
|
включая grimmory (64 ГиБ).
|
||||||
|
|
||||||
|
### Пул временных адресов и VMID
|
||||||
|
|
||||||
|
Свободны на 2026-09-02: IP `192.168.1.6-9, 11-19, 21, 22, 26, 33, 36-40`,
|
||||||
|
VMID `151+` и `199`. Временный адрес обязан лежать в `192.168.1.5-40` —
|
||||||
|
только этот диапазон ru-vps маршрутизирует в LAN через `tun0`, иначе новый
|
||||||
|
контейнер будет недоступен по ProxyJump.
|
||||||
|
|
||||||
|
Шаблон `local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst` присутствует на
|
||||||
|
обеих нодах — проверено.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Процедура переезда одного сервиса
|
||||||
|
|
||||||
|
Обозначения: `OLD` — текущий VMID, `NEW` — временный VMID, `IP` — боевой адрес,
|
||||||
|
`TMPIP` — временный адрес.
|
||||||
|
|
||||||
|
### 4.1 Подготовка
|
||||||
|
|
||||||
|
1. `make validate` и `make backup-audit` — оба зелёные.
|
||||||
|
2. Снять эталон: `ssh <node> sudo pct config OLD` — сохранить вывод. Это
|
||||||
|
источник правды для того, что нужно воспроизвести.
|
||||||
|
3. Запустить свежий бэкап: `ssh <node> sudo vzdump OLD --storage pbs --mode snapshot`.
|
||||||
|
- **Успех проверять наличием снапшота, а не кодом возврата.** Исторически
|
||||||
|
на 2026-09-02 client-side prune давал `missing Datastore.Modify|Datastore.Prune`,
|
||||||
|
из-за чего `vzdump` печатал `Backup of VM ... failed` и возвращал ненулевой
|
||||||
|
код после успешной выгрузки. Это объяснение для старых логов; сейчас
|
||||||
|
client-side prune из плейбуков убран, поэтому любой новый non-zero надо
|
||||||
|
считать проблемой. Проверять так:
|
||||||
|
`ssh cloud-pc 'sudo pvesm list pbs | grep ct/<OLD>'`. Подробности —
|
||||||
|
[`plan.md`](plan.md), раздел про prune.
|
||||||
|
- **Если у сервиса Docker с fuse-overlayfs, а rootfs на каталоговом
|
||||||
|
хранилище (`data`) — сначала остановить сервис.** Такой rootfs не
|
||||||
|
поддерживает снапшоты, vzdump уходит в режим `suspend` с rsync и падает
|
||||||
|
на `var/lib/docker/fuse-overlayfs/.../merged`: Permission denied.
|
||||||
|
Проверено на gitea 2026-09-02: с остановленным сервисом бэкап проходит.
|
||||||
|
У сервисов на `local-lvm` (vaultwarden) проблемы нет — там снапшот.
|
||||||
|
- **У сервиса с bind mount в бэкап попадает только rootfs.** vzdump пишет
|
||||||
|
`excluding bind mount point mpN (...) from backup (not a volume)`. До
|
||||||
|
переезда это касалось gitea: его репозитории и БД в PBS не попадали
|
||||||
|
вообще. После перехода на volume попадают.
|
||||||
|
|
||||||
|
### 4.2 Описание в Tofu
|
||||||
|
|
||||||
|
4. Завести ресурс в `tofu/` (например `tofu/services.tf`), воспроизведя
|
||||||
|
эталон: `vm_id = NEW`, hostname как у боевого, `TMPIP`, cores, memory, swap,
|
||||||
|
rootfs на том же datastore и того же размера, `features`, декларацию для
|
||||||
|
каждого устройства из `lxc.mount.entry`, `mount_point` для каждого `mpN`,
|
||||||
|
`startup.order`, `start_on_boot`, `unprivileged`, `console { type = "shell" }`
|
||||||
|
(эталон `cmode` — см. ниже).
|
||||||
|
- **`/dev/fuse` — это `features.fuse`, а НЕ `device_passthrough`.**
|
||||||
|
Ловушка: в эталоне `/dev/fuse` выглядит как пара сырых строк
|
||||||
|
(`lxc.cgroup2.devices.allow: c 10:229 rwm` + `lxc.mount.entry`), и её
|
||||||
|
хочется механически перевести в `device_passthrough`. Так делать не надо:
|
||||||
|
`device_passthrough` (`dev0:` в PVE 8.2+) предназначен для сырых
|
||||||
|
character-устройств вида `/dev/net/tun`, а у `/dev/fuse` есть штатный
|
||||||
|
флаг PVE, проверенный на пилоте. См. `tofu/pilot.tf.example:17-21`.
|
||||||
|
Следствие: `pct config` нового контейнера покажет `features: fuse=1,...`,
|
||||||
|
то есть будет текстуально отличаться от эталона при том же эффекте —
|
||||||
|
это ожидаемо, а не drift.
|
||||||
|
- **Bind mount заменять на volume.** Если у сервиса `mpN: /host/path,mp=...`
|
||||||
|
(сейчас так только у gitea), в новом контейнере это должен быть
|
||||||
|
`mount_point { volume = "<datastore>", size = "...", path = "..." }`.
|
||||||
|
Данные переносятся на шаге 4.4, а не монтированием того же каталога:
|
||||||
|
два контейнера, пишущие в один каталог, повредят данные.
|
||||||
|
- **`console { type = "shell" }` обязателен.** `roles/pve_lxc` всем
|
||||||
|
контейнерам ставит `cmode: shell`; провайдер отслеживает это через блок
|
||||||
|
`console`, и без явного объявления следующий `tofu-plan` предложит
|
||||||
|
откатить его на дефолт Proxmox `tty` — тот же класс drift, что и
|
||||||
|
`keyctl` в гибридной схеме. Подробности и история находки —
|
||||||
|
`tofu/README.md`.
|
||||||
|
5. `make tofu-plan` — убедиться, что план ровно `1 to add`.
|
||||||
|
6. `make tofu-apply CONFIRM=1`.
|
||||||
|
7. Сверить: `ssh <node> sudo pct config NEW` против эталона из шага 2. Отличаться
|
||||||
|
должны только `vmid` и адрес (плюс явно выписанные Proxmox-дефолты вида
|
||||||
|
`cpulimit`, `protection`, `template`, `tty` — эталон их просто не
|
||||||
|
показывал, реальное поведение то же самое). `cmode: shell` должен совпасть
|
||||||
|
с эталоном уже на этом шаге, если блок `console` объявлен в шаге 4 — правкой
|
||||||
|
постфактум через `pct set` не чинить, это создаёт drift (см. шаг 4 выше и
|
||||||
|
`tofu/README.md`).
|
||||||
|
|
||||||
|
### 4.3 Настройка
|
||||||
|
|
||||||
|
8. Добавить временную запись в `ansible/inventory/hosts.yml`: `<name>-new` с
|
||||||
|
`ansible_host: TMPIP` и `expected_lan_ip: TMPIP`, и такую же запись в
|
||||||
|
`ansible/ssh_config` (Host-блок + в агрегированные строки ProxyJump, root,
|
||||||
|
общие опции).
|
||||||
|
- Во время cutover использовалась временная группа `lxc_migration_new`;
|
||||||
|
она не входила в `servers`, поэтому незавершённый переезд не краснил
|
||||||
|
`make check` и `make status`.
|
||||||
|
- **Транспорт задавался только в `group_vars/lxc_migration_new/main.yml`, а
|
||||||
|
не блоком `vars:` в самом `hosts.yml`.** У Ansible inline-переменные
|
||||||
|
группы из inventory-файла имеют приоритет НИЖЕ, чем `group_vars/all/`,
|
||||||
|
поэтому `ansible_user: ansible` оттуда перебивает inline `root`, и
|
||||||
|
подключение падает с `Permission denied`. Каталог `group_vars/<группа>/`
|
||||||
|
— наоборот, выше `all`. Найдено 2026-09-02.
|
||||||
|
|
||||||
|
9. Прогнать конфигурационную часть существующего плейбука против нового
|
||||||
|
контейнера:
|
||||||
|
|
||||||
|
ansible-playbook playbooks/pve-<name>.yml \
|
||||||
|
-e pve_config_target=<name>-new --limit <name>-new
|
||||||
|
|
||||||
|
**Одного `--limit <name>-new` НЕДОСТАТОЧНО, и это не особенность отдельного
|
||||||
|
сервиса.** Ansible пересекает лимит с паттерном play, а не подменяет его.
|
||||||
|
Паттерн конфигурационного play — литеральное имя боевого хоста (`hosts:
|
||||||
|
gitea`), пересечение с `gitea-new` пусто, и play молча получает `hosts (0)`.
|
||||||
|
Проверено 2026-09-02 через `--list-hosts` на всех плейбуках сразу: ноль
|
||||||
|
хостов во ВСЕХ play, а не только в создающих. Прошлая сессия
|
||||||
|
(emergency-bot) сочла это особенностью того сервиса — на самом деле это
|
||||||
|
общее свойство.
|
||||||
|
|
||||||
|
Поэтому у конфигурационных play заведён переопределяемый таргет:
|
||||||
|
|
||||||
|
hosts: "{{ pve_config_target | default('<name>') }}"
|
||||||
|
|
||||||
|
Он есть в `pve-docker-test.yml`, `pve-gitea.yml`, `pve-vaultwarden.yml`,
|
||||||
|
`pve-grimmory.yml`, `gyro.yml` и `uptime-kuma.yml`. По умолчанию поведение
|
||||||
|
не меняется — без `-e` паттерн равен прежнему литералу.
|
||||||
|
|
||||||
|
`--limit` при этом всё равно обязателен: он отсекает play создания
|
||||||
|
контейнера и play-стражи, которые таргетят ноду (`cloud-pc`/`mini-pc`) или
|
||||||
|
`localhost`. Без него плейбук пойдёт делать `pct create` и править
|
||||||
|
`/etc/pve/lxc/<OLD>.conf` на боевом VMID.
|
||||||
|
|
||||||
|
**Перед каждым прогоном сверяться с `--list-hosts`.** Ожидание: у play
|
||||||
|
создания и стражей `hosts (0)`, у конфигурационного — ровно `hosts (1):
|
||||||
|
<name>-new`. Это и есть предохранитель, а не формальность.
|
||||||
|
|
||||||
|
Особые случаи:
|
||||||
|
- `monitoring` — конфигурации в `pve-monitoring.yml` нет вообще, там только
|
||||||
|
создание. Настраивает `playbooks/uptime-kuma.yml` (роль `uptime_kuma`).
|
||||||
|
`playbooks/monitoring.yml` (замороженный Prometheus) НЕ запускать.
|
||||||
|
- `gyro` — конфигурация в `playbooks/gyro.yml`, нужен пароль Ansible Vault
|
||||||
|
(`make gyro` / `--ask-vault-pass`). Роль читает `host_vars/gyro/`.
|
||||||
|
Во время cutover использовался временный host_vars shim; после перевода CT
|
||||||
|
156 на боевой IP он удалён.
|
||||||
|
|
||||||
|
10. Проверить, что сервис поднялся на `TMPIP` (его health-эндпоинт или порт).
|
||||||
|
У сервисов с данными новый контейнер на этом шаге работает на ПУСТОМ
|
||||||
|
состоянии — данные приезжают только на шаге 4.4. Пустой Gitea, пустой
|
||||||
|
Vaultwarden с экраном создания учётки, пустая Uptime Kuma, свежая схема
|
||||||
|
Flyway у grimmory — это ожидаемый результат шага 4.3, а не поломка.
|
||||||
|
Поднимать новую копию на боевых данных до остановки старой НЕЛЬЗЯ: две
|
||||||
|
живые копии над одной SQLite или одной MariaDB повредят состояние.
|
||||||
|
|
||||||
|
### 4.4 Перенос данных
|
||||||
|
|
||||||
|
11. Остановить сервис на СТАРОМ контейнере (`systemctl stop <unit>`), сам
|
||||||
|
контейнер пока не трогать.
|
||||||
|
12. Финальная синхронизация. Способ зависит от сервиса — см. раздел 5.
|
||||||
|
13. Запустить сервис на новом, проверить на `TMPIP`.
|
||||||
|
|
||||||
|
### 4.5 Переключение
|
||||||
|
|
||||||
|
14. `ssh <node> sudo pct stop OLD`.
|
||||||
|
15. В Tofu поменять адрес нового контейнера с `TMPIP` на `IP`, `make tofu-apply CONFIRM=1`.
|
||||||
|
16. Перезапустить контейнер, если адрес не подхватился на живую.
|
||||||
|
- **Если конфигурация сервиса содержит его собственный адрес — прогнать
|
||||||
|
плейбук заново сразу после смены адреса.** Из пакета 2026-09-02 это
|
||||||
|
касается только `grimmory` (`ports: <ip>:6060:6060` и правила
|
||||||
|
`GRIMMORY-FILTER`): его compose всё ещё содержит TMPIP, и сервис не
|
||||||
|
поднимется, пока файл не перегенерируют. Проверять просто:
|
||||||
|
`ss -ltn` на новом контейнере — если сокет висит на конкретном адресе,
|
||||||
|
а не на `0.0.0.0`/`*`, повторный прогон обязателен. У gitea,
|
||||||
|
vaultwarden и uptime-kuma сокеты wildcard, им это не нужно.
|
||||||
|
Прогон уже без `-e pve_config_target`: адрес стал боевым, и имя хоста
|
||||||
|
само резолвится в новый контейнер.
|
||||||
|
17. Health-check на боевом `IP`.
|
||||||
|
18. Если сервис публичный (`proxy` в реестре) — `make reverse-proxy` и проверить
|
||||||
|
домен снаружи. При сохранённом IP апстрим не меняется, но прогон подтвердит.
|
||||||
|
19. Обновить `~/.ssh/known_hosts` — host key нового контейнера другой:
|
||||||
|
`ssh-keygen -R <IP>` и `ssh-keygen -R <name>`. В `ssh_config` стоит
|
||||||
|
`StrictHostKeyChecking accept-new`, который принимает НОВЫЕ ключи, но не
|
||||||
|
изменившиеся, поэтому без чистки подключение будет отвергнуто.
|
||||||
|
- **Заодно закрыть ControlMaster: `ssh -F ansible/ssh_config -O exit
|
||||||
|
<host>`.** Мастер переживает cutover живым, но его туннель ведёт в уже
|
||||||
|
остановленный контейнер; новые сессии мультиплексируются поверх мёртвого
|
||||||
|
соединения и виснут с `Connection timed out during banner exchange`.
|
||||||
|
Симптом выглядит как сетевая проблема, хотя контейнер полностью доступен
|
||||||
|
с ноды. Разовый обход для диагностики — `-o ControlPath=none`.
|
||||||
|
Найдено 2026-09-02 на docker-test.
|
||||||
|
- У gitea SSH host-ключи git-over-ssh лежат в переносимых данных
|
||||||
|
(`<data>/ssh/ssh_host_*`), поэтому порт 2222 после переезда отдаёт ТОТ ЖЕ
|
||||||
|
ключ и `known_hosts` git-клиентов остаётся валидным. Менять нужно только
|
||||||
|
ключ sshd самого контейнера (порт 22).
|
||||||
|
- **Про gyro и gitea: ранее записанное предупреждение неверно, проверено
|
||||||
|
2026-09-02.** Утверждалось, что `roles/gyro` пинит host key
|
||||||
|
`[192.168.1.25]:2222` и после переезда gitea сломается. На самом деле
|
||||||
|
задача `Remove obsolete Gitea known host` стоит с `state: absent` — она
|
||||||
|
УДАЛЯЕТ устаревшую запись, а не создаёт её. Пинится
|
||||||
|
`gyro_git_known_hosts_name: github.com`, и `gyro_repo_url` —
|
||||||
|
`git@github.com:...`. Gyro клонирует с GitHub, к gitea отношения не
|
||||||
|
имеет. Никаких дополнительных действий после переезда gitea не нужно.
|
||||||
|
|
||||||
|
### 4.5a Offsite restic: профиль надо переустановить на новом контейнере
|
||||||
|
|
||||||
|
**Шаг, которого в плане не было. Найден 2026-09-02 на vaultwarden.**
|
||||||
|
|
||||||
|
Если offsite-профиль restic выполняется ВНУТРИ контейнера сервиса (а не на
|
||||||
|
ноде), то на новом контейнере его нет: там нет ни `restic`, ни `rclone`, ни
|
||||||
|
systemd-таймера. Бэкап тихо перестаёт выполняться, и `make backup-audit` этого
|
||||||
|
не ловит — он ставит только свой таймер аудита.
|
||||||
|
|
||||||
|
Проверять так:
|
||||||
|
|
||||||
|
ssh <name> 'command -v restic rclone; systemctl list-timers --all | grep restic'
|
||||||
|
|
||||||
|
Чинить так:
|
||||||
|
|
||||||
|
make offsite-restic EXTRA="--limit <name>"
|
||||||
|
|
||||||
|
Затем обязательно проверить всю цепочку, а не только наличие таймера:
|
||||||
|
|
||||||
|
ssh <name> 'systemctl start homelab-restic-offsite-<name>.service'
|
||||||
|
ssh <name> 'journalctl -u homelab-restic-offsite-<name>.service -n 30 --no-pager'
|
||||||
|
|
||||||
|
Успех выглядит как подключение к репозиторию с уже существующими снапшотами и
|
||||||
|
`Finished ... Deactivated successfully`.
|
||||||
|
|
||||||
|
Кого это касается:
|
||||||
|
|
||||||
|
- `vaultwarden` — профиль внутри LXC. Сделано и проверено 2026-09-02.
|
||||||
|
- `grimmory` — профиль тоже внутри LXC (`hosts: grimmory`). Понадобится то же.
|
||||||
|
- `gitea` — профиль выполняется на cloud-pc и указывает на `/opt/data/gitea`.
|
||||||
|
Там задача другая: после переезда на volume этот путь исчезает, профиль надо
|
||||||
|
переписать на выполнение внутри контейнера, по образцу vaultwarden.
|
||||||
|
|
||||||
|
### 4.6 Реестр и потребители
|
||||||
|
|
||||||
|
20. В `ansible/inventory/group_vars/all/services.yml` поменять `vmid: OLD` на
|
||||||
|
`vmid: NEW`. Если менялся `provisioner` — поправить и его на `tofu`.
|
||||||
|
21. Убрать временные записи `<name>-new` из `hosts.yml` и `ssh_config`.
|
||||||
|
22. Прогнать зависящие цели: `make backup-jobs` (списки VMID выведутся заново),
|
||||||
|
`make backup-audit` (перерендерит скрипт аудита с новым VMID).
|
||||||
|
23. `make validate` — должен быть зелёный.
|
||||||
|
24. `make status` — контейнер UP, юниты active.
|
||||||
|
|
||||||
|
### 4.7 Завершение
|
||||||
|
|
||||||
|
25. Оставить `OLD` остановленным минимум неделю.
|
||||||
|
26. Затем `pct destroy OLD` и решить судьбу его цепочки бэкапов в PBS.
|
||||||
|
27. Удалить из `playbooks/pve-<name>.yml` часть, создающую контейнер (первый play
|
||||||
|
с `pct create` / ролью `pve_lxc`, правками `/etc/pve/lxc/*.conf` и
|
||||||
|
`pct set --features`). Оставить конфигурационную часть.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Порядок сервисов и специфика
|
||||||
|
|
||||||
|
Порядок выбран по возрастанию риска. Не менять без причины.
|
||||||
|
|
||||||
|
| # | Сервис | VMID | Узел | Почему здесь |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 1 | `emergency-bot` | ~~148~~ **151** | mini-pc | ПЕРЕЕХАЛ 2026-09-02. Полностью шаблонный, персистентных данных нет |
|
||||||
|
| 2 | `docker-test` | ~~145~~ **152** | cloud-pc | ПЕРЕЕХАЛ 2026-09-02 |
|
||||||
|
| 3 | `gitea` | ~~141~~ **153** | cloud-pc | ПЕРЕЕХАЛ 2026-09-02; bind mount → volume, данные впервые попали в PBS |
|
||||||
|
| 4 | `vaultwarden` | ~~140~~ **154** | mini-pc | ПЕРЕЕХАЛ 2026-09-02 |
|
||||||
|
| 5 | `monitoring` | ~~146~~ **155** | cloud-pc | ПЕРЕЕХАЛ 2026-09-02; замороженный стек не переносился |
|
||||||
|
| 6 | `gyro` | ~~150~~ **156** | mini-pc | CUTOVER COMPLETED 2026-09-02; CT156 live on 192.168.1.35, CT150 held as rollback |
|
||||||
|
| 7 | `grimmory` | ~~149~~ **157** | cloud-pc | ПЕРЕЕХАЛ 2026-09-02; MariaDB через dump/restore |
|
||||||
|
| 8 | `adguard` | ~~144~~ **158** | mini-pc | ПЕРЕЕХАЛ 2026-09-03; данные /opt/adguard, DNS-провал ~2 мин |
|
||||||
|
| 9 | `mihomo` | ~~143~~ **159** | mini-pc | ПЕРЕЕХАЛ 2026-09-03; первый боевой device_passthrough /dev/net/tun |
|
||||||
|
| 10 | `ovpn-mini` | ~~132~~ **160** | mini-pc | ПЕРЕЕХАЛ 2026-09-03; туннель-петля при cutover, см. 5.10 |
|
||||||
|
| — | `hermes-ai` | 147 | cloud-pc | заморожен и недостижим по SSH, см. раздел 7 |
|
||||||
|
| — | `pbs` | 120 | cloud-pc | не мигрировать |
|
||||||
|
|
||||||
|
**Все мигрируемые сервисы (1-10) переехали на OpenTofu.** Осталось: `hermes-ai`
|
||||||
|
(заморожен), `pbs` (не мигрируется). Пост-миграционная уборка раздела 6
|
||||||
|
(удаление `roles/pve_lxc`, стрип creation plays у 8 плейбуков, правки
|
||||||
|
`architecture.md`) — отдельная задача, ещё не сделана; частично разблокирована.
|
||||||
|
|
||||||
|
### Специфика по сервисам
|
||||||
|
|
||||||
|
**1. `emergency-bot`.** ПЕРЕЕХАЛ 2026-09-02: OLD 148 остановлен, NEW 151 боевой
|
||||||
|
на 192.168.1.32. Данных нет вообще: `roles/emergency_bot` разворачивает
|
||||||
|
`emergency_bot.py.j2`, `.env.j2` и юнит из шаблонов. Шаг 4.4 пропущен целиком.
|
||||||
|
Бэкапа у него нет (`backup: none`) и это не мешает — он воспроизводим из
|
||||||
|
репозитория. Нужны `EMERGENCY_*` в корневом `.env`. Хороший первый заход
|
||||||
|
именно потому, что ошибиться почти негде — тем не менее, реальность разошлась
|
||||||
|
с планом в двух местах, оба задокументированы там, где их искать:
|
||||||
|
|
||||||
|
- **`playbooks/pve-emergency-bot.yml` не содержит конфигурационной части.**
|
||||||
|
У этого сервиса она в отдельном `playbooks/emergency-access.yml`, причём
|
||||||
|
два из четырёх play там таргетят `mini-pc` и `ru-vps`, а не сам контейнер
|
||||||
|
(доверие reverse-SSH через `authorized_key` с `exclusive: true` — полная
|
||||||
|
замена ключа, не добавление). Шаг 4.3.9 в его буквальном виде
|
||||||
|
(`--limit <name>-new` на существующем плейбуке) для этого сервиса не
|
||||||
|
работает: `hosts:` там — жёсткие имена, не группа, и полный прогон против
|
||||||
|
`mini-pc`/`ru-vps` до cutover преждевременно переключил бы доверие. Кроме
|
||||||
|
того, `emergency_bot.py.j2` использует Telegram `getUpdates` (не webhook) —
|
||||||
|
запуск второй копии с тем же `EMERGENCY_BOT_TOKEN`, что и боевая, ловит
|
||||||
|
409 conflict. Решение для шага 4.3 при следующем сервисе с похожей
|
||||||
|
архитектурой: деплоить только play(-и), таргетящие сам новый контейнер,
|
||||||
|
через одноразовый playbook с `hosts: <name>-new` и заведомо нерабочим,
|
||||||
|
но валидным по формату токеном/credential — так проверяется факт деплоя
|
||||||
|
(юнит стартует, зависимости стоят, egress работает) без конфликта с боевым
|
||||||
|
инстансом. Полный прогон с реальными credentials — уже после cutover,
|
||||||
|
когда исходный playbook с жёстким `hosts: <name>` сам начинает резолвиться
|
||||||
|
в новый контейнер (адрес не менялся, значит `--limit` не нужен вообще).
|
||||||
|
- **`cmode` — провайдер отслеживает его через блок `console`, но по
|
||||||
|
умолчанию блок не объявлен.** Подробности и правильная декларация —
|
||||||
|
`tofu/README.md` и шаг 4.2 выше.
|
||||||
|
|
||||||
|
**2. `docker-test` (145).** Тоже без данных. Проверяет проброс `/dev/fuse`
|
||||||
|
через `features.fuse` на реальном сервисе (не через `device_passthrough` —
|
||||||
|
см. уточнение в шаге 4.2).
|
||||||
|
|
||||||
|
**3. `gitea` (141 -> 153).** Главный приз и самый содержательный шаг.
|
||||||
|
- Было: `mp0: /opt/data/gitea,mp=/opt/gitea/data` — bind mount каталога ноды.
|
||||||
|
**Стало и подтверждено на живом контейнере 2026-09-02:**
|
||||||
|
`mp0: data:153/vm-153-disk-1.raw,mp=/opt/gitea/data,size=32G` — независимый
|
||||||
|
volume. Внутри виден как отдельная ФС `/dev/loop10`, ext4, 32 ГиБ.
|
||||||
|
Ради этого миграция и затевалась.
|
||||||
|
- Данные: `/opt/data/gitea` на cloud-pc (2.3 ГБ) → внутрь нового контейнера.
|
||||||
|
Каталоги `git/` (репозитории), `gitea/` (в т.ч. SQLite `gitea/gitea.db`,
|
||||||
|
~37 МиБ) и `ssh/`. На ноде всё принадлежит `101000:101000` — это host-side
|
||||||
|
отображение uid 1000 внутри unprivileged LXC.
|
||||||
|
Последовательность составлена по итогам разведки, но НЕ исполнялась:
|
||||||
|
|
||||||
|
ssh gitea 'systemctl stop gitea' # OLD, адрес ещё боевой
|
||||||
|
ssh cloud-pc "sudo sqlite3 /opt/data/gitea/gitea/gitea.db \
|
||||||
|
'PRAGMA integrity_check;'" # ожидается ok
|
||||||
|
|
||||||
|
# tar по SSH: rsync не годится, его нет на свежем контейнере,
|
||||||
|
# а tar есть в любом Debian
|
||||||
|
ssh cloud-pc 'sudo tar -C /opt/data/gitea -cf - .' \
|
||||||
|
| ssh gitea-new 'tar -C /opt/gitea/data -xf -'
|
||||||
|
|
||||||
|
# volume создан с нуля, поэтому владельца выставить явно ИЗНУТРИ
|
||||||
|
# контейнера — так результат не зависит от offset idmap
|
||||||
|
ssh gitea-new 'chown -R 1000:1000 /opt/gitea/data'
|
||||||
|
ssh gitea-new "sqlite3 /opt/gitea/data/gitea/gitea.db 'PRAGMA integrity_check;'"
|
||||||
|
ssh gitea-new 'systemctl start gitea'
|
||||||
|
|
||||||
|
Копирование внутри одного узла упирается в скорость диска, не сети: порядка
|
||||||
|
1-3 минут на 2.3 ГБ. Общее окно простоя `git.ada-dev.ru` — 5-10 минут.
|
||||||
|
- Offsite restic-профиль `gitea` выполняется **на cloud-pc**, а не внутри LXC,
|
||||||
|
и указывает на `/opt/data/gitea`. После переезда на volume этот путь исчезнет
|
||||||
|
— профиль в `playbooks/offsite-restic-yadisk.yml` придётся переписать на
|
||||||
|
новый источник. **Не забыть**, иначе offsite-бэкап Gitea тихо перестанет
|
||||||
|
работать.
|
||||||
|
- ~~`roles/gyro` пинит SSH host key gitea — обновить.~~ НЕВЕРНО, снято
|
||||||
|
2026-09-02: там `state: absent` (удаление устаревшей записи), а пинится
|
||||||
|
github.com. Подробности — в шаге 4.5.19.
|
||||||
|
- Публичный домен `git.ada-dev.ru` — после cutover `make reverse-proxy`.
|
||||||
|
|
||||||
|
**4. `vaultwarden` (140 -> 154).** SQLite `/opt/vaultwarden/data/db.sqlite3`,
|
||||||
|
публичный домен `pass.ada-dev.ru`.
|
||||||
|
|
||||||
|
Разведка 2026-09-02: каталог данных 2.9 МБ; `db.sqlite3` с живыми `-wal`/`-shm`,
|
||||||
|
`icon_cache`, `rsa_key.pem`, `config.json`. Каталогов `attachments/` и `sends/`
|
||||||
|
нет, эти возможности не используются. **`config.json` содержит ADMIN_TOKEN и
|
||||||
|
SMTP**, Ansible ими не управляет; они переедут вместе с каталогом. Это
|
||||||
|
ожидаемо, но файл не должен попасть в репозиторий.
|
||||||
|
|
||||||
|
Offsite-профиль restic здесь выполняется ВНУТРИ контейнера
|
||||||
|
(`hosts: vaultwarden`, `offsite_source_path: /opt/vaultwarden/data`), а не на
|
||||||
|
ноде, и адрес при переезде не меняется, поэтому **правки профиля не требуется**
|
||||||
|
в отличие от gitea.
|
||||||
|
|
||||||
|
Оба контейнера на mini-pc, поэтому копирование идёт через `pct exec` без
|
||||||
|
промежуточного файла. Последовательность составлена по итогам разведки, но НЕ
|
||||||
|
исполнялась:
|
||||||
|
|
||||||
|
ssh mini-pc 'sudo pgrep -x vzdump' # ожидается пусто
|
||||||
|
ssh mini-pc 'sudo pct exec 140 -- systemctl stop vaultwarden'
|
||||||
|
|
||||||
|
# .backup сливает WAL в один файл и заодно проверяет источник
|
||||||
|
ssh mini-pc "sudo pct exec 140 -- sqlite3 -cmd 'PRAGMA busy_timeout=30000;' \
|
||||||
|
/opt/vaultwarden/data/db.sqlite3 \".backup '/opt/vaultwarden/data/db.sqlite3.migrate'\""
|
||||||
|
ssh mini-pc 'sudo pct exec 140 -- sqlite3 \
|
||||||
|
/opt/vaultwarden/data/db.sqlite3.migrate "PRAGMA integrity_check;"'
|
||||||
|
|
||||||
|
ssh mini-pc 'sudo sh -c "pct exec 140 -- tar --exclude=./db.sqlite3 \
|
||||||
|
--exclude=./db.sqlite3-wal --exclude=./db.sqlite3-shm \
|
||||||
|
-cf - -C /opt/vaultwarden/data . | pct exec 154 -- tar -xf - -C /opt/vaultwarden/data"'
|
||||||
|
ssh mini-pc 'sudo pct exec 154 -- mv /opt/vaultwarden/data/db.sqlite3.migrate \
|
||||||
|
/opt/vaultwarden/data/db.sqlite3'
|
||||||
|
ssh mini-pc 'sudo pct exec 154 -- sqlite3 /opt/vaultwarden/data/db.sqlite3 \
|
||||||
|
"PRAGMA integrity_check;"'
|
||||||
|
ssh mini-pc 'sudo pct exec 140 -- rm /opt/vaultwarden/data/db.sqlite3.migrate'
|
||||||
|
ssh mini-pc 'sudo pct exec 154 -- systemctl restart vaultwarden'
|
||||||
|
|
||||||
|
Объём крошечный, простой на данных порядка полуминуты: время съедают
|
||||||
|
stop/restart и проверки, а не копирование.
|
||||||
|
|
||||||
|
**5. `monitoring` (146 -> 155).** Замороженный Prometheus-стек в новый
|
||||||
|
контейнер не разворачивать.
|
||||||
|
|
||||||
|
**Данные Uptime Kuma лежат НЕ в `/opt/monitoring`**, как утверждалось раньше.
|
||||||
|
Разведка 2026-09-02: это два независимых каталога верхнего уровня.
|
||||||
|
|
||||||
|
| Путь | Размер | Что это |
|
||||||
|
|---|---|---|
|
||||||
|
| `/opt/uptime-kuma` | 27 МБ | активный сервис, переносить целиком |
|
||||||
|
| `/opt/uptime-kuma/data/kuma.db` | 25.6 МиБ | мониторы и уведомления |
|
||||||
|
| `/opt/monitoring` | 1.7 ГБ | замороженный стек, почти всё — TSDB Prometheus |
|
||||||
|
|
||||||
|
Переносить нужно только `/opt/uptime-kuma`. TSDB замороженного стека переносить
|
||||||
|
незачем: читать его в новом контейнере будет нечему.
|
||||||
|
|
||||||
|
Конфигурация нового контейнера — `playbooks/uptime-kuma.yml`. У
|
||||||
|
`pve-monitoring.yml` конфигурационной части нет вообще, только создание.
|
||||||
|
`playbooks/monitoring.yml` не запускать.
|
||||||
|
|
||||||
|
Последовательность составлена по итогам разведки, но НЕ исполнялась:
|
||||||
|
|
||||||
|
ssh cloud-pc 'sudo pct exec 146 -- sqlite3 /opt/uptime-kuma/data/kuma.db \
|
||||||
|
"PRAGMA wal_checkpoint(TRUNCATE);"'
|
||||||
|
ssh cloud-pc 'sudo pct exec 146 -- systemctl stop uptime-kuma'
|
||||||
|
ssh cloud-pc 'sudo pct exec 146 -- sqlite3 /opt/uptime-kuma/data/kuma.db \
|
||||||
|
"PRAGMA integrity_check;"'
|
||||||
|
ssh cloud-pc 'sudo sh -c "pct exec 146 -- tar -C /opt -cpf - uptime-kuma \
|
||||||
|
| pct exec 155 -- tar -C /opt -xpf -"'
|
||||||
|
ssh cloud-pc 'sudo pct exec 155 -- sqlite3 /opt/uptime-kuma/data/kuma.db \
|
||||||
|
"PRAGMA integrity_check;"'
|
||||||
|
ssh cloud-pc 'sudo pct exec 155 -- systemctl start uptime-kuma'
|
||||||
|
|
||||||
|
**Вторую копию нельзя поднимать на боевых мониторах до остановки первой:**
|
||||||
|
Uptime Kuma активно пробит сервисы и шлёт уведомления, две копии дадут
|
||||||
|
дубликаты алертов.
|
||||||
|
|
||||||
|
Уменьшение контейнера — отдельное решение, не часть переезда. Цифры для него,
|
||||||
|
снятые 2026-09-02: боевой CT 146 занимает 173 МБ RAM из 4096 и 4.7 ГБ из 24;
|
||||||
|
новый CT 155 без Prometheus-стека — 184 МБ RAM и 1.8 ГБ диска.
|
||||||
|
|
||||||
|
В контейнере нет `curl` — для ручных HTTP-проверок использовать `wget`.
|
||||||
|
|
||||||
|
**6. `gyro` (150 -> 156).** CUTOVER COMPLETED 2026-09-02. CT 156 живёт на
|
||||||
|
неизменном production IP `192.168.1.35`, provisioned by Tofu. CT 150
|
||||||
|
остановлен и удерживается как rollback минимум на неделю; его PBS chain не
|
||||||
|
удалялся. Файл фаервола `156.fw` уже копия `150.fw`, cluster firewall по-
|
||||||
|
прежнему disabled. Timer `gyro.timer` active, last oneshot succeeded.
|
||||||
|
Данных для переноса не было, и TMPIP-specific bind/re-run не требовались.
|
||||||
|
|
||||||
|
**7. `grimmory` (149 -> 157).** Самый болезненный, и единственный, где
|
||||||
|
конфигурационный play пришлось чинить.
|
||||||
|
|
||||||
|
**Найденный баг и его исправление (2026-09-02).** В конфигурационном play
|
||||||
|
боевой адрес был зашит в трёх местах, из-за чего play невозможно было
|
||||||
|
прогнать против любого другого контейнера:
|
||||||
|
|
||||||
|
- `ports: "192.168.1.34:6060:6060"` в compose — Docker не биндит чужой адрес и
|
||||||
|
роняет `grimmory.service` ещё до старта приложения;
|
||||||
|
- правила `GRIMMORY-FILTER` (`--ctorigdst 192.168.1.34`) — фильтровали бы не
|
||||||
|
тот адрес;
|
||||||
|
- URL задачи `Wait for Grimmory health endpoint`. **Это было опаснее всего:**
|
||||||
|
задача выполняется на самом целевом хосте, поэтому даже после починки
|
||||||
|
биндинга она уходила бы по сети в БОЕВОЙ grimmory, получала 200 и давала
|
||||||
|
ложно-положительный результат, ничего не проверив в новом контейнере.
|
||||||
|
|
||||||
|
Исправлено через `grimmory_bind_ip: "{{ expected_lan_ip }}"` — ту же идиому,
|
||||||
|
что использует `roles/uptime_kuma`. На боевом хосте переменная равна
|
||||||
|
192.168.1.34, поэтому рендер не изменился: `--check --diff` с `--limit
|
||||||
|
grimmory` даёт `changed=0`, включая обе задачи, которые пишут адрес в файлы.
|
||||||
|
|
||||||
|
**Следствие для шага 4.5, которого в плане не было.** Grimmory — единственный
|
||||||
|
из шести сервисов пакета, кто биндится на конкретный адрес (`ss -ltn` на новых
|
||||||
|
контейнерах: gitea `0.0.0.0:3000`, vaultwarden `0.0.0.0:80`, uptime-kuma
|
||||||
|
`*:3001`, grimmory `192.168.1.16:6060`). После смены адреса на боевой compose
|
||||||
|
всё ещё будет содержать TMPIP, и сервис не поднимется. Плейбук ОБЯЗАН быть
|
||||||
|
прогнан заново сразу после cutover — см. шаг 4.5.16.
|
||||||
|
|
||||||
|
**Состояние после конфигурации на TMPIP (проверено):** оба юнита active,
|
||||||
|
оба контейнера healthy, `health=200` на 192.168.1.16, прогон идемпотентен
|
||||||
|
(`changed=0`).
|
||||||
|
|
||||||
|
**Образы совпали с боевым по digest** — `grimmory/grimmory:v3.2.4@sha256:dfa7afdf…`
|
||||||
|
и `lscr.io/linuxserver/mariadb:11.4.8@sha256:91de7f70…`. Это требование
|
||||||
|
`legacy-warning.md`: code-only downgrade после Flyway-миграции запрещён.
|
||||||
|
|
||||||
|
**Flyway: схемы сошлись.** На боевом и на новом контейнере одинаково —
|
||||||
|
`MAX(version)=144`, 142 миграции, все успешны. То есть тот же образ на пустой
|
||||||
|
базе приходит ровно к боевой версии схемы, и restore дампа новых миграций не
|
||||||
|
вызовет. Холодный старт с нуля занимает около 5 минут — health-check ретраится,
|
||||||
|
это нормально.
|
||||||
|
|
||||||
|
**Перенос данных.** `/opt/grimmory` — 311 МБ: `books/` 139 МБ (библиотека),
|
||||||
|
`data/` 5.6 МБ (обложки), `mariadb/` 167 МБ. Файлы MariaDB копировать НЕЛЬЗЯ,
|
||||||
|
нужен dump/restore. Рабочий рецепт дампа уже есть в
|
||||||
|
`tasks/offsite-restic-profile.yml` (`mariadb-dump --single-transaction
|
||||||
|
--routines --events`). Последовательность составлена по итогам разведки, но НЕ
|
||||||
|
исполнялась:
|
||||||
|
|
||||||
|
# приложение стоп, MariaDB оставить живой
|
||||||
|
ssh grimmory 'cd /opt/grimmory && docker compose stop grimmory'
|
||||||
|
ssh grimmory 'set -a; . /opt/grimmory/.env; set +a; \
|
||||||
|
docker exec -e MYSQL_PWD="$DB_PASSWORD" grimmory-mariadb mariadb-dump \
|
||||||
|
--user=grimmory --single-transaction --routines --events \
|
||||||
|
--databases grimmory > /opt/grimmory/backup-staging/grimmory.sql'
|
||||||
|
|
||||||
|
# библиотека и обложки
|
||||||
|
ssh cloud-pc 'sudo sh -c "pct exec 149 -- tar -C /opt/grimmory -cpf - books data \
|
||||||
|
| pct exec 157 -- tar -C /opt/grimmory -xpf -"'
|
||||||
|
|
||||||
|
# дамп на новый и restore
|
||||||
|
ssh cloud-pc 'sudo sh -c "pct exec 149 -- cat /opt/grimmory/backup-staging/grimmory.sql \
|
||||||
|
| pct exec 157 -- tee /opt/grimmory/backup-staging/grimmory.sql >/dev/null"'
|
||||||
|
ssh grimmory-new 'cd /opt/grimmory && docker compose stop grimmory'
|
||||||
|
ssh grimmory-new 'set -a; . /opt/grimmory/.env; set +a; \
|
||||||
|
docker exec -i -e MYSQL_PWD="$DB_PASSWORD" grimmory-mariadb mariadb \
|
||||||
|
--user=grimmory grimmory < /opt/grimmory/backup-staging/grimmory.sql'
|
||||||
|
ssh grimmory-new 'systemctl restart grimmory'
|
||||||
|
|
||||||
|
**Дамп не содержит `--add-drop-database`.** На новом контейнере схема уже
|
||||||
|
создана Flyway с нуля, поэтому перед restore её нужно либо очистить, либо
|
||||||
|
добавить эту опцию в дамп. Отдельно стоит знать, что restore-путь в этом
|
||||||
|
репозитории НИКОГДА не проверялся: `edge-cases.md` отмечает, что аудит
|
||||||
|
проверяет непустоту дампа, но не импортирует его.
|
||||||
|
|
||||||
|
**Эталон для сверки после restore** (снят с боевого 2026-09-02):
|
||||||
|
`book 31`, `author 30`, `book_file 40`, `book_metadata 31`, `category 87`,
|
||||||
|
`reading_sessions 68`, `shelf 3`, `library 1`, `users 1`, `opds_user_v2 2`,
|
||||||
|
`koreader_user 1`, `flyway_schema_history 142`.
|
||||||
|
|
||||||
|
**После cutover — проверка OPDS на живой читалке, а не только curl'ом.**
|
||||||
|
Быстрая проверка заголовков:
|
||||||
|
|
||||||
|
curl -sS -D- -o /dev/null https://books.ada-dev.ru/api/v1/opds # atom+xml, без Content-Encoding
|
||||||
|
curl -sS -D- -o /dev/null https://books.ada-dev.ru/api/v1/opds/search.opds
|
||||||
|
curl -sS https://books.ada-dev.ru/api/v1/healthcheck
|
||||||
|
|
||||||
|
Затем в KOReader: добавить каталог, увидеть список полок, скачать книгу
|
||||||
|
целиком, проверить синхронизацию прогресса.
|
||||||
|
|
||||||
|
**8. `adguard` (144 → 158).** ПЕРЕЕХАЛ 2026-09-03. OLD 144 остановлен (откат
|
||||||
|
≥ неделя, до ~2026-09-10). Данные `/opt/adguard/{conf,work}` (~313 МБ,
|
||||||
|
`conf/AdGuardHome.yaml` несёт хэш пароля, DNS rewrites, upstream, клиентов)
|
||||||
|
перенесены целиком через `pct exec … tar` (оба контейнера на mini-pc).
|
||||||
|
|
||||||
|
- **Свежий AdGuard на пустом `conf/` не проходит health-гейт плейбука.** Он
|
||||||
|
уходит в setup-wizard: порт 3000 отдаёт 302, порт 80 — connection reset, а
|
||||||
|
`pve-adguard.yml` ждёт `http://127.0.0.1/` `[200,302]` и падает после 24
|
||||||
|
ретраев. Поэтому для adguard данные пред-заливаются ДО шага 4.3 (config play),
|
||||||
|
а не после. После пред-заливки прогон идемпотентен (`ok=13 changed=0`),
|
||||||
|
фильтрация подтверждена: `doubleclick.net → 0.0.0.0`.
|
||||||
|
- Ноды используют DNS роутера (192.168.1.1), контейнеры — 1.1.1.1 (из
|
||||||
|
`pct config` `nameserver`). Провал .28 задел только DHCP-клиентов LAN и
|
||||||
|
рабочую станцию (у неё fallback на роутер). Окно ~2 мин, ночью. Роутер не
|
||||||
|
трогали.
|
||||||
|
- Сокеты wildcard (`0.0.0.0`), повторный прогон после смены адреса не нужен.
|
||||||
|
|
||||||
|
**9. `mihomo` (143 → 159).** ПЕРЕЕХАЛ 2026-09-03. OLD 143 остановлен (откат
|
||||||
|
≥ неделя). Первый боевой сервис с **двумя механизмами проброса устройств**:
|
||||||
|
`/dev/fuse` через `features.fuse`, `/dev/net/tun` через блок
|
||||||
|
`device_passthrough` (`dev0: path=/dev/net/tun,mode=0660`) — оба под root@pam,
|
||||||
|
проверены здесь на живом сервисе (до этого `device_passthrough` был только на
|
||||||
|
пилоте).
|
||||||
|
|
||||||
|
- Данные `/opt/mihomo` (~80 КБ): `config/config.yaml`, `config/cache.db`,
|
||||||
|
`config/providers/main.yaml`. `config.yaml` содержит URL подписки прокси-
|
||||||
|
провайдера — секрет, копируется `pct exec`, в репозиторий не попадает.
|
||||||
|
Задача «Install default mihomo config if missing» идёт с `force: false`,
|
||||||
|
перенесённый конфиг не перетирается.
|
||||||
|
- Все сокеты wildcard (`0.0.0.0`) — повторный прогон после смены адреса не
|
||||||
|
нужен. Прокси реально проверен: `curl -x .27:7890 …/generate_204 → 204`.
|
||||||
|
- `ru-vps-mihomo-harden.yml` повторного прогона НЕ требует: он таргетит
|
||||||
|
`hosts: ru-vps`, работает со скриптом `/usr/local/sbin/ru-vps-mihomo-harden`
|
||||||
|
на ru-vps, адрес mihomo нигде в нём не зашит, и он за `CONFIRM`-гейтом.
|
||||||
|
- Зависимые (`bash_config_proxy_*` у hermes-ai, `emergency_telegram_proxy`,
|
||||||
|
`uptime_kuma_http_proxy`, правило gyro-фаервола `OUT ACCEPT 192.168.1.27:7890`,
|
||||||
|
`prometheus.yml.j2`) все ссылаются на .27 — адрес сохранён, правок не нужно.
|
||||||
|
|
||||||
|
**10. `ovpn-mini` (132 → 160).** ПЕРЕЕХАЛ 2026-09-03 из локальной сети. OLD 132
|
||||||
|
остановлен (откат ≥ неделя). `/dev/net/tun` через `device_passthrough`.
|
||||||
|
`features` — только `nesting` (как в эталоне). Конфигурации в `pve-ovpn-mini.yml`
|
||||||
|
нет — шлюз настраивает `openvpn-vps-mini.yml` (роль `openvpn_gateway`, группа
|
||||||
|
`vpn_openvpn`). Единственные данные — `/etc/openvpn/homelab/static.key`, общий
|
||||||
|
с ru-vps; LAN-адрес ovpn-mini ни в одном шаблоне роли не фигурирует
|
||||||
|
(masquerade по `-o eth0`), поэтому туннель не зависит от смены адреса.
|
||||||
|
|
||||||
|
- **Петля транспорта при cutover.** `-F ansible/ssh_config` до нод PVE идёт
|
||||||
|
ProxyJump через ru-vps, а ru-vps достаёт LAN ЧЕРЕЗ туннель, который
|
||||||
|
терминирует ovpn-mini. `make tofu-*` строит SSH-туннель к PVE API тем же
|
||||||
|
путём. `pct stop 132` кладёт туннель → `make tofu-apply` больше не достаёт
|
||||||
|
API. Разрыв: сначала поднять шлюз на НОВОМ контейнере ещё на TMPIP
|
||||||
|
(одноразовый плейбук `hosts: ru-vps` slurp ключа + `hosts: <name>-new` роль
|
||||||
|
`openvpn_gateway`), туннель встаёт с TMPIP-адреса → `make tofu-apply` снова
|
||||||
|
работает → сменить адрес на боевой → повторный `openvpn-vps-mini.yml`
|
||||||
|
(идемпотентен, `changed=0` на обоих концах). Прямой доступ к нодам во время
|
||||||
|
провала — `ssh -o ProxyJump=none <ansible-user>@192.168.1.{5,10}` из LAN.
|
||||||
|
- **`ssh_config`: у `ovpn-mini` теперь `ProxyJump none`** (в индивидуальном
|
||||||
|
Host-блоке, побеждает по «первое значение опции»). Путь через ru-vps
|
||||||
|
закольцовывался бы на его же туннель. Из LAN хост доступен напрямую; вне
|
||||||
|
LAN управление — консоль ноды (`pct exec`) или заранее поднятый туннель.
|
||||||
|
- **НЕ запускать OLD 132 как диагностику.** Его `pct config` всё ещё держит
|
||||||
|
`ip=192.168.1.23/24`; параллельный старт с CT 160 даёт конфликт .23 и роняет
|
||||||
|
туннель на 1-2 мин (проверено случайно 2026-09-03, восстановилось само).
|
||||||
|
- `make openvpn-check` — 7/7 ok. Кворум кластера не затронут (qdevice ходит
|
||||||
|
напрямую нода → ru-vps:5403, не через туннель).
|
||||||
|
- Косметика: `polkit.service` на CT 160 в `failed` (`status=217/USER` —
|
||||||
|
минимальный LXC-шаблон без нужного окружения polkit). На OpenVPN не влияет,
|
||||||
|
не чинилось.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. После завершения всех переездов
|
||||||
|
|
||||||
|
- Удалить `roles/pve_lxc` — он станет не нужен.
|
||||||
|
- Убрать из `services.yml` поле `provisioner` со значениями `pct_ssh`/`pve_lxc`
|
||||||
|
либо заменить на `tofu`.
|
||||||
|
- Обновить `docs/ai/architecture.md`: раздел «Provisioning Flow» описывает два
|
||||||
|
пути через `pct` и API — оба исчезнут.
|
||||||
|
- Обновить `legacy-warning.md`: пункт про два provisioner-пути потеряет смысл.
|
||||||
|
- Расширить `validate.yml`: сейчас он сверяет hostname, IP, cores, memory, swap.
|
||||||
|
После миграции имеет смысл добавить features, устройства и mount points —
|
||||||
|
ровно те поля, которыми теперь управляет Tofu.
|
||||||
|
- Решить, нужен ли `roles/lxc_docker_host` — он до сих пор ни к чему не
|
||||||
|
подключён.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Известные проблемы вне миграции
|
||||||
|
|
||||||
|
Не блокируют переезд, но про них надо знать.
|
||||||
|
|
||||||
|
- **`hermes-ai` (147) недостижим по SSH.** Прозрачный прокси заворачивает в
|
||||||
|
`hermes-tun` всё, что не пришло с `lo`, включая ответные пакеты входящих
|
||||||
|
соединений (`ip rule` 9002). Ansible до хоста не достучится, управление —
|
||||||
|
только через `pct exec`. Сервис заморожен, `make check` показывает его DOWN
|
||||||
|
ожидаемо. Мигрировать не раньше, чем починится сеть.
|
||||||
|
- **`resticprofile-check@profile-default` на ru-vps падает еженедельно**:
|
||||||
|
профиль `default` без репозитория. Реальный `profile-services` работает.
|
||||||
|
Шум в секции FAILED отчёта `make status`.
|
||||||
|
- **Gitea runner** зарегистрирован и опрашивает Gitea, но метка `ru-vps` не
|
||||||
|
совпадает с `runs-on: ubuntu-latest` в workflow, поэтому CI не выполняется.
|
||||||
|
Раннер монтирует `/var/run/docker.sock` и `/opt/services` на запись — перед
|
||||||
|
включением CI это стоит пересмотреть.
|
||||||
|
- **`homelab_pve_egress_ip` динамический.** При смене домашнего адреса qdevice
|
||||||
|
замолчит. Видно в секции CLUSTER QUORUM отчёта `make status`.
|
||||||
|
- **`bootstrap-pve-api-token.yml` перезаписывает корневой `.env` целиком** —
|
||||||
|
вместе с `PROXMOX_ROOT_PASSWORD`, `MONITORING_*` и `EMERGENCY_*`. У задачи
|
||||||
|
есть `backup: true`, но восстанавливать придётся руками.
|
||||||
|
- Устаревшие правила UFW на ru-vps (`3128`, `1080`, `993`, `7892`) — за
|
||||||
|
переключателем `zt_cleanup_unrelated_stale_rules` в
|
||||||
|
`playbooks/ru-vps-zerotier-decommission.yml`, по умолчанию выключен.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Откат
|
||||||
|
|
||||||
|
**До шага 4.5.14** (пока старый контейнер работает): просто не переключаться.
|
||||||
|
Удалить новый контейнер `make tofu-destroy CONFIRM=1` или точечно.
|
||||||
|
|
||||||
|
**После переключения, но до `pct destroy`:** остановить новый, вернуть адрес
|
||||||
|
старому не нужно — он не менялся, `pct start OLD` возвращает всё как было.
|
||||||
|
Затем откатить `vmid` в `services.yml` и прогнать `make backup-jobs`,
|
||||||
|
`make backup-audit`, `make validate`.
|
||||||
|
|
||||||
|
**После `pct destroy OLD`:** только восстановление из PBS. Именно поэтому
|
||||||
|
шаг 4.1.3 (свежий бэкап) обязателен, а шаг 4.7.25 (неделя ожидания) не
|
||||||
|
сокращается.
|
||||||
+546
@@ -0,0 +1,546 @@
|
|||||||
|
# План работ
|
||||||
|
|
||||||
|
## Активные задачи
|
||||||
|
|
||||||
|
### Дашборд-обзор инфраструктуры (Homepage)
|
||||||
|
|
||||||
|
Решение от 2026-09-03: поднять стартовую страницу-обзор всей инфры (список
|
||||||
|
сервисов + переходы + виджеты Proxmox/Uptime Kuma).
|
||||||
|
|
||||||
|
Выбор: **Homepage** (gethomepage.dev) вторым compose-стеком на LXC
|
||||||
|
`monitoring` (CT 155) рядом с Uptime Kuma; доступ только LAN/OpenVPN, наружу
|
||||||
|
не публикуется. Конфиг Homepage **генерируется из `homelab_services`** —
|
||||||
|
`playbooks/dashboard.yml` + шаблоны `playbooks/templates/homepage-*.j2`,
|
||||||
|
новый потребитель реестра. Плитка на сервис: `href` = `https://<proxy.domain>`
|
||||||
|
для публичных, иначе `http(s)://<ip>:<порт>`; беспортовые — в группу Headless
|
||||||
|
с ICMP ping.
|
||||||
|
|
||||||
|
**Развёрнуто на CT 155 2026-09-03.** Контейнер `homelab-homepage` (`Up healthy`),
|
||||||
|
`homelab-homepage.service` enabled, `http://192.168.1.30:8082/` -> 200. Прогон
|
||||||
|
`make dashboard` идемпотентен (третий заход `changed=0`), `make validate` и
|
||||||
|
`make lint` зелёные. Образ закреплён по digest
|
||||||
|
`sha256:753eeb0c…1e1bdd`. Порт 8082 (3000 в модели реестра занят замороженной
|
||||||
|
Grafana, 3001 — Kuma) -> контейнерный 3000.
|
||||||
|
|
||||||
|
Состав:
|
||||||
|
|
||||||
|
- `playbooks/dashboard.yml`, шаблоны `homepage-{settings,services,widgets,bookmarks}.yaml.j2`,
|
||||||
|
`homepage-compose.yml.j2`, `homepage.env.j2`;
|
||||||
|
- `playbooks/bootstrap-dashboard-pve-token.yml` — read-only токен
|
||||||
|
`homepage@pve!dashboard` (роль `PVEAuditor`), пишет `DASHBOARD_PVE_*` в корневой `.env`;
|
||||||
|
- реестр: блок `homelab_dashboard_*` в `group_vars/all/services.yml`, запись в
|
||||||
|
списках потребителей (`services.yml`, `AGENTS.md`, `architecture.md`);
|
||||||
|
- `Makefile`: цели `dashboard`, `dry-dashboard`, `bootstrap-dashboard-token`.
|
||||||
|
|
||||||
|
Оставшиеся шаги оператора:
|
||||||
|
|
||||||
|
1. `make bootstrap-dashboard-token` — выпустить PVE-токен для виджета Proxmox.
|
||||||
|
(В сессии-агенте был заблокирован auto-mode-классификатором как создание
|
||||||
|
credential; репозиторная часть готова, плейбук идемпотентен и сам допишет
|
||||||
|
`DASHBOARD_PVE_*` в корневой `.env`.) После — `make dashboard` заново, чтобы
|
||||||
|
`homepage.env` подхватил секрет. До этого плитка «Proxmox кластер» показывает
|
||||||
|
ошибку авторизации — не блокер.
|
||||||
|
2. Добавить те же три ключа-заглушки в `.env.example` (в сессии путь `.env*`
|
||||||
|
заблокирован в permissions).
|
||||||
|
3. В UI Uptime Kuma опубликовать Status Page со slug `homelab` — иначе виджет
|
||||||
|
сводки up/down показывает ошибку. Не блокер.
|
||||||
|
|
||||||
|
### Вывод memoir-bot (CT 142)
|
||||||
|
|
||||||
|
Решение от 2026-09-02: сервис не используется и выводится из эксплуатации.
|
||||||
|
|
||||||
|
Repository-часть выполнена: удалены playbook, host, запись реестра, VMID из backup
|
||||||
|
job и backup audit, SSH host-блок, проверки status, Prometheus target и no_proxy
|
||||||
|
Uptime Kuma. Оговорка "Memoir Bot строится локально" убрана из документации -
|
||||||
|
теперь единственные исключения из digest pinning это frozen Prometheus и docker-test.
|
||||||
|
|
||||||
|
Live-шаги оператора выполнены 2026-09-02: deploy key `SecondBrain` отозван,
|
||||||
|
monitor в Uptime Kuma снят, `pct stop 142` выполнен (подтверждено `pct list` на
|
||||||
|
mini-pc). Это заодно репетиция удаления старого контейнера перед первым
|
||||||
|
blue-green переездом (`migration-tofu.md`).
|
||||||
|
|
||||||
|
Оставшиеся шаги:
|
||||||
|
|
||||||
|
1. Выдержать паузу (по аналогии с общим инвариантом blue-green - минимум
|
||||||
|
неделю), затем `pct destroy 142`.
|
||||||
|
2. Решить судьбу цепочки бэкапов VMID 142 в PBS.
|
||||||
|
3. Не переиспользовать VMID 142 и `192.168.1.26` сразу.
|
||||||
|
|
||||||
|
### Регрессия переезда: update-плейбуки зашивают старые VMID
|
||||||
|
|
||||||
|
Обнаружено 2026-09-02 сразу после cutover.
|
||||||
|
|
||||||
|
`playbooks/vaultwarden-update.yml` и `playbooks/grimmory-update.yml` теперь берут
|
||||||
|
backup VMID из registry, а не из жёстко прошитых чисел. Это закрывает старую
|
||||||
|
регрессию со «страховочным бэкапом» на остановленном контейнере.
|
||||||
|
|
||||||
|
Аудит также нашёл и исправил две проблемы в Gitea: небезопасный импорт legacy
|
||||||
|
provisioning без явного выключателя и запуск `homelab-restic-offsite-gitea` на
|
||||||
|
`cloud-pc` вместо `gitea` (профиль теперь живёт в CT 153). Теперь consumer status
|
||||||
|
следует реальному backup unit на `gitea`, а `homelab-backup-audit-gitea` остаётся
|
||||||
|
на `cloud-pc`.
|
||||||
|
|
||||||
|
Обновлено 2026-09-03: `adguard-update.yml` и `mihomo-update.yml` изначально
|
||||||
|
читают VMID из реестра (`homelab_services['<svc>'].vmid`), поэтому после переезда
|
||||||
|
adguard→158 и mihomo→159 они автоматически указывают на новые контейнеры —
|
||||||
|
правок не потребовалось.
|
||||||
|
|
||||||
|
`make update-vaultwarden`, `make update-grimmory`, `make update-gitea`,
|
||||||
|
`make update-adguard`, `make update-mihomo` и `make update-all` не запускались
|
||||||
|
во время проверки.
|
||||||
|
|
||||||
|
### Task 3 — PBS storage-level prune removed declaratively
|
||||||
|
|
||||||
|
Выполнено 2026-09-02: storage-level `prune-backups` на PVE storage `pbs` удалён
|
||||||
|
декларативно, retention authority остался в PBS `prune-pbs`.
|
||||||
|
|
||||||
|
Локальное недельное PBS-container backup на storage `backup` с
|
||||||
|
`keep-last=2` — намеренное исключение и не трогалось.
|
||||||
|
|
||||||
|
### Кворум кластера: qdevice не голосует
|
||||||
|
|
||||||
|
Обнаружено 2026-09-02. `pvecm status`: `Expected votes: 3`, `Total votes: 2`,
|
||||||
|
флаги узлов `A,NV,NMW` (NV = Not-Voted), `corosync-qdevice: Connect timeout`.
|
||||||
|
|
||||||
|
Причина: `corosync.conf` указывает арбитр по публичному адресу ru-vps
|
||||||
|
(`host: 157.22.231.198`), а UFW пускал 5403/tcp только из `10.122.62.0/24` —
|
||||||
|
сети ZeroTier, выведенной в июле 2026. Арбитр отвалился молча.
|
||||||
|
|
||||||
|
Последствие: у двухнодового кластера нет третьего голоса. Кворум держится лишь
|
||||||
|
пока живы обе ноды; отказ любой из них оставляет выжившую с `1 < 2`.
|
||||||
|
|
||||||
|
Решение: чинить ПРЯМОЙ путь нода -> ru-vps:5403, а не заворачивать арбитр в
|
||||||
|
OpenVPN. Туннель терминируется в `ovpn-mini` (CT 132 на mini-pc), поэтому при
|
||||||
|
падении mini-pc арбитр исчез бы вместе с ним — защищён был бы только отказ
|
||||||
|
cloud-pc. Плюс `ovpn-mini` стоит в плане на blue-green переезд, и его
|
||||||
|
пересоздание роняло бы кворум.
|
||||||
|
|
||||||
|
Сделано: `homelab_pve_egress_ip` в `group_vars/all/main.yml`, правила UFW в
|
||||||
|
`playbooks/ru-vps-base.yml` (открыть 5403 с этого адреса, удалить правило для
|
||||||
|
`10.122.62.0/24`), проверка кворума добавлена в `playbooks/status.yml` — секция
|
||||||
|
CLUSTER QUORUM, чтобы повторный отказ не был снова молчаливым.
|
||||||
|
|
||||||
|
ВЫПОЛНЕНО 2026-09-02. После прогона `make ru-vps-base`: `Total votes: 3`,
|
||||||
|
флаги узлов сменились с `A,NV,NMW` на `A,V,NMW`, qdevice отдаёт голос.
|
||||||
|
Кластер снова имеет арбитра.
|
||||||
|
|
||||||
|
Открытым остаётся динамический `homelab_pve_egress_ip`: адрес зафиксирован
|
||||||
|
статически, при его смене qdevice снова замолчит. Отличие от прошлого раза в
|
||||||
|
том, что теперь это видно в секции CLUSTER QUORUM отчёта `make status`.
|
||||||
|
|
||||||
|
### Вывод ZeroTier с ru-vps
|
||||||
|
|
||||||
|
Решение от 2026-09-02: выводить полностью. Проверено — ZT-интерфейса на хосте
|
||||||
|
нет, маршрутов через него нет, на ZT-адресах никто не слушает; контейнер
|
||||||
|
`zerotier` подключён в никуда. Мёртв и `ssh-zt22.service` (sshd на порту 22 для
|
||||||
|
setup qdevice) — порт 22 не слушает никто.
|
||||||
|
|
||||||
|
ВЫПОЛНЕНО 2026-09-02 через `playbooks/ru-vps-zerotier-decommission.yml`
|
||||||
|
(`make zerotier-decommission CONFIRM=1`). Контейнер снят, `ssh-zt22.service`
|
||||||
|
отключён, правила UFW для интерфейса `zt6q3dmi2d`, `9993/udp`, `9001` и подсети
|
||||||
|
`10.122.62.0/24` удалены. Сброшено состояние failed у `ssh-zt22.service` и
|
||||||
|
фантомного `homelab-pve-routes.service`. Прогон идемпотентен, публичные сервисы
|
||||||
|
и кворум не пострадали.
|
||||||
|
|
||||||
|
Намеренно НЕ удалено: каталог `/opt/services/ru-vps/zerotier`, данные
|
||||||
|
`/opt/data/zerotier`, файл `/etc/ssh/sshd_config_zt22`. `docker compose down`
|
||||||
|
выполняется без `-v`, identity узла сохранена. Удаление — отдельный шаг.
|
||||||
|
|
||||||
|
Правила UFW для `3128/tcp` (squid не запущен), `1080/tcp` (danted слушает 1081),
|
||||||
|
`993/tcp` и `7892/tcp` оставлены: они не наследие ZeroTier. В плейбуке есть
|
||||||
|
переключатель `zt_cleanup_unrelated_stale_rules`, по умолчанию выключен.
|
||||||
|
|
||||||
|
### resticprofile-check@profile-default падает еженедельно
|
||||||
|
|
||||||
|
Обнаружено 2026-09-02 при чистке упавших юнитов на ru-vps.
|
||||||
|
|
||||||
|
Fatal: Please specify repository location (-r or --repository-file)
|
||||||
|
check on profile 'default': exit status 1
|
||||||
|
|
||||||
|
Профиль `default` в `/opt/services/ru-vps/resticprofile/profiles.toml` не имеет
|
||||||
|
репозитория — это профиль-заготовка, для которого не должно быть таймера
|
||||||
|
проверки. Реальный `profile-services` бэкапится и проверяется штатно, так что
|
||||||
|
это шум, а не потеря бэкапов.
|
||||||
|
|
||||||
|
Значение: юнит постоянно висит в секции FAILED SYSTEMD UNITS отчёта
|
||||||
|
`make status` и притупляет внимание к настоящим отказам. Стек resticprofile на
|
||||||
|
ru-vps в Ansible не описан, поэтому чинится либо вручную
|
||||||
|
(`systemctl disable --now resticprofile-check@profile-default.timer`), либо
|
||||||
|
вместе со взятием стека под управление.
|
||||||
|
|
||||||
|
Прочие упавшие юниты на ru-vps — `ifup@eth0.service` и `networking.service`;
|
||||||
|
не разбирались, хост при этом полностью работоспособен.
|
||||||
|
|
||||||
|
### Gitea Actions: документация устарела
|
||||||
|
|
||||||
|
Проверено 2026-09-02. Контейнер `gitea-runner-gitea-runner-1` на ru-vps работает
|
||||||
|
и опрашивает Gitea, то есть раннер ЗАРЕГИСТРИРОВАН. Утверждения в
|
||||||
|
`.gitea/workflows/lint.yml` и `tech-stack.md` об обратном неверны.
|
||||||
|
|
||||||
|
CI при этом всё равно не выполняется: у раннера метка `ru-vps`, а workflow
|
||||||
|
требует `runs-on: ubuntu-latest`. Отдельный вопрос перед включением CI: раннер
|
||||||
|
монтирует `/var/run/docker.sock` и `/opt/services` на запись, то есть любой
|
||||||
|
workflow получает root над публичной VPS.
|
||||||
|
|
||||||
|
### hermes-ai недостижим по SSH: прозрачный прокси съедает обратный путь
|
||||||
|
|
||||||
|
Обнаружено 2026-09-02. CT 147 запущен, sshd слушает `*:22`, UFW разрешает 22/tcp
|
||||||
|
из `192.168.1.0/24` и `10.78.0.0/30`, но хост не отвечает ни с ru-vps, ни с
|
||||||
|
cloud-pc (то есть и из самой LAN). `make check` показывает его unreachable.
|
||||||
|
|
||||||
|
Причина видна в `ip rule` внутри контейнера:
|
||||||
|
|
||||||
|
9002: not from all iif lo lookup 2022
|
||||||
|
|
||||||
|
Правило заворачивает в таблицу прозрачного прокси (`hermes-tun`) всё, что не
|
||||||
|
пришло с `lo`, включая ответные пакеты входящих соединений. SYN доходит, SYN-ACK
|
||||||
|
уходит в туннель — соединение не устанавливается. Исключения для трафика,
|
||||||
|
пришедшего с eth0, в конфигурации нет.
|
||||||
|
|
||||||
|
Побочное следствие: `playbooks/pve-hermes-ai.yml` больше не может отработать —
|
||||||
|
Ansible не достучится до хоста. Управлять контейнером можно только через
|
||||||
|
`pct exec` с cloud-pc. Плейбук, судя по всему, отработал один раз и запер себя:
|
||||||
|
включение прокси не рвёт уже установленную сессию, только новые.
|
||||||
|
|
||||||
|
Не чиню: сервис признан малополезным и заморожен (см. ниже). Но `make check` и
|
||||||
|
`make status` будут показывать его DOWN, и это ожидаемо, а не новая поломка.
|
||||||
|
|
||||||
|
### Hermes AI (CT 147) - осознанно заморожен
|
||||||
|
|
||||||
|
Решение от 2026-09-02: сервис признан малополезным, но контейнер остается.
|
||||||
|
`playbooks/pve-hermes-ai.yml` разворачивает runtime, Docker и transparent TUN proxy
|
||||||
|
через mihomo, но не разворачивает приложение Hermes - это не недоделка, а принятое
|
||||||
|
состояние. Не предлагать "дописать деплой Hermes" как opportunistic cleanup.
|
||||||
|
|
||||||
|
### Секреты переехали в корень репозитория
|
||||||
|
|
||||||
|
Решение от 2026-09-02. `.env` и `.env.example` перенесены из `ansible/` в корень:
|
||||||
|
их потребляет не только Ansible, но и OpenTofu, а держать два файла или ходить в
|
||||||
|
соседний подкаталог неудобно.
|
||||||
|
|
||||||
|
`ENV_FILE` в `ansible/Makefile` теперь абсолютный (`$(REPO_ROOT)/.env`), поэтому
|
||||||
|
цели работают из любого cwd. Переименование `.env.example` заведено в индекс git,
|
||||||
|
чтобы файл не потерялся при коммите. Оба пути покрыты `.gitignore`.
|
||||||
|
|
||||||
|
Принято решение хранить пароль `root@pam` в `.env` (а не спрашивать его при
|
||||||
|
запуске). Компромисс осознанный: пользователь `ansible` и так имеет passwordless
|
||||||
|
sudo на нодах, то есть эффективный root в автоматизации уже был; пароль добавляет
|
||||||
|
не новый класс доступа, а секрет с худшими свойствами — он же логин в веб-интерфейс
|
||||||
|
и консоль, его нельзя ограничить по scope и нельзя отозвать иначе, чем сменив
|
||||||
|
пароль root на нодах.
|
||||||
|
|
||||||
|
Переменные: `PROXMOX_ROOT_USER` (по умолчанию `root@pam`) и
|
||||||
|
`PROXMOX_ROOT_PASSWORD`. Их читают ТОЛЬКО цели `tofu-*`; Ansible ими не
|
||||||
|
пользуется. Если пароль не задан или равен `replace-me`, Tofu идёт токеном
|
||||||
|
`ansible@pve`. Выбранный режим печатается в stderr перед запуском.
|
||||||
|
|
||||||
|
### Пилот OpenTofu: граница возможностей API-токена
|
||||||
|
|
||||||
|
Выполнено 2026-09-02. Каталог `tofu/`, цели `make tofu-*`, подробности и матрица
|
||||||
|
возможностей — в [`../../tofu/README.md`](../../tofu/README.md).
|
||||||
|
|
||||||
|
Кратко, проверено на живом кластере контейнером VMID 199:
|
||||||
|
|
||||||
|
- API-токен создаёт LXC со всеми ресурсами, сетью, rootfs, `nesting`, startup и
|
||||||
|
тегами; `plan` идемпотентен.
|
||||||
|
- **Mount point как volume на datastore токеном создаётся.** Это снимает вопрос
|
||||||
|
по `mp0` у gitea: bind mount каталога хоста требует root@pam, а volume — нет.
|
||||||
|
- `keyctl`, `fuse`, `mount` и `device_passthrough` требуют root@pam (HTTP 403).
|
||||||
|
Это ограничение Proxmox, а не Tofu: Ansible упирается в то же самое, поэтому в
|
||||||
|
репозитории уже есть `pct set --features` по SSH и правка
|
||||||
|
`/etc/pve/lxc/<vmid>.conf`.
|
||||||
|
- Гибрид Tofu+Ansible требует `lifecycle { ignore_changes = [features] }`, иначе
|
||||||
|
Tofu откатывает выставленный извне `keyctl` и ломает Docker в контейнере.
|
||||||
|
- Tofu не умеет ProxyJump: вне LAN нужен SSH-туннель, он встроен в цели `tofu-*`.
|
||||||
|
|
||||||
|
РЕЖИМ ВЫБРАН 2026-09-02: root@pam по паролю. Проверено на пилоте — `dev0`,
|
||||||
|
`features: fuse=1,keyctl=1,nesting=1` и volume mount point выставляются
|
||||||
|
декларативно, повторный `plan` даёт `No changes`. Значит при переезде каждого
|
||||||
|
сервиса из его `pve-*.yml` можно убирать `pct set --features` и правку
|
||||||
|
`/etc/pve/lxc/<vmid>.conf`; `ignore_changes` не нужен.
|
||||||
|
|
||||||
|
Токен, принадлежащий root@pam, ограничение НЕ обходит: `$authuser` при токенной
|
||||||
|
аутентификации равен полному `user@realm!tokenname` (`PVE/HTTPServer.pm:86`), а
|
||||||
|
проверка в `PVE/LXC.pm:1658` сравнивает строку с `root@pam` буквально. Права
|
||||||
|
токену добавлять бесполезно. Единственная альтернатива гибриду — пароль root@pam,
|
||||||
|
то есть системный пароль root узлов Proxmox.
|
||||||
|
|
||||||
|
Гибридная схема (Tofu создаёт, Ansible доводит по SSH) отвергнута в пользу
|
||||||
|
полной декларативности. Она остаётся запасным вариантом, если пароль root
|
||||||
|
решат из `.env` убрать.
|
||||||
|
|
||||||
|
Пилотный контейнер VMID 199 `tofu-pilot` снесён (подтверждено 2026-09-02:
|
||||||
|
`pct list` на cloud-pc его не показывает).
|
||||||
|
|
||||||
|
## Переход на OpenTofu
|
||||||
|
|
||||||
|
Пошаговый план миграции provisioning на Tofu вынесен в отдельный документ:
|
||||||
|
[`migration-tofu.md`](migration-tofu.md). Там же порядок сервисов, специфика
|
||||||
|
каждого и процедура отката.
|
||||||
|
|
||||||
|
Сервис №1 `emergency-bot` переехал 2026-09-02: OLD (VMID 148) остановлен
|
||||||
|
(откат минимум неделю, затем `pct destroy`), боевой — NEW (VMID 151,
|
||||||
|
`tofu/services.tf`), адрес не менялся (192.168.1.32). Реестр обновлён
|
||||||
|
(`vmid: 151`, `provisioner: tofu`), `make validate` и `make status` зелёные.
|
||||||
|
Две находки, важные для следующих сервисов, задокументированы в
|
||||||
|
`migration-tofu.md` (раздел 5.1) и `tofu/README.md`: конфигурационная часть
|
||||||
|
сервиса может жить в отдельном playbook с play, таргетящими другие хосты (не
|
||||||
|
подходит под буквальный `--limit <name>-new`), и провайдер отслеживает
|
||||||
|
`cmode` через блок `console`, который нужно объявлять явно с самого начала.
|
||||||
|
|
||||||
|
### Пакетный переезд сервисов 2-7 (параллельно)
|
||||||
|
|
||||||
|
Решение от 2026-09-02: последовательный переезд по одному сервису слишком
|
||||||
|
медленный. Инвариант «один сервис за раз» уточнён в `migration-tofu.md`:
|
||||||
|
последовательным обязан быть только cutover, а подготовка, создание и
|
||||||
|
конфигурация на временных адресах параллелятся безопасно.
|
||||||
|
|
||||||
|
**Создано и проверено 2026-09-02.** Один пакетный `tofu apply` (`6 to add,
|
||||||
|
0 to change, 0 to destroy` — уже переехавший emergency-bot дрейфа не дал):
|
||||||
|
|
||||||
|
| Сервис | OLD | NEW | Узел | TMPIP |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| docker-test | 145 | 152 | cloud-pc | 192.168.1.11 |
|
||||||
|
| gitea | 141 | 153 | cloud-pc | 192.168.1.12 |
|
||||||
|
| vaultwarden | 140 | 154 | mini-pc | 192.168.1.13 |
|
||||||
|
| monitoring | 146 | 155 | cloud-pc | 192.168.1.14 |
|
||||||
|
| gyro | 150 | 156 | mini-pc | 192.168.1.15 |
|
||||||
|
| grimmory | 149 | 157 | cloud-pc | 192.168.1.16 |
|
||||||
|
|
||||||
|
`pct config` всех шести сверен с эталонами: cpu, память, swap, диск, features,
|
||||||
|
startup, `cmode: shell` совпали. Запас на нодах после создания: cloud-pc
|
||||||
|
12.3 ГиБ RAM и 849 ГиБ на `data`, mini-pc 8.8 ГиБ RAM и 117 ГиБ на `local-lvm`.
|
||||||
|
`make validate` зелёный до и после — новые контейнеры в реестре пока не
|
||||||
|
числятся, это ожидаемо до шага 4.6.
|
||||||
|
|
||||||
|
Что подтвердилось на живых контейнерах впервые:
|
||||||
|
|
||||||
|
- **`/dev/fuse` через `features.fuse` эквивалентен старому обходу.** Устройство
|
||||||
|
присутствует в 152, 153, 154, 157 и отсутствует в 156 (у gyro его и не
|
||||||
|
должно быть). Это снимает вопрос, который до сих пор был помечен как
|
||||||
|
непроверенный: пилот проверял `device_passthrough` только на `/dev/net/tun`.
|
||||||
|
- **Bind mount gitea заменён на volume декларативно:**
|
||||||
|
`mp0: data:153/vm-153-disk-1.raw,mp=/opt/gitea/data,size=32G`. Ради этого
|
||||||
|
миграция и затевалась.
|
||||||
|
- **Пустой `features` у gyro сохраняется.** В `pct config 156` строки
|
||||||
|
`features` нет вообще, `nesting` не подкрался.
|
||||||
|
|
||||||
|
Две ловушки, найденные при этом заходе и почищенные в коде:
|
||||||
|
|
||||||
|
1. **`--limit <name>-new` не перенацеливает play, а обнуляет его** — и это
|
||||||
|
общее свойство, а не особенность emergency-bot, как считалось раньше.
|
||||||
|
Проверено `--list-hosts`: ноль хостов во ВСЕХ play всех плейбуков.
|
||||||
|
Заведён переопределяемый таргет `pve_config_target`, поведение по
|
||||||
|
умолчанию не изменилось. Подробности — `migration-tofu.md`, шаг 4.3.9.
|
||||||
|
2. **Inline `vars:` у группы в inventory-файле проигрывают `group_vars/all/`.**
|
||||||
|
Из-за этого Ansible ходил в новые контейнеры под `ansible@` вместо `root`
|
||||||
|
и получал `Permission denied`. Транспорт вынесен в
|
||||||
|
`group_vars/lxc_migration_new/main.yml`.
|
||||||
|
|
||||||
|
Попутно `roles/uptime_kuma` научился не падать на хосте, где замороженного
|
||||||
|
юнита `homelab-monitoring` никогда не было: заморозка теперь выполняется
|
||||||
|
только при его наличии.
|
||||||
|
|
||||||
|
Ещё один потребитель, которого не было в плане: `playbooks/vaultwarden-update.yml`
|
||||||
|
жёстко зашивает VMID 140, включая `vzdump "140"`. Его нужно поправить на шаге
|
||||||
|
4.6 вместе с реестром.
|
||||||
|
|
||||||
|
**Конфигурация на временных адресах (шаг 4.3) выполнена для пяти сервисов.**
|
||||||
|
Все прогоны идемпотентны — второй заход даёт `changed=0`. Боевые контейнеры не
|
||||||
|
затронуты: проверено после прогонов, на каждом боевом хосте активен ровно свой
|
||||||
|
сервис.
|
||||||
|
|
||||||
|
| Сервис | Итог | Проверка |
|
||||||
|
|---|---|---|
|
||||||
|
| docker-test 152 | OK | Docker active, storage-driver `fuse-overlayfs`, `hello-world` проходит, версия Docker совпала с боевой |
|
||||||
|
| gitea 153 | OK | HTTP 200, порт 2222 слушает, volume — отдельная ФС `/dev/loop10` ext4 32 ГиБ |
|
||||||
|
| vaultwarden 154 | OK | HTTP 200, контейнер healthy, база свежая 278 КБ без `icon_cache` |
|
||||||
|
| monitoring 155 | OK | HTTP 302→200, `kuma.db` создан на месте, `/opt/monitoring` отсутствует |
|
||||||
|
| grimmory 157 | OK после починки | оба юнита active, health 200, Flyway 144 — как на боевом |
|
||||||
|
| gyro 156 | OK | конфигурация и cutover завершены 2026-09-02 |
|
||||||
|
|
||||||
|
Digest'ы образов на всех пяти совпали с боевыми побайтово.
|
||||||
|
|
||||||
|
**Баг в `pve-grimmory.yml`, найденный при этом.** Боевой адрес был зашит в
|
||||||
|
конфигурационном play трижды: в `ports` compose, в правилах `GRIMMORY-FILTER`
|
||||||
|
и в URL health-check. Первое роняло `grimmory.service` на любом другом адресе.
|
||||||
|
Третье опаснее: задача выполняется на целевом хосте, поэтому после починки
|
||||||
|
биндинга она уходила бы в БОЕВОЙ grimmory, получала 200 и давала
|
||||||
|
ложно-положительный результат. Исправлено через
|
||||||
|
`grimmory_bind_ip: "{{ expected_lan_ip }}"`; на боевом хосте рендер не
|
||||||
|
изменился, `--check --diff` даёт `changed=0`.
|
||||||
|
|
||||||
|
**Следствие для cutover.** Grimmory — единственный из шести, кто биндится на
|
||||||
|
конкретный адрес. После смены адреса его плейбук обязан быть прогнан заново,
|
||||||
|
иначе compose останется с временным адресом. Добавлено в шаг 4.5.16.
|
||||||
|
|
||||||
|
**Ещё одно ошибочное утверждение снято.** План предупреждал, что `roles/gyro`
|
||||||
|
пинит SSH host key gitea и после переезда сломается. Проверено дословно: там
|
||||||
|
`state: absent` — задача УДАЛЯЕТ устаревшую запись, а пинится `github.com`,
|
||||||
|
и `gyro_repo_url` ведёт на GitHub. Переезд gitea на gyro не влияет.
|
||||||
|
|
||||||
|
Legacy `pve-gyro.yml` теперь по умолчанию завершает все три plays, когда registry
|
||||||
|
говорит `provisioner: tofu`; для intentional legacy rollback/recovery нужен
|
||||||
|
явный override `-e pve_gyro_legacy_provisioning_enabled=true`. Это не normal
|
||||||
|
deployment.
|
||||||
|
|
||||||
|
**Cutover выполнен 2026-09-02 для пяти сервисов.** Все прошли по процедуре
|
||||||
|
раздела 4: свежий бэкап, перенос данных со сверкой, остановка старого
|
||||||
|
контейнера, смена адреса, правка реестра, перегенерация заданий бэкапа.
|
||||||
|
|
||||||
|
| Сервис | OLD → NEW | Сверка данных |
|
||||||
|
|---|---|---|
|
||||||
|
| docker-test | 145 → 152 | данных нет; Docker на fuse-overlayfs, hello-world проходит |
|
||||||
|
| vaultwarden | 140 → 154 | 1 users, 314 ciphers; integrity_check ok |
|
||||||
|
| monitoring | 146 → 155 | kuma.db совпал по sha256 и размеру; редирект на /dashboard |
|
||||||
|
| gitea | 141 → 153 | 64 repos, 5 users, 203 actions; integrity_check ok |
|
||||||
|
| grimmory | 149 → 157 | book=31 author=30 book_file=40 category=87 reading_sessions=68; Flyway 142/144 без новых миграций |
|
||||||
|
|
||||||
|
Публичные домены снаружи: `pass.ada-dev.ru` 200, `git.ada-dev.ru` 200,
|
||||||
|
`books.ada-dev.ru` health 200 (OPDS отдаёт штатный `401 Basic realm="Booklore
|
||||||
|
OPDS"` — это требование авторизации, а не поломка; в базе 2 учётки OPDS).
|
||||||
|
`make validate`, `make lint` зелёные, `make backup-jobs` перегенерировал списки
|
||||||
|
VMID сам из реестра.
|
||||||
|
|
||||||
|
**Смена адреса выполняется провайдером на месте.** Проверено на всех пяти:
|
||||||
|
`0 added, 1 changed, 0 destroyed`. Это снимало главный риск — пересоздание
|
||||||
|
контейнера с данными.
|
||||||
|
|
||||||
|
Старые контейнеры остановлены и целы: 140, 141, 145, 146, 149. Не удалять
|
||||||
|
минимум неделю, до 2026-09-09.
|
||||||
|
|
||||||
|
### Находки cutover, которых не было в плане
|
||||||
|
|
||||||
|
1. **Данные gitea никогда не попадали в PBS.** `vzdump` печатает
|
||||||
|
`excluding bind mount point mp0 ('/opt/gitea/data') from backup (not a
|
||||||
|
volume)` — снапшоты `ct/141` содержали только rootfs. Защищал данные лишь
|
||||||
|
offsite-профиль restic. Переезд на volume это чинит: теперь данные в бэкапе.
|
||||||
|
Побочно это самый весомый аргумент за миграцию, которого в плане не было.
|
||||||
|
2. **`vzdump` падает, пока работает Docker с fuse-overlayfs на rootfs в
|
||||||
|
каталоговом хранилище.** rootfs на `data` снапшоты не поддерживает, vzdump
|
||||||
|
уходит в режим `suspend` с rsync и спотыкается о
|
||||||
|
`var/lib/docker/fuse-overlayfs/.../merged`: Permission denied. У vaultwarden
|
||||||
|
этого нет — его rootfs на `local-lvm`, там настоящий снапшот. Лечится
|
||||||
|
остановкой сервиса перед бэкапом.
|
||||||
|
3. **Offsite-профиль restic надо переустанавливать на новом контейнере**, если
|
||||||
|
он выполняется внутри LXC. Сделано для vaultwarden и grimmory, проверено
|
||||||
|
прогоном юнита. Профиль gitea переписан с ноды внутрь контейнера
|
||||||
|
(`playbooks/offsite-restic-yadisk.yml`), старый таймер на cloud-pc отключён.
|
||||||
|
4. **`lost+found` на новом volume ломает restic.** Свежая ext4 создаёт этот
|
||||||
|
каталог с владельцем uid 0 хоста, внутри unprivileged LXC он
|
||||||
|
`nobody:nogroup` и нечитаем; restic отдаёт exit 3, юнит падает каждую ночь,
|
||||||
|
хотя снапшот сохраняется. Добавлен в исключения профиля gitea.
|
||||||
|
5. **SSH host-ключи gitea перенеслись вместе с данными.** Ключ на порту 2222
|
||||||
|
совпал с боевым (`AAAAC3NzaC1lZDI1NTE5AAAAIDE+iqCj`), поэтому у git-клиентов
|
||||||
|
`known_hosts` остался валидным. Обновлять пришлось только ключ sshd
|
||||||
|
контейнера на порту 22.
|
||||||
|
6. **Зависший ControlMaster даёт таймаут на баннере после cutover.** Мастер
|
||||||
|
остаётся живым, но его туннель ведёт в остановленный контейнер, и новые
|
||||||
|
сессии мультиплексируются поверх мёртвого соединения. Лечится
|
||||||
|
`ssh -O exit <host>` или разовым `-o ControlPath=none`.
|
||||||
|
7. **`systemctl start grimmory` — no-op.** Юнит `Type=oneshot` с
|
||||||
|
`RemainAfterExit=yes` уже числится активным, нужен `restart`.
|
||||||
|
8. **`MYSQL_ROOT_PASSWORD` из `.env` не подошёл к MariaDB** (`Access denied for
|
||||||
|
user 'root'@'localhost'`). Restore выполнен пользователем `grimmory`, у него
|
||||||
|
`ALL PRIVILEGES` на свою базу — этого достаточно для DROP/CREATE DATABASE.
|
||||||
|
|
||||||
|
### Gyro cutover verified
|
||||||
|
|
||||||
|
`gyro` завершён как cutover on 2026-09-02: CT 156 живёт на неизменном
|
||||||
|
production IP `192.168.1.35` и provisioned by Tofu. CT 150 остановлен и
|
||||||
|
удерживается как rollback минимум на неделю; его PBS snapshot от
|
||||||
|
`2026-09-02T19:40:56Z` сохранён, цепочку PBS не удаляли. Файл фаервола
|
||||||
|
`156.fw` уже копия `150.fw`, Datacenter firewall по-прежнему disabled. Timer
|
||||||
|
`gyro.timer` active, last oneshot success. Данных для переноса не было, и
|
||||||
|
TMPIP-specific bind/re-run не потребовались.
|
||||||
|
|
||||||
|
Удаление старого CT или его backup chain не выполнялось.
|
||||||
|
|
||||||
|
### Пакетный переезд сервисов 8-10: adguard, mihomo, ovpn-mini
|
||||||
|
|
||||||
|
Выполнено 2026-09-03 автономно, одной сессией. Этим OpenTofu-переезд всех
|
||||||
|
мигрируемых LXC завершён (осталось: `hermes-ai` заморожен, `pbs` не мигрируется).
|
||||||
|
|
||||||
|
| Сервис | OLD → NEW | Узел | Боевой IP (не менялся) | Свежий PBS перед стартом |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| adguard | 144 → **158** | mini-pc | 192.168.1.28 | `ct/144/2026-09-02T21:06:12Z` |
|
||||||
|
| mihomo | 143 → **159** | mini-pc | 192.168.1.27 | `ct/143/2026-09-02T21:12:49Z` |
|
||||||
|
| ovpn-mini | 132 → **160** | mini-pc | 192.168.1.23 | `ct/132/2026-09-02T21:17:55Z` |
|
||||||
|
|
||||||
|
OLD 144/143/132 остановлены и удерживаются как откат минимум неделю
|
||||||
|
(до ~2026-09-10). PBS-цепочки не трогали; у NEW 158/159/160 сняты первые
|
||||||
|
свои снапшоты (`…T21:37-39Z`), чтобы `backup-audit` был чист сразу.
|
||||||
|
`make validate`, `make lint`, `make tofu-plan` (`No changes`), `make openvpn-check`
|
||||||
|
(7/7) — зелёные. Внешние домены `git/pass/books.ada-dev.ru` — 200.
|
||||||
|
|
||||||
|
**Смена адреса — провайдером на месте** (`0 added, 1 changed, 0 destroyed`),
|
||||||
|
пересоздания контейнера с данными нет — как и на сервисах 2-7.
|
||||||
|
|
||||||
|
Специфика и находки перенесены в
|
||||||
|
[`migration-tofu.md`](migration-tofu.md) раздел 5 (пункты 8-10). Кратко:
|
||||||
|
|
||||||
|
- **adguard:** свежий AdGuard на пустом `conf/` не проходит health-гейт
|
||||||
|
плейбука (setup-wizard, порт 80 не слушает), поэтому данные заливаются ДО
|
||||||
|
config play, а не после. DNS-провал ~2 мин, ночью; ноды на DNS роутера,
|
||||||
|
контейнеры на 1.1.1.1.
|
||||||
|
- **mihomo:** первый боевой `device_passthrough` для `/dev/net/tun`
|
||||||
|
(`dev0: path=/dev/net/tun,mode=0660`). `ru-vps-mihomo-harden.yml` повторно
|
||||||
|
прогонять не нужно. Все зависимые ссылки на .27 остаются валидны.
|
||||||
|
- **ovpn-mini — петля транспорта.** `-F ansible/ssh_config` и `make tofu-*`
|
||||||
|
ходят к PVE через ru-vps → туннель, который терминирует сам ovpn-mini.
|
||||||
|
`pct stop 132` рвёт этот путь и `make tofu-apply` не достаёт API. Разрыв:
|
||||||
|
поднять шлюз на НОВОМ контейнере ещё на TMPIP (одноразовый плейбук), туннель
|
||||||
|
встаёт → tofu-apply работает → сменить адрес → повторный `openvpn-vps-mini.yml`
|
||||||
|
(идемпотентен). Прямой путь к нодам во время провала —
|
||||||
|
`ssh -o ProxyJump=none <user>@192.168.1.{5,10}` из LAN.
|
||||||
|
Совокупный провал туннеля (внешний reverse-proxy) ~10 мин.
|
||||||
|
- **`ssh_config`:** у `ovpn-mini` выставлен `ProxyJump none` (было — через
|
||||||
|
ru-vps). Путь через ru-vps закольцовывался бы на его же туннель.
|
||||||
|
- Три creation-play (`pve-adguard.yml`, `pve-mihomo.yml`, `pve-ovpn-mini.yml`)
|
||||||
|
за гейтом `provisioner != 'tofu'` (`meta: end_play`, override
|
||||||
|
`-e pve_<svc>_legacy_provisioning_enabled=true`) — как у `pve-gyro.yml`.
|
||||||
|
Их handlers иначе сделали бы `pct start <OLD>` на боевом адресе.
|
||||||
|
- **Ошибка сессии:** диагностический `pct start 132` при живом CT 160 дал
|
||||||
|
конфликт IP .23 и провал туннеля на 1-2 мин (восстановилось само). OLD
|
||||||
|
контейнеры трогать нельзя, их `pct config` держит боевой адрес.
|
||||||
|
- Косметика: `polkit.service` на CT 160 в `failed` (`status=217/USER`,
|
||||||
|
минимальный LXC-шаблон). На OpenVPN не влияет, не чинилось (можно
|
||||||
|
`systemctl mask polkit.service` — ничего от него не зависит).
|
||||||
|
|
||||||
|
**`onboot: 0` на всех остановленных OLD-контейнерах (2026-09-03).** Найдено:
|
||||||
|
132/140/141/142/143/144/145/146/148/149/150 стояли с `onboot: 1` и держат
|
||||||
|
боевые адреса в `pct config` — ребут любой ноды поднял бы их в конфликт с
|
||||||
|
живыми NEW (151-160). Снято через `pct set <id> --onboot 0`. Это временная
|
||||||
|
мера на окно отката: `pct destroy` (шаг 4.7) уберёт их целиком через неделю.
|
||||||
|
Обратимо (`--onboot 1`) для намеренного продвижения OLD обратно.
|
||||||
|
|
||||||
|
**Коммиты (2026-09-03).** Всё рабочее дерево (накопление сессий 2026-09-02/03)
|
||||||
|
разложено в 11 тематических коммитов на `main` (`22394cb`..`d2e1e68`),
|
||||||
|
не запушено. Границы — по инициативам: build-env, .env-в-корень+Make,
|
||||||
|
tofu scaffold, validate gate, registry-driven backups, вывод memoir-bot,
|
||||||
|
ru-vps base+кворум+ZeroTier, PBS storage prune, blue-green миграция на Tofu,
|
||||||
|
фикс VMID в update-плейбуках, docs. Несколько файлов (`services.yml`,
|
||||||
|
`status.yml`, `ssh_config`) несут вторичные правки других инициатив — отмечено
|
||||||
|
в телах коммитов.
|
||||||
|
|
||||||
|
**НЕ сделано (осознанно):** уборка раздела 6 `migration-tofu.md` — удаление
|
||||||
|
`roles/pve_lxc` (ещё используется creation-play пяти сервисов батча 2026-09-02
|
||||||
|
через `- role: pve_lxc`), стрип/полное гейчение creation plays, правки
|
||||||
|
`architecture.md`/`legacy-warning.md`, расширение `validate.yml` (features,
|
||||||
|
devices, mount points). Это отдельная задача; переездом сервисов 8-10
|
||||||
|
разблокирована (все 10 на Tofu), но исполнение — не сейчас.
|
||||||
|
|
||||||
|
Obsidian-заметки (`HomeLab.md`, `Notes/Текущее состояние…`, `Log.md`) по этому
|
||||||
|
переезду ещё не обновлены.
|
||||||
|
|
||||||
|
## Исторические планы
|
||||||
|
|
||||||
|
- [`../../current-task.md`](../../current-task.md) - исторический план Prometheus
|
||||||
|
monitoring. Он не описывает текущую активную архитектуру: Uptime Kuma активен,
|
||||||
|
Prometheus stack заморожен.
|
||||||
|
- [`../../tasks/grimmory-deployment-plan.md`](../../tasks/grimmory-deployment-plan.md)
|
||||||
|
- план и запись развертывания Grimmory; фактическое состояние проверять по inventory,
|
||||||
|
service registry и playbooks.
|
||||||
|
|
||||||
|
Выявленные риски и deferred work записаны в [`edge-cases.md`](edge-cases.md) и
|
||||||
|
[`legacy-warning.md`](legacy-warning.md), но не считаются утвержденным backlog.
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Технологический стек
|
||||||
|
|
||||||
|
## Control Plane
|
||||||
|
|
||||||
|
| Компонент | Роль | Declared / resolved evidence |
|
||||||
|
|---|---|---|
|
||||||
|
| Nix | Воспроизводимый dev shell | `flake.nix`, locked nixpkgs revision в `flake.lock` |
|
||||||
|
| Python | Ansible controller runtime | `pkgs.python3` в `flake.nix`; точная версия зависит от lock |
|
||||||
|
| ansible-core | Inventory, playbooks, roles | `>=2.19` в `requirements.txt`; locked Nix package на момент аудита 2.21.3 |
|
||||||
|
| proxmoxer | Proxmox API client | `>=2.3`; locked Nix package на момент аудита 2.3.0 |
|
||||||
|
| requests | HTTP dependency | `>=2.31`; точная Nix version определяется `flake.lock` |
|
||||||
|
| ansible.posix | POSIX modules | `>=2.0.0`; installed version не закреплена |
|
||||||
|
| community.proxmox | Proxmox modules | `>=2.0.0`; installed version не закреплена |
|
||||||
|
| community.general | Общие modules | `>=10.0.0`; installed version не закреплена |
|
||||||
|
| ansible-lint | Static validation | Nix package; CI отдельно pin `25.8.2` |
|
||||||
|
| yamllint | YAML validation | Nix package; CI отдельно pin `1.37.1` |
|
||||||
|
|
||||||
|
Nix shell также содержит Git, jq, OpenSSH, curl и GNU Make. Galaxy collections не
|
||||||
|
устанавливаются автоматически и живут в ignored `ansible/collections/`.
|
||||||
|
|
||||||
|
## Managed Runtime
|
||||||
|
|
||||||
|
- Proxmox VE/LXC и PBS являются внешними runtime systems; их версии не заданы manifest.
|
||||||
|
- Debian LXC templates и Docker/systemd используются service playbooks.
|
||||||
|
- Caddy, OpenVPN и restic устанавливаются/используются на managed hosts; точные
|
||||||
|
runtime versions репозиторий не фиксирует.
|
||||||
|
- Active update-managed container images обычно закреплены `tag@sha256:digest` в
|
||||||
|
`ansible/inventory/group_vars/all/services.yml` и service playbooks.
|
||||||
|
- Исключения: frozen Prometheus images закреплены только тегами, а `docker-test`
|
||||||
|
использует smoke image без declared digest.
|
||||||
|
|
||||||
|
## Grimmory MCP
|
||||||
|
|
||||||
|
| Компонент | Version | Evidence |
|
||||||
|
|---|---|---|
|
||||||
|
| Node.js | `>=22` | `tools/grimmory-mcp/package.json` |
|
||||||
|
| `@modelcontextprotocol/sdk` | `1.30.0` | package manifest и lockfile |
|
||||||
|
| Zod | `3.25.76` | package manifest и lockfile |
|
||||||
|
| Grimmory API compatibility | deployed `v3.2.4` | service registry и compatibility code |
|
||||||
|
|
||||||
|
Node.js/npm не входят в `flake.nix`; их нужно предоставлять отдельно.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix develop
|
||||||
|
ansible-galaxy collection install \
|
||||||
|
-r ansible/requirements.yml \
|
||||||
|
-p ansible/collections
|
||||||
|
make -C ansible help
|
||||||
|
```
|
||||||
|
|
||||||
|
`make -C ansible setup` остается venv fallback, но Nix является предпочтительным
|
||||||
|
контроллерным окружением.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make -C ansible inventory
|
||||||
|
make -C ansible docs
|
||||||
|
make -C ansible lint
|
||||||
|
nix develop -c sh -c \
|
||||||
|
'cd ansible && for f in playbooks/*.yml; do ansible-playbook --syntax-check "$f"; done'
|
||||||
|
npm test --prefix tools/grimmory-mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
`make check`, `make status`, `make backup-audit` и `make openvpn-check` обращаются к
|
||||||
|
живой инфраструктуре и не являются локальными unit tests. Документационное изменение
|
||||||
|
не требует их запуска.
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
`.gitea/workflows/lint.yml` описывает yamllint, ansible-lint и syntax-check всех
|
||||||
|
playbooks. Комментарии в самом workflow фиксируют, что Gitea Actions отключены и
|
||||||
|
runner не зарегистрирован; автоматического gate сейчас нет. MCP tests в workflow
|
||||||
|
не включены.
|
||||||
@@ -38,6 +38,8 @@
|
|||||||
pkgs.openssh
|
pkgs.openssh
|
||||||
pkgs.curl
|
pkgs.curl
|
||||||
pkgs.gnumake
|
pkgs.gnumake
|
||||||
|
# Provisioning LXC на этапе пилота: см. tofu/README.md.
|
||||||
|
pkgs.opentofu
|
||||||
];
|
];
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
|
|||||||
Generated
+36
@@ -0,0 +1,36 @@
|
|||||||
|
# This file is maintained automatically by "tofu init".
|
||||||
|
# Manual edits may be lost in future updates.
|
||||||
|
|
||||||
|
provider "registry.opentofu.org/bpg/proxmox" {
|
||||||
|
version = "0.111.1"
|
||||||
|
constraints = "~> 0.84"
|
||||||
|
hashes = [
|
||||||
|
"h1:0qABXcf7ulwKRhKGoK4LYZ7ObyoFkPxHmCrL6jRhMgE=",
|
||||||
|
"h1:0xM4HmXdu0hLrauA8ugcxjZvYHA+PuRybUxrXZEAdRI=",
|
||||||
|
"h1:ML2D3UUZTM99yrll/EBXj7wBYMb8xmQgomqFNybEoxY=",
|
||||||
|
"h1:PFGpaC7xcdC8w9jpxZPLJ/FFbVK5hd/CArUY+oXXTu8=",
|
||||||
|
"h1:XoYUWDsGWvxSlir/sYiGzx1MkcPVUtWIgc6OWEdf/C8=",
|
||||||
|
"h1:Zn7LyCWL/Xn0yCHiigXFYEp3MIv0MOVzUfL657QOplY=",
|
||||||
|
"h1:dIGBUSoC89e/PbpBEjbZ/YOzAkzrG1kF6EYcqk+8IqE=",
|
||||||
|
"h1:eERpiB94PyhN2SqE9E1+6sgpk0Kn6qjy555R2cjfpug=",
|
||||||
|
"h1:ejD4OSOL98W/SA4jLDvxEwCW8NSdVGKLyKmgfAUROK8=",
|
||||||
|
"h1:iTQv4FVFhMVl2juw6lgrVTpGFrdmdNPj+NFY2Dms0SE=",
|
||||||
|
"h1:jcqEv/zW+heFIPq5xwXxgS9EuBmbjIM1MriwQjx75WE=",
|
||||||
|
"h1:xF+AQJqpRf30WbrfHgSeuLqDBptWhTIdVoMyv7j6yVo=",
|
||||||
|
"h1:xWV1Y6ItiFXNCJO9OtyINMx8cqT5XiTOK2Rne7Jyu3w=",
|
||||||
|
"zh:18fb7c31a08dde6bffa1a4d4a211e604d6d17eec7092fd59331b3db3c6f3742c",
|
||||||
|
"zh:1cd60761538289d4dd2a1086b3ae62a7b0bdd4b1a2f824e9a44e243413168dba",
|
||||||
|
"zh:2eb76f6fc8299b6820ff678c8252332cc3366e226b5ae2e61748fd2449c1ed92",
|
||||||
|
"zh:45e6f7ebd0bf48911d37060359a4f359b5743b3092e985295733990e406d0416",
|
||||||
|
"zh:4aa8ba912eae37975d2e983394d173e595ca34fc76b5bf220b37d0e99d76e98c",
|
||||||
|
"zh:58e0789923103a77d502a0a9fc3eb920625e8eb935ec2d4ac0d006aebd1d186c",
|
||||||
|
"zh:6df8aa85fb8865915537e946c19b02538ad188018a629759c213c6f03730f642",
|
||||||
|
"zh:6ed47bc00d0913a1d0880618fa1376115e9edab6b4a658c081061a7f0e4ca360",
|
||||||
|
"zh:c5b10ff4f33df7e4c29e8f1127d49845b561b37b57517e844fb0954d7923d65e",
|
||||||
|
"zh:d016510e14b738499f0db9d9b3aafe82fc6877fb4ab4e9f831fb68a8d70a1385",
|
||||||
|
"zh:d941f394069bbf24351b363da1c64383f487067aaee0a84f9b96476d4912e212",
|
||||||
|
"zh:ddf271dbc2632ae8ffa8de3972f243ee47d260cb2ac90aa784f2746d98e21a0f",
|
||||||
|
"zh:ed0caa3501c42f611b7e9622c9b1df69fd85dc25a3cd88d3076381829688cd62",
|
||||||
|
"zh:f26e0763dbe6a6b2195c94b44696f2110f7f55433dc142839be16b9697fa5597",
|
||||||
|
]
|
||||||
|
}
|
||||||
+172
@@ -0,0 +1,172 @@
|
|||||||
|
# OpenTofu — пилот provisioning LXC
|
||||||
|
|
||||||
|
Экспериментальный каталог. Ни один боевой контейнер здесь не описан: цель —
|
||||||
|
выяснить, что провайдер `bpg/proxmox` реально может в этой инфраструктуре,
|
||||||
|
прежде чем принимать решение о переезде.
|
||||||
|
|
||||||
|
Активных ресурсов сейчас нет: пилотный контейнер снесён, а его описание лежит
|
||||||
|
в `pilot.tf.example` — расширение не даёт Tofu его загрузить. Это рабочий
|
||||||
|
образец формы ресурса для реальных сервисов, см.
|
||||||
|
[`../docs/ai/migration-tofu.md`](../docs/ai/migration-tofu.md).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make -C ansible tofu-init
|
||||||
|
make -C ansible tofu-plan
|
||||||
|
make -C ansible tofu-apply CONFIRM=1
|
||||||
|
make -C ansible tofu-destroy CONFIRM=1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Результат пилота (2026-09-02, provider 0.84, PVE-кластер homelab)
|
||||||
|
|
||||||
|
Всё проверено на живом кластере контейнером VMID 199 `tofu-pilot`.
|
||||||
|
|
||||||
|
### Что API-токен МОЖЕТ
|
||||||
|
|
||||||
|
| Возможность | Проверено |
|
||||||
|
|---|---|
|
||||||
|
| Создание LXC: vmid, node, hostname, cores, memory, swap, unprivileged | да |
|
||||||
|
| Сеть: bridge, статический IP, шлюз, firewall | да |
|
||||||
|
| rootfs на datastore | да |
|
||||||
|
| **Mount point как volume на datastore** | да — `mp0: data:199/vm-199-disk-1.raw,mp=/opt/pilot-data,size=4G` |
|
||||||
|
| `features { nesting }` | да |
|
||||||
|
| onboot, startup order, tags | да |
|
||||||
|
| Идемпотентность (`plan` -> `No changes`) | да |
|
||||||
|
|
||||||
|
Строка про mount point — ответ на вопрос про `mp0` у gitea. Bind mount каталога
|
||||||
|
хоста (`mp0: /opt/data/gitea,mp=...`) токеном недоступен, а **volume на
|
||||||
|
datastore — доступен**. То есть при пересоздании gitea хранилище можно
|
||||||
|
перевести на volume и снять зависимость от root@pam.
|
||||||
|
|
||||||
|
### Чего API-токен НЕ МОЖЕТ
|
||||||
|
|
||||||
|
Proxmox отвечает HTTP 403:
|
||||||
|
|
||||||
|
```
|
||||||
|
configuring device passthrough is only allowed for root@pam
|
||||||
|
changing feature flags (except nesting) is only allowed for root@pam
|
||||||
|
```
|
||||||
|
|
||||||
|
То есть `keyctl`, `fuse`, `mount` и `device_passthrough` (`dev0:`) требуют
|
||||||
|
root@pam. Это ограничение **Proxmox, а не Tofu**: Ansible упирается в ровно ту
|
||||||
|
же стену, поэтому в `pve-monitoring.yml`, `pve-grimmory.yml` и
|
||||||
|
`pve-hermes-ai.yml` есть отдельный шаг `pct set <vmid> --features
|
||||||
|
nesting=1,keyctl=1` по SSH под root, а `/dev/fuse` и `/dev/net/tun`
|
||||||
|
пробрасываются правкой `/etc/pve/lxc/<vmid>.conf`.
|
||||||
|
|
||||||
|
Вывод: по этому пункту Tofu не хуже нынешнего состояния — ему нужен такой же
|
||||||
|
дошаг.
|
||||||
|
|
||||||
|
### Ловушка гибридной схемы
|
||||||
|
|
||||||
|
Если Tofu создаёт контейнер, а Ansible потом добавляет `keyctl` через
|
||||||
|
`pct set`, Tofu видит это как дрейф: `plan` даёт `1 to change` и следующий
|
||||||
|
`apply` откатил бы features обратно, лишив контейнер возможности запускать
|
||||||
|
Docker. Проверено на пилоте.
|
||||||
|
|
||||||
|
Обход — `lifecycle { ignore_changes = [features] }`, он в `pilot.tf`. После
|
||||||
|
него `plan` снова даёт `No changes`, а `keyctl=1` на узле сохраняется. Плата:
|
||||||
|
features перестают управляться декларативно.
|
||||||
|
|
||||||
|
### `cmode` — не в списке атрибутов ресурса, но провайдер его отслеживает через `console`
|
||||||
|
|
||||||
|
Обнаружено 2026-09-02 при переезде `emergency-bot` (сервис №1), исправлено в
|
||||||
|
тот же день. `roles/pve_lxc` всем создаваемым контейнерам ставит
|
||||||
|
`cmode: shell` (`pct console` заходит сразу в root shell, а не в getty-логин)
|
||||||
|
— `pve_lxc_cmode` в `roles/pve_lxc/defaults/main.yml`. У ресурса
|
||||||
|
`proxmox_virtual_environment_container` провайдера `bpg/proxmox` нет
|
||||||
|
top-level атрибута `cmode` — он скрыт в блоке `console { type = ... }`,
|
||||||
|
который в plan/apply при создании не показывается, если не задан явно.
|
||||||
|
|
||||||
|
Первая ошибка: контейнер создан без блока `console`, получил дефолт Proxmox
|
||||||
|
(`cmode: tty`), исправлено одноразовым `pct set <vmid> --cmode shell` по SSH.
|
||||||
|
Это была ошибка: следующий `tofu-plan` (уже на шаге 4.5 этого же сервиса)
|
||||||
|
показал `console { type = "shell" -> null }` — провайдер ЗАМЕТИЛ ручную правку
|
||||||
|
через refresh state и на следующем apply откатил бы её обратно на `tty`. То
|
||||||
|
есть `cmode` ведёт себя ровно как `keyctl` в гибридной схеме ниже: правится
|
||||||
|
вручную — ловит drift.
|
||||||
|
|
||||||
|
Правильное решение — объявить блок в ресурсе явно:
|
||||||
|
|
||||||
|
console {
|
||||||
|
type = "shell"
|
||||||
|
}
|
||||||
|
|
||||||
|
После этого `tofu-plan` не показывает `console` как diff, ручная правка не
|
||||||
|
нужна. Добавлять этот блок в описание любого сервиса на шаге 4.2 сразу же,
|
||||||
|
не как отдельный ручной пост-шаг.
|
||||||
|
|
||||||
|
### Рутовый токен не помогает — проверено по исходникам
|
||||||
|
|
||||||
|
Проверка в `/usr/share/perl5/PVE/LXC.pm:1658` буквальная:
|
||||||
|
|
||||||
|
return 1 if $authuser eq 'root@pam';
|
||||||
|
|
||||||
|
А `$authuser` при токенной аутентификации — это полный token-ID. Из
|
||||||
|
`/usr/share/perl5/PVE/HTTPServer.pm:86`:
|
||||||
|
|
||||||
|
# the token-ID `<user>@<realm>!<tokenname>` is the user for token based authentication
|
||||||
|
$username = eval { PVE::AccessControl::verify_token($api_token); };
|
||||||
|
|
||||||
|
`verify_token` возвращает `$tokenid`, то есть `user@realm!tokenname`. Строка
|
||||||
|
`root@pam!mytoken` не равна `root@pam`, поэтому токен root проверку НЕ проходит.
|
||||||
|
|
||||||
|
Следствия:
|
||||||
|
|
||||||
|
- выдать токену больше прав бесполезно: это не проверка привилегий, а сравнение
|
||||||
|
имени пользователя, ACL на неё не влияют;
|
||||||
|
- единственный вариант «сделать всё через Tofu» — аутентификация root@pam по
|
||||||
|
паролю. В realm `pam` это системный пароль root узла Proxmox: его нельзя
|
||||||
|
ограничить по scope и нельзя отозвать отдельно от смены пароля root.
|
||||||
|
|
||||||
|
### Решение: root@pam по паролю (принято и проверено 2026-09-02)
|
||||||
|
|
||||||
|
Выбран привилегированный режим. Проверено на пилоте — всё привилегированное
|
||||||
|
выставляется декларативно, без единого шага Ansible:
|
||||||
|
|
||||||
|
dev0: deny-write=0,path=/dev/net/tun,uid=0,gid=0,mode=0660
|
||||||
|
features: fuse=1,keyctl=1,nesting=1,mknod=0
|
||||||
|
mp0: data:199/vm-199-disk-1.raw,mp=/opt/pilot-data,size=4G
|
||||||
|
|
||||||
|
Повторный `plan` даёт `No changes`.
|
||||||
|
|
||||||
|
Следствия для миграции сервисов:
|
||||||
|
|
||||||
|
- дошаг `pct set <vmid> --features nesting=1,keyctl=1` по SSH больше не нужен —
|
||||||
|
при переезде сервиса его можно удалять из соответствующего `pve-*.yml`;
|
||||||
|
- правка `/etc/pve/lxc/<vmid>.conf` через `lineinfile` (девять контейнеров)
|
||||||
|
заменяется блоком `device_passthrough`;
|
||||||
|
- `lifecycle { ignore_changes = [features] }` не нужен: он был обходом для
|
||||||
|
гибридной схемы, от которой отказались;
|
||||||
|
- apply одной фазой, без чередования Tofu и Ansible.
|
||||||
|
|
||||||
|
Отвергнутая альтернатива — гибрид: Tofu создаёт то, что доступно токену,
|
||||||
|
Ansible доводит привилегированное по SSH через существующий sudo. Не требует
|
||||||
|
новых секретов, но оставляет декларативность частичной и apply двухфазным.
|
||||||
|
|
||||||
|
## Аутентификация
|
||||||
|
|
||||||
|
Способ выбирает Makefile по корневому `.env`:
|
||||||
|
|
||||||
|
| Условие | Режим | Что доступно |
|
||||||
|
|---|---|---|
|
||||||
|
| `PROXMOX_ROOT_PASSWORD` задан и не `replace-me` | root@pam по паролю | всё, включая features и device passthrough |
|
||||||
|
| иначе | токен `ansible@pve` | всё кроме features (кроме nesting), device passthrough и bind mount |
|
||||||
|
|
||||||
|
Выбранный режим печатается в stderr перед запуском, чтобы из вывода было видно,
|
||||||
|
какими правами шёл apply. В `pilot.tf` привилегированные поля включаются через
|
||||||
|
`local.privileged`, поэтому конфигурация валидна в обоих режимах.
|
||||||
|
|
||||||
|
## Сеть
|
||||||
|
|
||||||
|
Tofu ходит в Proxmox API напрямую по HTTPS и **не умеет ProxyJump** из
|
||||||
|
`ssh_config`. Вне LAN узлы недоступны, поэтому цели `tofu-*` в Makefile сами
|
||||||
|
поднимают SSH-туннель через `ru-vps` на время команды и направляют провайдер в
|
||||||
|
`127.0.0.1`. Отсюда же `insecure = true`: сертификат выписан на узел, а
|
||||||
|
обращение идёт к localhost.
|
||||||
|
|
||||||
|
## State
|
||||||
|
|
||||||
|
`tofu/*.tfstate*` в `.gitignore`: состояние содержит фактическую конфигурацию
|
||||||
|
гостей. `.terraform.lock.hcl` наоборот коммитится — он закрепляет версию
|
||||||
|
провайдера. Отдельного бэкенда нет; при переходе к боевому использованию
|
||||||
|
состояние нужно куда-то бэкапить.
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# ОБРАЗЕЦ, НЕ ЗАГРУЖАЕТСЯ. Расширение .example выбрано намеренно: сам пилот
|
||||||
|
# (VMID 199) снесён 2026-09-02, и будь этот файл активным, каждый `tofu-plan`
|
||||||
|
# предлагал бы создать его заново.
|
||||||
|
#
|
||||||
|
# Держим как рабочий пример формы ресурса: по нему описываются реальные
|
||||||
|
# сервисы при переезде. См. docs/ai/migration-tofu.md, шаг 4.2.
|
||||||
|
# ============================================================================
|
||||||
|
#
|
||||||
|
# Пилот: одноразовый LXC, созданный OpenTofu.
|
||||||
|
# ============================================================================
|
||||||
|
#
|
||||||
|
# ЗАЧЕМ
|
||||||
|
# Проверить на живом кластере ровно те места, из-за которых переезд на Tofu
|
||||||
|
# выглядел рискованным, не трогая ни один боевой контейнер:
|
||||||
|
#
|
||||||
|
# 1. Проброс /dev/fuse. В pve-*.yml он сделан правкой /etc/pve/lxc/<vmid>.conf
|
||||||
|
# (lxc.cgroup2.devices.allow + lxc.mount.entry), потому что Proxmox API
|
||||||
|
# сырые lxc.* ключи не принимает. У провайдера для этого есть
|
||||||
|
# features.fuse — штатный флаг PVE, а не обход.
|
||||||
|
# 2. Проброс /dev/net/tun — блок device_passthrough (dev0: в PVE 8.2+).
|
||||||
|
# 3. Mount point на storage вместо bind mount каталога хоста. Именно bind
|
||||||
|
# mount у gitea (mp0: /opt/data/gitea) требует прав root@pam и не
|
||||||
|
# создаётся API-токеном; volume на datastore — создаётся.
|
||||||
|
#
|
||||||
|
# Контейнер намеренно не входит в inventory и не автозапускается.
|
||||||
|
# Снести после проверки: make tofu-destroy CONFIRM=1
|
||||||
|
#
|
||||||
|
# VMID 199 и 192.168.1.39 выбраны свободными на 2026-09-02 и лежат вне
|
||||||
|
# диапазона сервисов, но внутри маршрутов ru-vps (192.168.1.5-40).
|
||||||
|
|
||||||
|
locals {
|
||||||
|
# Привилегированный режим = аутентификация root@pam по паролю. Makefile
|
||||||
|
# выбирает его, когда в корневом .env задан PROXMOX_ROOT_PASSWORD.
|
||||||
|
privileged = var.pve_password != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "proxmox_virtual_environment_container" "pilot" {
|
||||||
|
node_name = "cloud-pc"
|
||||||
|
vm_id = 199
|
||||||
|
unprivileged = true
|
||||||
|
start_on_boot = false
|
||||||
|
started = true
|
||||||
|
tags = ["tofu", "pilot"]
|
||||||
|
|
||||||
|
initialization {
|
||||||
|
hostname = "tofu-pilot"
|
||||||
|
|
||||||
|
ip_config {
|
||||||
|
ipv4 {
|
||||||
|
address = "192.168.1.39/24"
|
||||||
|
gateway = "192.168.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dns {
|
||||||
|
servers = ["1.1.1.1"]
|
||||||
|
}
|
||||||
|
|
||||||
|
user_account {
|
||||||
|
keys = [trimspace(file(pathexpand("~/.ssh/id_ed25519_homelab.pub")))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operating_system {
|
||||||
|
template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst"
|
||||||
|
type = "debian"
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu {
|
||||||
|
cores = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
memory {
|
||||||
|
dedicated = 512
|
||||||
|
swap = 512
|
||||||
|
}
|
||||||
|
|
||||||
|
disk {
|
||||||
|
datastore_id = "data"
|
||||||
|
size = 8
|
||||||
|
}
|
||||||
|
|
||||||
|
# Проверка №1 и №2: то, что в pve-*.yml делается правкой конфига на узле.
|
||||||
|
# Токену Proxmox разрешает только nesting; keyctl и fuse требуют root@pam.
|
||||||
|
# null означает «не задавать поле», а не «выключить».
|
||||||
|
features {
|
||||||
|
nesting = true
|
||||||
|
keyctl = local.privileged ? true : null
|
||||||
|
fuse = local.privileged ? true : null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Проброс устройства Proxmox разрешает только root@pam (403 для токена,
|
||||||
|
# проверено на пилоте 2026-09-02), поэтому блок появляется лишь в
|
||||||
|
# привилегированном режиме.
|
||||||
|
dynamic "device_passthrough" {
|
||||||
|
for_each = local.privileged ? [1] : []
|
||||||
|
content {
|
||||||
|
path = "/dev/net/tun"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Проверка №3: volume на datastore, а не bind mount каталога хоста.
|
||||||
|
mount_point {
|
||||||
|
volume = "data"
|
||||||
|
size = "4G"
|
||||||
|
path = "/opt/pilot-data"
|
||||||
|
}
|
||||||
|
|
||||||
|
network_interface {
|
||||||
|
name = "eth0"
|
||||||
|
bridge = "vmbr0"
|
||||||
|
firewall = true
|
||||||
|
}
|
||||||
|
|
||||||
|
startup {
|
||||||
|
order = 999
|
||||||
|
}
|
||||||
|
|
||||||
|
# ignore_changes = [features] здесь НЕТ намеренно. Он нужен только в
|
||||||
|
# гибридной схеме, где features выставляет Ansible через `pct set`: иначе
|
||||||
|
# Tofu на следующем apply откатывает их и ломает Docker в контейнере
|
||||||
|
# (проверено на пилоте). В привилегированном режиме features описаны выше
|
||||||
|
# декларативно, и обход не требуется.
|
||||||
|
}
|
||||||
|
|
||||||
|
output "pilot" {
|
||||||
|
description = "Что проверять на узле после apply"
|
||||||
|
value = {
|
||||||
|
vmid = proxmox_virtual_environment_container.pilot.vm_id
|
||||||
|
node = proxmox_virtual_environment_container.pilot.node_name
|
||||||
|
verify = "ssh cloud-pc sudo pct config ${proxmox_virtual_environment_container.pilot.vm_id}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
provider "proxmox" {
|
||||||
|
endpoint = var.pve_endpoint
|
||||||
|
insecure = var.pve_insecure
|
||||||
|
|
||||||
|
# null вместо пустой строки принципиален: провайдер выбирает способ
|
||||||
|
# аутентификации по тому, какие поля заданы, и пустая строка считалась бы
|
||||||
|
# заданным значением.
|
||||||
|
api_token = var.pve_api_token != "" ? var.pve_api_token : null
|
||||||
|
username = var.pve_username != "" ? var.pve_username : null
|
||||||
|
password = var.pve_password != "" ? var.pve_password : null
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# Сервисы, переведённые на OpenTofu provisioning по схеме blue-green.
|
||||||
|
# Процедура и порядок сервисов: docs/ai/migration-tofu.md.
|
||||||
|
# ============================================================================
|
||||||
|
#
|
||||||
|
# Сервис №1: emergency-bot (OLD vmid 148, mini-pc).
|
||||||
|
# Эталон снят 2026-09-02: ssh mini-pc sudo pct config 148.
|
||||||
|
# Данных нет (backup: none), шаг 4.4 (перенос данных) пропускается целиком.
|
||||||
|
#
|
||||||
|
# vm_id 151 — новый VMID для blue-green переезда, TMPIP 192.168.1.6 был
|
||||||
|
# использован только на шаге 4.3 (проверка перед cutover). После шага 4.5.15
|
||||||
|
# адрес — боевой 192.168.1.32, OLD (148) остановлен и остаётся откатом
|
||||||
|
# минимум неделю.
|
||||||
|
|
||||||
|
resource "proxmox_virtual_environment_container" "emergency_bot" {
|
||||||
|
node_name = "mini-pc"
|
||||||
|
vm_id = 151
|
||||||
|
unprivileged = true
|
||||||
|
start_on_boot = true
|
||||||
|
started = true
|
||||||
|
tags = ["tofu"]
|
||||||
|
|
||||||
|
initialization {
|
||||||
|
hostname = "emergency-bot"
|
||||||
|
|
||||||
|
ip_config {
|
||||||
|
ipv4 {
|
||||||
|
address = "192.168.1.32/24"
|
||||||
|
gateway = "192.168.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dns {
|
||||||
|
servers = ["1.1.1.1"]
|
||||||
|
}
|
||||||
|
|
||||||
|
user_account {
|
||||||
|
keys = [trimspace(file(pathexpand("~/.ssh/id_ed25519_homelab.pub")))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operating_system {
|
||||||
|
template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst"
|
||||||
|
type = "debian"
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu {
|
||||||
|
cores = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
memory {
|
||||||
|
dedicated = 512
|
||||||
|
swap = 256
|
||||||
|
}
|
||||||
|
|
||||||
|
disk {
|
||||||
|
datastore_id = "local-lvm"
|
||||||
|
size = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
# Эталон: features: nesting=1. Ни keyctl, ни fuse, ни device passthrough
|
||||||
|
# эталону не нужны, поэтому доступно в обоих режимах аутентификации.
|
||||||
|
features {
|
||||||
|
nesting = true
|
||||||
|
}
|
||||||
|
|
||||||
|
# roles/pve_lxc всем контейнерам ставит cmode=shell (pve_lxc_cmode), провайдер
|
||||||
|
# это отслеживает через блок console.type — без него на следующем apply
|
||||||
|
# откатит на дефолт Proxmox tty. Обнаружено 2026-09-02 на этом сервисе,
|
||||||
|
# см. tofu/README.md.
|
||||||
|
console {
|
||||||
|
type = "shell"
|
||||||
|
}
|
||||||
|
|
||||||
|
network_interface {
|
||||||
|
name = "eth0"
|
||||||
|
bridge = "vmbr0"
|
||||||
|
firewall = true
|
||||||
|
}
|
||||||
|
|
||||||
|
startup {
|
||||||
|
order = 70
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "emergency_bot" {
|
||||||
|
description = "Что проверять на узле после apply"
|
||||||
|
value = {
|
||||||
|
vmid = proxmox_virtual_environment_container.emergency_bot.vm_id
|
||||||
|
node = proxmox_virtual_environment_container.emergency_bot.node_name
|
||||||
|
verify = "ssh mini-pc sudo pct config ${proxmox_virtual_environment_container.emergency_bot.vm_id}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# Сервис №8 по плану (docs/ai/migration-tofu.md, раздел 5): adguard.
|
||||||
|
# DNS-фильтр всей LAN. Жёсткое окно простоя только между шагами 4.5.14 и
|
||||||
|
# 4.5.17 (два контейнера на 192.168.1.28 одновременно невозможны).
|
||||||
|
#
|
||||||
|
# OLD vmid 144, узел mini-pc, боевой адрес 192.168.1.28.
|
||||||
|
# Эталон снят 2026-09-02: ssh mini-pc sudo pct config 144 (дословно совпадает
|
||||||
|
# с /etc/pve/lxc/144.conf):
|
||||||
|
#
|
||||||
|
# arch: amd64
|
||||||
|
# cmode: shell
|
||||||
|
# cores: 1
|
||||||
|
# features: nesting=1,keyctl=1
|
||||||
|
# hostname: adguard
|
||||||
|
# memory: 512
|
||||||
|
# nameserver: 1.1.1.1
|
||||||
|
# net0: name=eth0,bridge=vmbr0,firewall=1,gw=192.168.1.1,ip=192.168.1.28/24,type=veth
|
||||||
|
# onboot: 1
|
||||||
|
# ostype: debian
|
||||||
|
# rootfs: local-lvm:vm-144-disk-0,size=8G
|
||||||
|
# startup: order=40
|
||||||
|
# swap: 512
|
||||||
|
# unprivileged: 1
|
||||||
|
# lxc.cgroup2.devices.allow: c 10:229 rwm
|
||||||
|
# lxc.mount.entry: /dev/fuse dev/fuse none bind,create=file
|
||||||
|
#
|
||||||
|
# vm_id 158 — новый VMID для blue-green переезда (назначен, не менять).
|
||||||
|
# TMPIP 192.168.1.17 используется только на шаге 4.3 (проверка перед cutover).
|
||||||
|
# После шага 4.5.15 адрес меняется на боевой 192.168.1.28, OLD (144)
|
||||||
|
# останавливается и остаётся откатом минимум неделю.
|
||||||
|
#
|
||||||
|
# --- /dev/fuse: features.fuse, а не device_passthrough ----------------------
|
||||||
|
# Эталонные lxc.cgroup2.devices.allow + lxc.mount.entry для /dev/fuse — ручной
|
||||||
|
# обход (lineinfile по /etc/pve/lxc/144.conf в pve-adguard.yml), потому что
|
||||||
|
# Proxmox API сырые lxc.* ключи не принимает. Заменяется штатным флагом PVE
|
||||||
|
# features.fuse, проверенным на пилоте VMID 199 и на сервисах 2-7. Следствие:
|
||||||
|
# pct config нового контейнера покажет features: fuse=1,keyctl=1,nesting=1 —
|
||||||
|
# текстуально иначе, чем эталон, при том же эффекте. См. tofu/README.md.
|
||||||
|
#
|
||||||
|
# --- Данные ----------------------------------------------------------------
|
||||||
|
# /opt/adguard/{conf,work} (~313 МБ, почти всё — work/: журнал запросов,
|
||||||
|
# статистика, кэш фильтров). conf/AdGuardHome.yaml несёт весь конфиг, включая
|
||||||
|
# хэш пароля админа, DNS rewrites, upstream и клиентов. Переносится целиком
|
||||||
|
# на шаге 4.4; Ansible этим каталогом не управляет.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
resource "proxmox_virtual_environment_container" "adguard" {
|
||||||
|
node_name = "mini-pc"
|
||||||
|
vm_id = 158
|
||||||
|
unprivileged = true
|
||||||
|
start_on_boot = true
|
||||||
|
started = true
|
||||||
|
tags = ["tofu"]
|
||||||
|
|
||||||
|
initialization {
|
||||||
|
hostname = "adguard"
|
||||||
|
|
||||||
|
ip_config {
|
||||||
|
ipv4 {
|
||||||
|
address = "192.168.1.28/24"
|
||||||
|
gateway = "192.168.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dns {
|
||||||
|
servers = ["1.1.1.1"]
|
||||||
|
}
|
||||||
|
|
||||||
|
user_account {
|
||||||
|
keys = [trimspace(file(pathexpand("~/.ssh/id_ed25519_homelab.pub")))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operating_system {
|
||||||
|
template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst"
|
||||||
|
type = "debian"
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu {
|
||||||
|
cores = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
memory {
|
||||||
|
dedicated = 512
|
||||||
|
swap = 512
|
||||||
|
}
|
||||||
|
|
||||||
|
disk {
|
||||||
|
datastore_id = "local-lvm"
|
||||||
|
size = 8
|
||||||
|
}
|
||||||
|
|
||||||
|
# Эталон: features: nesting=1,keyctl=1 плюс проброс /dev/fuse через сырые
|
||||||
|
# lxc.* строки. fuse=1 — штатная замена этого обхода (см. комментарий выше).
|
||||||
|
features {
|
||||||
|
nesting = true
|
||||||
|
keyctl = true
|
||||||
|
fuse = true
|
||||||
|
}
|
||||||
|
|
||||||
|
# roles/pve_lxc всем контейнерам ставит cmode=shell (pve_lxc_cmode), провайдер
|
||||||
|
# это отслеживает через блок console.type — без него на следующем apply
|
||||||
|
# откатит на дефолт Proxmox tty. Обнаружено 2026-09-02 на emergency-bot,
|
||||||
|
# см. tofu/README.md.
|
||||||
|
console {
|
||||||
|
type = "shell"
|
||||||
|
}
|
||||||
|
|
||||||
|
network_interface {
|
||||||
|
name = "eth0"
|
||||||
|
bridge = "vmbr0"
|
||||||
|
firewall = true
|
||||||
|
}
|
||||||
|
|
||||||
|
startup {
|
||||||
|
order = 40
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "adguard" {
|
||||||
|
description = "Что проверять на узле после apply"
|
||||||
|
value = {
|
||||||
|
vmid = proxmox_virtual_environment_container.adguard.vm_id
|
||||||
|
node = proxmox_virtual_environment_container.adguard.node_name
|
||||||
|
verify = "ssh mini-pc sudo pct config ${proxmox_virtual_environment_container.adguard.vm_id}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# Сервис №2: docker-test (OLD vmid 145, cloud-pc).
|
||||||
|
# Эталон снят 2026-09-02: ssh cloud-pc sudo pct config 145.
|
||||||
|
# Данных нет (песочница), шаг 4.4 (перенос данных) пропускается целиком.
|
||||||
|
#
|
||||||
|
# vm_id 152 — новый VMID blue-green переезда. TMPIP 192.168.1.11 отработал на
|
||||||
|
# шаге 4.3; cutover выполнен 2026-09-02, адрес боевой — 192.168.1.29.
|
||||||
|
# OLD (145) остановлен и остаётся откатом минимум неделю, до 2026-09-09.
|
||||||
|
#
|
||||||
|
# В отличие от emergency_bot, этот сервис проверяет проброс /dev/fuse на
|
||||||
|
# реальном сервисе: Docker внутри работает на storage-driver fuse-overlayfs.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
resource "proxmox_virtual_environment_container" "docker_test" {
|
||||||
|
node_name = "cloud-pc"
|
||||||
|
vm_id = 152
|
||||||
|
unprivileged = true
|
||||||
|
start_on_boot = true
|
||||||
|
started = true
|
||||||
|
tags = ["tofu"]
|
||||||
|
|
||||||
|
initialization {
|
||||||
|
hostname = "docker-test"
|
||||||
|
|
||||||
|
ip_config {
|
||||||
|
ipv4 {
|
||||||
|
address = "192.168.1.29/24"
|
||||||
|
gateway = "192.168.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dns {
|
||||||
|
servers = ["1.1.1.1"]
|
||||||
|
}
|
||||||
|
|
||||||
|
user_account {
|
||||||
|
keys = [trimspace(file(pathexpand("~/.ssh/id_ed25519_homelab.pub")))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operating_system {
|
||||||
|
template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst"
|
||||||
|
type = "debian"
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu {
|
||||||
|
cores = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
memory {
|
||||||
|
dedicated = 512
|
||||||
|
swap = 512
|
||||||
|
}
|
||||||
|
|
||||||
|
disk {
|
||||||
|
datastore_id = "data"
|
||||||
|
size = 8
|
||||||
|
}
|
||||||
|
|
||||||
|
# Эталон: features: nesting=1,keyctl=1 плюс проброс /dev/fuse, сделанный в
|
||||||
|
# pve-docker-test.yml правкой /etc/pve/lxc/145.conf (lxc.cgroup2.devices.allow
|
||||||
|
# + lxc.mount.entry) — Proxmox API сырые lxc.* ключи не принимает.
|
||||||
|
#
|
||||||
|
# У провайдера для /dev/fuse есть ШТАТНЫЙ флаг features.fuse, а не
|
||||||
|
# device_passthrough: последний нужен только для /dev/net/tun (dev0: в PVE
|
||||||
|
# 8.2+). См. tofu/pilot.tf.example:17-21 и tofu/README.md.
|
||||||
|
# Поэтому pct config нового контейнера покажет features: fuse=1,keyctl=1,
|
||||||
|
# nesting=1 — текстуально иначе, чем эталон, но это тот же результат
|
||||||
|
# штатным механизмом вместо обхода.
|
||||||
|
#
|
||||||
|
# Всё три флага требуют root@pam; токену Proxmox разрешает только nesting.
|
||||||
|
features {
|
||||||
|
nesting = true
|
||||||
|
keyctl = true
|
||||||
|
fuse = true
|
||||||
|
}
|
||||||
|
|
||||||
|
# roles/pve_lxc всем контейнерам ставит cmode=shell (pve_lxc_cmode), провайдер
|
||||||
|
# это отслеживает через блок console.type — без него на следующем apply
|
||||||
|
# откатит на дефолт Proxmox tty. Обнаружено 2026-09-02 на emergency-bot,
|
||||||
|
# см. tofu/README.md.
|
||||||
|
console {
|
||||||
|
type = "shell"
|
||||||
|
}
|
||||||
|
|
||||||
|
network_interface {
|
||||||
|
name = "eth0"
|
||||||
|
bridge = "vmbr0"
|
||||||
|
firewall = true
|
||||||
|
}
|
||||||
|
|
||||||
|
startup {
|
||||||
|
order = 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "docker_test" {
|
||||||
|
description = "Что проверять на узле после apply"
|
||||||
|
value = {
|
||||||
|
vmid = proxmox_virtual_environment_container.docker_test.vm_id
|
||||||
|
node = proxmox_virtual_environment_container.docker_test.node_name
|
||||||
|
verify = "ssh cloud-pc sudo pct config ${proxmox_virtual_environment_container.docker_test.vm_id}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# Сервис №3 по плану (docs/ai/migration-tofu.md, раздел 5): gitea.
|
||||||
|
# Главный приз всей миграции — ради него она и затевалась.
|
||||||
|
#
|
||||||
|
# OLD vmid 141, узел cloud-pc, боевой адрес 192.168.1.25.
|
||||||
|
# Эталон снят 2026-09-02: ssh cloud-pc sudo pct config 141 (и совпадающий
|
||||||
|
# /etc/pve/lxc/141.conf):
|
||||||
|
#
|
||||||
|
# arch: amd64
|
||||||
|
# cmode: shell
|
||||||
|
# cores: 2
|
||||||
|
# features: nesting=1,keyctl=1
|
||||||
|
# hostname: gitea
|
||||||
|
# memory: 2048
|
||||||
|
# mp0: /opt/data/gitea,mp=/opt/gitea/data
|
||||||
|
# nameserver: 1.1.1.1
|
||||||
|
# net0: name=eth0,bridge=vmbr0,firewall=1,gw=192.168.1.1,ip=192.168.1.25/24,type=veth
|
||||||
|
# onboot: 1
|
||||||
|
# ostype: debian
|
||||||
|
# rootfs: data:141/vm-141-disk-0.raw,size=32G
|
||||||
|
# startup: order=50
|
||||||
|
# swap: 1024
|
||||||
|
# unprivileged: 1
|
||||||
|
# lxc.cgroup2.devices.allow: c 10:229 rwm
|
||||||
|
# lxc.mount.entry: /dev/fuse dev/fuse none bind,create=file
|
||||||
|
#
|
||||||
|
# vm_id 153 — новый VMID для blue-green переезда. IP ниже — TMPIP
|
||||||
|
# 192.168.1.12, используется только до шага 4.5.15 (проверка перед cutover);
|
||||||
|
# затем меняется на боевой 192.168.1.25, а OLD (141) останавливается и
|
||||||
|
# остаётся откатом минимум неделю.
|
||||||
|
#
|
||||||
|
# --- Bind mount -> volume ----------------------------------------------------
|
||||||
|
# Эталонный mp0 — bind mount каталога НОДЫ (/opt/data/gitea на cloud-pc), а не
|
||||||
|
# volume на datastore. Provider с root@pam мог бы воспроизвести и bind mount
|
||||||
|
# буквально, но делать это НЕЛЬЗЯ: если по ошибке одновременно поднимутся
|
||||||
|
# оба контейнера, два процесса Gitea будут писать в один и тот же каталог
|
||||||
|
# хоста и повредят SQLite и репозитории. Поэтому mount_point ниже — volume на
|
||||||
|
# том же datastore, что и rootfs ("data"), с независимым хранилищем. Данные
|
||||||
|
# переносятся отдельно на шаге 4.4 (rsync/pct push), а не общим bind mount.
|
||||||
|
# Размер volume — 32G по решению migration-tofu.md (раздел 5, пункт 3):
|
||||||
|
# фактическое использование на 2026-09-02 — 2.3G (`sudo du -sh
|
||||||
|
# /opt/data/gitea`), так что 32G — запас с большим множителем на рост
|
||||||
|
# репозиториев, а не минимально достаточное значение.
|
||||||
|
#
|
||||||
|
# --- /dev/fuse: features.fuse, а не device_passthrough ----------------------
|
||||||
|
# Эталонные строки lxc.cgroup2.devices.allow + lxc.mount.entry для /dev/fuse —
|
||||||
|
# это ручной обход, потому что Proxmox API не принимает сырые lxc.* ключи
|
||||||
|
# (pve-gitea.yml добавляет их через lineinfile). У провайдера bpg/proxmox для
|
||||||
|
# ровно этого эффекта есть штатный флаг PVE features.fuse — это подтверждено
|
||||||
|
# на пилоте (tofu/README.md, раздел «Решение: root@pam по паролю») и явно
|
||||||
|
# названо в комментарии tofu/pilot.tf.example: "У провайдера для этого есть
|
||||||
|
# features.fuse — штатный флаг PVE, а не обход". device_passthrough (dev0:)
|
||||||
|
# нужен только для сырых character-устройств вида /dev/net/tun; у gitea
|
||||||
|
# такого устройства нет (в pct config нет строки dev0:), поэтому блока
|
||||||
|
# device_passthrough в этом ресурсе нет вообще — только features.fuse.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
resource "proxmox_virtual_environment_container" "gitea" {
|
||||||
|
node_name = "cloud-pc"
|
||||||
|
vm_id = 153
|
||||||
|
unprivileged = true
|
||||||
|
start_on_boot = true
|
||||||
|
started = true
|
||||||
|
tags = ["tofu"]
|
||||||
|
|
||||||
|
initialization {
|
||||||
|
hostname = "gitea"
|
||||||
|
|
||||||
|
ip_config {
|
||||||
|
ipv4 {
|
||||||
|
address = "192.168.1.25/24"
|
||||||
|
gateway = "192.168.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dns {
|
||||||
|
servers = ["1.1.1.1"]
|
||||||
|
}
|
||||||
|
|
||||||
|
user_account {
|
||||||
|
keys = [trimspace(file(pathexpand("~/.ssh/id_ed25519_homelab.pub")))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operating_system {
|
||||||
|
template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst"
|
||||||
|
type = "debian"
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu {
|
||||||
|
cores = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
memory {
|
||||||
|
dedicated = 2048
|
||||||
|
swap = 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
disk {
|
||||||
|
datastore_id = "data"
|
||||||
|
size = 32
|
||||||
|
}
|
||||||
|
|
||||||
|
# Эталон: features: nesting=1,keyctl=1 плюс ручной обход для /dev/fuse
|
||||||
|
# (см. комментарий выше). fuse=1 — штатная замена этого обхода.
|
||||||
|
features {
|
||||||
|
nesting = true
|
||||||
|
keyctl = true
|
||||||
|
fuse = true
|
||||||
|
}
|
||||||
|
|
||||||
|
# roles/pve_lxc всем контейнерам ставит cmode=shell (pve_lxc_cmode), провайдер
|
||||||
|
# это отслеживает через блок console.type — без него на следующем apply
|
||||||
|
# откатит на дефолт Proxmox tty. Обнаружено 2026-09-02 на emergency-bot,
|
||||||
|
# см. tofu/README.md.
|
||||||
|
console {
|
||||||
|
type = "shell"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Bind mount ноды заменён на независимый volume — см. комментарий выше.
|
||||||
|
# Тот же datastore, что и rootfs.
|
||||||
|
mount_point {
|
||||||
|
volume = "data"
|
||||||
|
size = "32G"
|
||||||
|
path = "/opt/gitea/data"
|
||||||
|
}
|
||||||
|
|
||||||
|
network_interface {
|
||||||
|
name = "eth0"
|
||||||
|
bridge = "vmbr0"
|
||||||
|
firewall = true
|
||||||
|
}
|
||||||
|
|
||||||
|
startup {
|
||||||
|
order = 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "gitea" {
|
||||||
|
description = "Что проверять на узле после apply"
|
||||||
|
value = {
|
||||||
|
vmid = proxmox_virtual_environment_container.gitea.vm_id
|
||||||
|
node = proxmox_virtual_environment_container.gitea.node_name
|
||||||
|
verify = "ssh cloud-pc sudo pct config ${proxmox_virtual_environment_container.gitea.vm_id}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# Сервис №7 по плану (docs/ai/migration-tofu.md, раздел 5): grimmory.
|
||||||
|
# Самый болезненный шаг: MariaDB, Flyway-миграции, контракт OPDS/KOReader.
|
||||||
|
#
|
||||||
|
# OLD vmid 149, узел cloud-pc, боевой адрес 192.168.1.34.
|
||||||
|
# Эталон снят 2026-09-02: ssh cloud-pc sudo pct config 149 (и совпадающий
|
||||||
|
# дословно /etc/pve/lxc/149.conf):
|
||||||
|
#
|
||||||
|
# arch: amd64
|
||||||
|
# cmode: shell
|
||||||
|
# cores: 2
|
||||||
|
# features: nesting=1,keyctl=1
|
||||||
|
# hostname: grimmory
|
||||||
|
# memory: 4096
|
||||||
|
# nameserver: 1.1.1.1
|
||||||
|
# net0: name=eth0,bridge=vmbr0,firewall=1,gw=192.168.1.1,ip=192.168.1.34/24,type=veth
|
||||||
|
# onboot: 1
|
||||||
|
# ostype: debian
|
||||||
|
# rootfs: data:149/vm-149-disk-0.raw,size=64G
|
||||||
|
# startup: order=100
|
||||||
|
# swap: 1024
|
||||||
|
# unprivileged: 1
|
||||||
|
# lxc.cgroup2.devices.allow: c 10:229 rwm
|
||||||
|
# lxc.mount.entry: /dev/fuse dev/fuse none bind,create=file
|
||||||
|
#
|
||||||
|
# Ни mpN, ни dev0 в эталоне нет — только /dev/fuse через сырые lxc.* строки.
|
||||||
|
# Значит правило про bind mount -> mount_point (как у gitea) здесь не
|
||||||
|
# применяется: mount_point в этом ресурсе не добавлен.
|
||||||
|
#
|
||||||
|
# vm_id 157 — новый VMID для blue-green переезда (назначен, не менять).
|
||||||
|
# TMPIP 192.168.1.16 ниже используется только на шаге 4.3 (проверка перед
|
||||||
|
# cutover; запись grimmory-new уже есть в hosts.yml/ssh_config — заведена
|
||||||
|
# координатором). После шага 4.5.15 адрес меняется на боевой 192.168.1.34,
|
||||||
|
# OLD (149) останавливается и остаётся откатом минимум неделю.
|
||||||
|
#
|
||||||
|
# --- /dev/fuse: features.fuse, а не device_passthrough ----------------------
|
||||||
|
# Эталонные lxc.cgroup2.devices.allow + lxc.mount.entry для /dev/fuse — это
|
||||||
|
# ручной обход (lineinfile по /etc/pve/lxc/149.conf в pve-grimmory.yml),
|
||||||
|
# потому что Proxmox API сырые lxc.* ключи не принимает.
|
||||||
|
#
|
||||||
|
# Заменяется штатным флагом PVE features.fuse, а НЕ device_passthrough.
|
||||||
|
# device_passthrough (dev0:) предназначен для сырых character-устройств вида
|
||||||
|
# /dev/net/tun; для /dev/fuse у PVE есть флаг, и именно он проверен на пилоте
|
||||||
|
# VMID 199 (`features: fuse=1,keyctl=1,nesting=1`), см. tofu/pilot.tf.example
|
||||||
|
# строки 17-21 и tofu/README.md. Формулировка «device_passthrough для каждого
|
||||||
|
# устройства из lxc.mount.entry» в migration-tofu.md п.4.2 была слишком
|
||||||
|
# широкой и уточнена там же 2026-09-02; так же поправлены svc-docker-test.tf
|
||||||
|
# и svc-vaultwarden.tf, так что все ресурсы tofu/ теперь единообразны.
|
||||||
|
#
|
||||||
|
# Следствие: pct config нового контейнера покажет features: fuse=1,keyctl=1,
|
||||||
|
# nesting=1 — текстуально иначе, чем эталон, при том же эффекте.
|
||||||
|
#
|
||||||
|
# --- MariaDB: только dump/restore, не копирование файлов --------------------
|
||||||
|
# /opt/grimmory/mariadb — это datadir MariaDB (grimmory-mariadb, БД grimmory).
|
||||||
|
# Копировать эти файлы между старым и новым контейнером НЕЛЬЗЯ: нужен
|
||||||
|
# mariadb-dump на старом контейнере (сервис приложения остановлен, MariaDB
|
||||||
|
# работает) и restore на новом. Рецепт dump уже есть и проверен в рабочем
|
||||||
|
# offsite-профиле restic (playbooks/offsite-restic-yadisk.yml, задача
|
||||||
|
# ../tasks/offsite-restic-profile.yml): docker exec -e MYSQL_PWD=...
|
||||||
|
# grimmory-mariadb mariadb-dump --single-transaction --routines --events
|
||||||
|
# --databases grimmory. legacy-warning.md и docs/ai/edge-cases.md запрещают
|
||||||
|
# code-only downgrade после Flyway-миграции — на новом контейнере обязан
|
||||||
|
# развернуться РОВНО ТОТ ЖЕ pinned образ v3.2.4 (проверено 2026-09-02:
|
||||||
|
# запущенный в CT 149 образ grimmory/grimmory:v3.2.4 имеет digest
|
||||||
|
# sha256:dfa7afdfcf25d649fd664497a62385dd00cd9678c37546e182c172e41c8e80cb —
|
||||||
|
# совпадает с реестром services.yml байт-в-байт, расхождения нет).
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
resource "proxmox_virtual_environment_container" "grimmory" {
|
||||||
|
node_name = "cloud-pc"
|
||||||
|
vm_id = 157
|
||||||
|
unprivileged = true
|
||||||
|
start_on_boot = true
|
||||||
|
started = true
|
||||||
|
tags = ["tofu"]
|
||||||
|
|
||||||
|
initialization {
|
||||||
|
hostname = "grimmory"
|
||||||
|
|
||||||
|
ip_config {
|
||||||
|
ipv4 {
|
||||||
|
address = "192.168.1.34/24"
|
||||||
|
gateway = "192.168.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dns {
|
||||||
|
servers = ["1.1.1.1"]
|
||||||
|
}
|
||||||
|
|
||||||
|
user_account {
|
||||||
|
keys = [trimspace(file(pathexpand("~/.ssh/id_ed25519_homelab.pub")))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operating_system {
|
||||||
|
template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst"
|
||||||
|
type = "debian"
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu {
|
||||||
|
cores = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
memory {
|
||||||
|
dedicated = 4096
|
||||||
|
swap = 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
disk {
|
||||||
|
datastore_id = "data"
|
||||||
|
size = 64
|
||||||
|
}
|
||||||
|
|
||||||
|
# Эталон: features: nesting=1,keyctl=1 плюс /dev/fuse через сырые lxc.*
|
||||||
|
# строки. fuse=1 — штатная замена этого обхода, см. комментарий файла выше.
|
||||||
|
features {
|
||||||
|
nesting = true
|
||||||
|
keyctl = true
|
||||||
|
fuse = true
|
||||||
|
}
|
||||||
|
|
||||||
|
# roles/pve_lxc всем контейнерам ставит cmode=shell (pve_lxc_cmode), провайдер
|
||||||
|
# это отслеживает через блок console.type — без него на следующем apply
|
||||||
|
# откатит на дефолт Proxmox tty. Обнаружено 2026-09-02 на emergency-bot,
|
||||||
|
# см. tofu/README.md.
|
||||||
|
console {
|
||||||
|
type = "shell"
|
||||||
|
}
|
||||||
|
|
||||||
|
network_interface {
|
||||||
|
name = "eth0"
|
||||||
|
bridge = "vmbr0"
|
||||||
|
firewall = true
|
||||||
|
}
|
||||||
|
|
||||||
|
startup {
|
||||||
|
order = 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "grimmory" {
|
||||||
|
description = "Что проверять на узле после apply"
|
||||||
|
value = {
|
||||||
|
vmid = proxmox_virtual_environment_container.grimmory.vm_id
|
||||||
|
node = proxmox_virtual_environment_container.grimmory.node_name
|
||||||
|
verify = "ssh cloud-pc sudo pct config ${proxmox_virtual_environment_container.grimmory.vm_id}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# Сервис №6: gyro (OLD vmid 150, mini-pc).
|
||||||
|
# Эталон снят 2026-09-02: ssh mini-pc sudo pct config 150.
|
||||||
|
#
|
||||||
|
# vm_id 156 — новый VMID для blue-green переезда, TMPIP 192.168.1.15.
|
||||||
|
# После шага 4.5.15 адрес — боевой 192.168.1.35, OLD (150) остановлен
|
||||||
|
# и остаётся откатом минимум неделю.
|
||||||
|
#
|
||||||
|
# ОСОБЕННОСТИ:
|
||||||
|
# - Контейнер намеренно без nesting (features: "" в эталоне).
|
||||||
|
# - Proxmox-фаервол уже перенесён на VMID 156; Datacenter firewall остаётся выключен.
|
||||||
|
# - Требует Ansible Vault password (make gyro для конфигурации).
|
||||||
|
# - Старое предупреждение про host key Gitea неверно: gyro пинит github.com,
|
||||||
|
# поэтому после cutover менять known_hosts не нужно.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
resource "proxmox_virtual_environment_container" "gyro" {
|
||||||
|
node_name = "mini-pc"
|
||||||
|
vm_id = 156
|
||||||
|
unprivileged = true
|
||||||
|
start_on_boot = true
|
||||||
|
started = true
|
||||||
|
tags = ["tofu"]
|
||||||
|
|
||||||
|
initialization {
|
||||||
|
hostname = "gyro"
|
||||||
|
|
||||||
|
ip_config {
|
||||||
|
ipv4 {
|
||||||
|
address = "192.168.1.35/24"
|
||||||
|
gateway = "192.168.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dns {
|
||||||
|
servers = ["1.1.1.1"]
|
||||||
|
}
|
||||||
|
|
||||||
|
user_account {
|
||||||
|
keys = [trimspace(file(pathexpand("~/.ssh/id_ed25519_homelab.pub")))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operating_system {
|
||||||
|
template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst"
|
||||||
|
type = "debian"
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu {
|
||||||
|
cores = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
memory {
|
||||||
|
dedicated = 512
|
||||||
|
swap = 256
|
||||||
|
}
|
||||||
|
|
||||||
|
disk {
|
||||||
|
datastore_id = "local-lvm"
|
||||||
|
size = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
# Эталон: features: (пусто) — nesting отключён НАМЕРЕННО, no keyctl, no fuse.
|
||||||
|
# Блок features не объявляется, чтобы провайдер не создавал его с дефолтами.
|
||||||
|
# Это консервативное решение: gyro этот функционал не требует.
|
||||||
|
|
||||||
|
# roles/pve_lxc всем контейнерам ставит cmode=shell (pve_lxc_cmode), провайдер
|
||||||
|
# это отслеживает через блок console.type — без него на следующем apply
|
||||||
|
# откатит на дефолт Proxmox tty. Обнаружено 2026-09-02 на emergency-bot.
|
||||||
|
console {
|
||||||
|
type = "shell"
|
||||||
|
}
|
||||||
|
|
||||||
|
network_interface {
|
||||||
|
name = "eth0"
|
||||||
|
bridge = "vmbr0"
|
||||||
|
firewall = true
|
||||||
|
}
|
||||||
|
|
||||||
|
startup {
|
||||||
|
order = 80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "gyro" {
|
||||||
|
description = "Что проверять на узле после apply"
|
||||||
|
value = {
|
||||||
|
vmid = proxmox_virtual_environment_container.gyro.vm_id
|
||||||
|
node = proxmox_virtual_environment_container.gyro.node_name
|
||||||
|
verify = "ssh mini-pc sudo pct config ${proxmox_virtual_environment_container.gyro.vm_id}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# Сервис №9 по плану (docs/ai/migration-tofu.md, раздел 5): mihomo.
|
||||||
|
# Локальный прокси + MetaCubeXD UI. На него завязаны egress hermes-ai
|
||||||
|
# (bash_config_proxy_* в hosts.yml), внешние пробы Uptime Kuma, emergency-бот
|
||||||
|
# и уведомления gyro — простой затрагивает всё это разом.
|
||||||
|
#
|
||||||
|
# OLD vmid 143, узел mini-pc, боевой адрес 192.168.1.27.
|
||||||
|
# Эталон снят 2026-09-02: ssh mini-pc sudo pct config 143 (дословно совпадает
|
||||||
|
# с /etc/pve/lxc/143.conf):
|
||||||
|
#
|
||||||
|
# arch: amd64
|
||||||
|
# cmode: shell
|
||||||
|
# cores: 1
|
||||||
|
# features: nesting=1,keyctl=1
|
||||||
|
# hostname: mihomo
|
||||||
|
# memory: 512
|
||||||
|
# nameserver: 1.1.1.1
|
||||||
|
# net0: name=eth0,bridge=vmbr0,firewall=1,gw=192.168.1.1,ip=192.168.1.27/24,type=veth
|
||||||
|
# onboot: 1
|
||||||
|
# ostype: debian
|
||||||
|
# rootfs: local-lvm:vm-143-disk-0,size=8G
|
||||||
|
# startup: order=70
|
||||||
|
# swap: 512
|
||||||
|
# unprivileged: 1
|
||||||
|
# lxc.cgroup2.devices.allow: c 10:229 rwm # /dev/fuse
|
||||||
|
# lxc.mount.entry: /dev/fuse dev/fuse none bind,create=file
|
||||||
|
# lxc.cgroup2.devices.allow: c 10:200 rwm # /dev/net/tun
|
||||||
|
# lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
|
||||||
|
#
|
||||||
|
# vm_id 159 — новый VMID для blue-green переезда (назначен, не менять).
|
||||||
|
# TMPIP 192.168.1.18 используется только на шаге 4.3. После шага 4.5.15 адрес
|
||||||
|
# меняется на боевой 192.168.1.27, OLD (143) останавливается и остаётся
|
||||||
|
# откатом минимум неделю.
|
||||||
|
#
|
||||||
|
# --- Два устройства, ДВА разных механизма -----------------------------------
|
||||||
|
# /dev/fuse -> штатный флаг features.fuse (Docker внутри на fuse-overlayfs).
|
||||||
|
# /dev/net/tun -> блок device_passthrough (dev0: в PVE 8.2+). mihomo.service
|
||||||
|
# запускает контейнер с `--device /dev/net/tun` для TUN-режима.
|
||||||
|
# Это первый боевой сервис, использующий device_passthrough: на пилоте VMID
|
||||||
|
# 199 механизм проверен (`dev0: deny-write=0,path=/dev/net/tun,...`, повторный
|
||||||
|
# plan -> No changes), см. tofu/pilot.tf.example и tofu/README.md. Оба требуют
|
||||||
|
# root@pam; в обычном токенном режиме Proxmox отвечает 403.
|
||||||
|
# Следствие: pct config покажет `features: fuse=1,...` и `dev0: path=/dev/net/tun`
|
||||||
|
# вместо сырых lxc.* строк эталона — тот же эффект штатным механизмом.
|
||||||
|
#
|
||||||
|
# --- Данные ----------------------------------------------------------------
|
||||||
|
# /opt/mihomo (~80 КБ): config/config.yaml (реальный конфиг прокси),
|
||||||
|
# config/cache.db, config/providers/main.yaml. Переносится целиком на шаге
|
||||||
|
# 4.4. Задача "Install default mihomo config if missing" в pve-mihomo.yml
|
||||||
|
# идёт с force: false, поэтому перенесённый конфиг не перетирается.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
resource "proxmox_virtual_environment_container" "mihomo" {
|
||||||
|
node_name = "mini-pc"
|
||||||
|
vm_id = 159
|
||||||
|
unprivileged = true
|
||||||
|
start_on_boot = true
|
||||||
|
started = true
|
||||||
|
tags = ["tofu"]
|
||||||
|
|
||||||
|
initialization {
|
||||||
|
hostname = "mihomo"
|
||||||
|
|
||||||
|
ip_config {
|
||||||
|
ipv4 {
|
||||||
|
address = "192.168.1.27/24"
|
||||||
|
gateway = "192.168.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dns {
|
||||||
|
servers = ["1.1.1.1"]
|
||||||
|
}
|
||||||
|
|
||||||
|
user_account {
|
||||||
|
keys = [trimspace(file(pathexpand("~/.ssh/id_ed25519_homelab.pub")))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operating_system {
|
||||||
|
template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst"
|
||||||
|
type = "debian"
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu {
|
||||||
|
cores = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
memory {
|
||||||
|
dedicated = 512
|
||||||
|
swap = 512
|
||||||
|
}
|
||||||
|
|
||||||
|
disk {
|
||||||
|
datastore_id = "local-lvm"
|
||||||
|
size = 8
|
||||||
|
}
|
||||||
|
|
||||||
|
# Эталон: features: nesting=1,keyctl=1 плюс /dev/fuse через сырые lxc.*
|
||||||
|
# строки. fuse=1 — штатная замена этого обхода (см. комментарий выше).
|
||||||
|
features {
|
||||||
|
nesting = true
|
||||||
|
keyctl = true
|
||||||
|
fuse = true
|
||||||
|
}
|
||||||
|
|
||||||
|
# /dev/net/tun: в эталоне проброшен сырыми lxc.cgroup2.devices.allow +
|
||||||
|
# lxc.mount.entry. Штатная декларация — device_passthrough (dev0:).
|
||||||
|
device_passthrough {
|
||||||
|
path = "/dev/net/tun"
|
||||||
|
}
|
||||||
|
|
||||||
|
# roles/pve_lxc всем контейнерам ставит cmode=shell (pve_lxc_cmode), провайдер
|
||||||
|
# это отслеживает через блок console.type — без него на следующем apply
|
||||||
|
# откатит на дефолт Proxmox tty. Обнаружено 2026-09-02 на emergency-bot,
|
||||||
|
# см. tofu/README.md.
|
||||||
|
console {
|
||||||
|
type = "shell"
|
||||||
|
}
|
||||||
|
|
||||||
|
network_interface {
|
||||||
|
name = "eth0"
|
||||||
|
bridge = "vmbr0"
|
||||||
|
firewall = true
|
||||||
|
}
|
||||||
|
|
||||||
|
startup {
|
||||||
|
order = 70
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "mihomo" {
|
||||||
|
description = "Что проверять на узле после apply"
|
||||||
|
value = {
|
||||||
|
vmid = proxmox_virtual_environment_container.mihomo.vm_id
|
||||||
|
node = proxmox_virtual_environment_container.mihomo.node_name
|
||||||
|
verify = "ssh mini-pc sudo pct config ${proxmox_virtual_environment_container.mihomo.vm_id}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# Сервис №5: monitoring (OLD vmid 146, cloud-pc).
|
||||||
|
# Эталон снят 2026-09-02: ssh cloud-pc sudo pct config 146 (совпадает с
|
||||||
|
# cat /etc/pve/lxc/146.conf дословно).
|
||||||
|
#
|
||||||
|
# vm_id 155 — новый VMID для blue-green переезда, TMPIP 192.168.1.14
|
||||||
|
# используется только на шаге 4.3 (проверка перед cutover). После шага 4.5.15
|
||||||
|
# адрес — боевой 192.168.1.30 (тот же, что и у OLD: в этой схеме переезжает
|
||||||
|
# VMID, а не IP), OLD (146) остановлен и остаётся откатом минимум неделю.
|
||||||
|
#
|
||||||
|
# Активный сервис — Uptime Kuma: SQLite в /opt/uptime-kuma/data/kuma.db,
|
||||||
|
# UI-состояние (мониторы, уведомления) в Ansible не описано, каталог
|
||||||
|
# переносится целиком на шаге 4.4. Стек Prometheus + Alertmanager + Grafana
|
||||||
|
# в этом контейнере ЗАМОРОЖЕН (docs/ai/legacy-warning.md) — его данные лежат
|
||||||
|
# отдельно в /opt/monitoring (в основном TSDB Prometheus) и в новый контейнер
|
||||||
|
# намеренно НЕ переносятся; решение о судьбе замороженного стека отдельное,
|
||||||
|
# не часть этого переезда.
|
||||||
|
#
|
||||||
|
# ОСОБЕННОСТЬ features: реестр (services.yml) отмечает, что роль создаёт
|
||||||
|
# контейнер с nesting=1, а keyctl=1 добавляет отдельной задачей `pct set 146
|
||||||
|
# --features nesting=1,keyctl=1` уже после создания (playbooks/pve-monitoring.yml).
|
||||||
|
# Здесь воспроизведён ЭТАЛОН как он есть на живом контейнере — оба флага сразу,
|
||||||
|
# декларативно, без промежуточного шага: в привилегированном режиме root@pam
|
||||||
|
# (см. tofu/README.md, «Решение: root@pam по паролю») дошаг `pct set` для этого
|
||||||
|
# и остальных сервисов больше не нужен, features применяются одной фазой.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
resource "proxmox_virtual_environment_container" "monitoring" {
|
||||||
|
node_name = "cloud-pc"
|
||||||
|
vm_id = 155
|
||||||
|
unprivileged = true
|
||||||
|
start_on_boot = true
|
||||||
|
started = true
|
||||||
|
tags = ["tofu"]
|
||||||
|
|
||||||
|
initialization {
|
||||||
|
hostname = "monitoring"
|
||||||
|
|
||||||
|
ip_config {
|
||||||
|
ipv4 {
|
||||||
|
address = "192.168.1.30/24"
|
||||||
|
gateway = "192.168.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dns {
|
||||||
|
servers = ["1.1.1.1"]
|
||||||
|
}
|
||||||
|
|
||||||
|
user_account {
|
||||||
|
keys = [trimspace(file(pathexpand("~/.ssh/id_ed25519_homelab.pub")))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operating_system {
|
||||||
|
template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst"
|
||||||
|
type = "debian"
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu {
|
||||||
|
cores = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
memory {
|
||||||
|
dedicated = 4096
|
||||||
|
swap = 512
|
||||||
|
}
|
||||||
|
|
||||||
|
disk {
|
||||||
|
datastore_id = "data"
|
||||||
|
size = 24
|
||||||
|
}
|
||||||
|
|
||||||
|
# Эталон: features: nesting=1,keyctl=1 — см. ОСОБЕННОСТЬ в комментарии выше.
|
||||||
|
# Ни fuse, ни device passthrough эталону не нужны (dev0 в pct config нет).
|
||||||
|
features {
|
||||||
|
nesting = true
|
||||||
|
keyctl = true
|
||||||
|
}
|
||||||
|
|
||||||
|
# roles/pve_lxc всем контейнерам ставит cmode=shell (pve_lxc_cmode), провайдер
|
||||||
|
# это отслеживает через блок console.type — без него на следующем apply
|
||||||
|
# откатит на дефолт Proxmox tty. Обнаружено 2026-09-02 на emergency-bot,
|
||||||
|
# см. tofu/README.md.
|
||||||
|
console {
|
||||||
|
type = "shell"
|
||||||
|
}
|
||||||
|
|
||||||
|
network_interface {
|
||||||
|
name = "eth0"
|
||||||
|
bridge = "vmbr0"
|
||||||
|
firewall = true
|
||||||
|
}
|
||||||
|
|
||||||
|
startup {
|
||||||
|
order = 80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "monitoring" {
|
||||||
|
description = "Что проверять на узле после apply"
|
||||||
|
value = {
|
||||||
|
vmid = proxmox_virtual_environment_container.monitoring.vm_id
|
||||||
|
node = proxmox_virtual_environment_container.monitoring.node_name
|
||||||
|
verify = "ssh cloud-pc sudo pct config ${proxmox_virtual_environment_container.monitoring.vm_id}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# Сервис №10 по плану (docs/ai/migration-tofu.md, раздел 5): ovpn-mini.
|
||||||
|
# Терминирует OpenVPN-туннель ru-vps <-> LAN (gateway-конец, 10.78.0.2).
|
||||||
|
# Через него идёт ProxyJump ко всем LXC вне LAN и обратный путь reverse-proxy.
|
||||||
|
# ПЕРЕЕЗД ТОЛЬКО ИЗ ЛОКАЛЬНОЙ СЕТИ: вне LAN прогон оборвёт себе транспорт.
|
||||||
|
#
|
||||||
|
# OLD vmid 132, узел mini-pc, боевой адрес 192.168.1.23.
|
||||||
|
# Эталон снят 2026-09-02: ssh mini-pc sudo pct config 132 (дословно совпадает
|
||||||
|
# с /etc/pve/lxc/132.conf):
|
||||||
|
#
|
||||||
|
# arch: amd64
|
||||||
|
# cmode: shell
|
||||||
|
# cores: 1
|
||||||
|
# features: nesting=1
|
||||||
|
# hostname: ovpn-mini
|
||||||
|
# memory: 256
|
||||||
|
# nameserver: 1.1.1.1
|
||||||
|
# net0: name=eth0,bridge=vmbr0,firewall=1,gw=192.168.1.1,ip=192.168.1.23/24,type=veth
|
||||||
|
# onboot: 1
|
||||||
|
# ostype: debian
|
||||||
|
# rootfs: local-lvm:vm-132-disk-0,size=8G
|
||||||
|
# startup: order=30
|
||||||
|
# swap: 128
|
||||||
|
# unprivileged: 1
|
||||||
|
# lxc.cgroup2.devices.allow: c 10:200 rwm # /dev/net/tun
|
||||||
|
# lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file
|
||||||
|
#
|
||||||
|
# vm_id 160 — новый VMID для blue-green переезда (назначен, не менять).
|
||||||
|
# TMPIP 192.168.1.19 используется только на шаге 4.3. После шага 4.5.15 адрес
|
||||||
|
# меняется на боевой 192.168.1.23, OLD (132) останавливается и остаётся
|
||||||
|
# откатом минимум неделю.
|
||||||
|
#
|
||||||
|
# --- features: только nesting ---------------------------------------------
|
||||||
|
# Эталон: features: nesting=1 — ни keyctl, ни fuse. Оставлено как есть:
|
||||||
|
# OpenVPN-шлюзу этот функционал не нужен.
|
||||||
|
#
|
||||||
|
# --- /dev/net/tun: device_passthrough ------------------------------------
|
||||||
|
# В эталоне проброшен сырыми lxc.cgroup2.devices.allow + lxc.mount.entry
|
||||||
|
# (play "Allow TUN device" в pve-ovpn-mini.yml). Штатная декларация —
|
||||||
|
# device_passthrough (dev0:), проверена на пилоте VMID 199 и на mihomo.
|
||||||
|
# Требует root@pam.
|
||||||
|
#
|
||||||
|
# --- Конфигурация и данные ----------------------------------------------
|
||||||
|
# pve-ovpn-mini.yml конфигурационной части НЕ содержит (только создание +
|
||||||
|
# проброс TUN). Настраивает playbooks/openvpn-vps-mini.yml (роль
|
||||||
|
# openvpn_gateway, группа vpn_openvpn = ru-vps + ovpn-mini).
|
||||||
|
# Единственные данные для переноса: /etc/openvpn/homelab/static.key —
|
||||||
|
# общий статический ключ с ru-vps. Остальное (homelab.conf, up.sh, down.sh,
|
||||||
|
# systemd-юнит) роль рендерит заново. LAN-адрес ovpn-mini ни в одном шаблоне
|
||||||
|
# роли не фигурирует (masquerade идёт по `-o eth0`), поэтому туннель не
|
||||||
|
# зависит от смены адреса — меняется только VMID.
|
||||||
|
#
|
||||||
|
# --- roles/pve_lxc -----------------------------------------------------
|
||||||
|
# До этого переезда ovpn-mini был единственным потребителем roles/pve_lxc.
|
||||||
|
# После него роль не подключена нигде — кандидат на удаление, см.
|
||||||
|
# docs/ai/migration-tofu.md раздел 6.
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
resource "proxmox_virtual_environment_container" "ovpn_mini" {
|
||||||
|
node_name = "mini-pc"
|
||||||
|
vm_id = 160
|
||||||
|
unprivileged = true
|
||||||
|
start_on_boot = true
|
||||||
|
started = true
|
||||||
|
tags = ["tofu"]
|
||||||
|
|
||||||
|
initialization {
|
||||||
|
hostname = "ovpn-mini"
|
||||||
|
|
||||||
|
ip_config {
|
||||||
|
ipv4 {
|
||||||
|
address = "192.168.1.23/24"
|
||||||
|
gateway = "192.168.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dns {
|
||||||
|
servers = ["1.1.1.1"]
|
||||||
|
}
|
||||||
|
|
||||||
|
user_account {
|
||||||
|
keys = [trimspace(file(pathexpand("~/.ssh/id_ed25519_homelab.pub")))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operating_system {
|
||||||
|
template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst"
|
||||||
|
type = "debian"
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu {
|
||||||
|
cores = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
memory {
|
||||||
|
dedicated = 256
|
||||||
|
swap = 128
|
||||||
|
}
|
||||||
|
|
||||||
|
disk {
|
||||||
|
datastore_id = "local-lvm"
|
||||||
|
size = 8
|
||||||
|
}
|
||||||
|
|
||||||
|
# Эталон: features: nesting=1 (без keyctl/fuse).
|
||||||
|
features {
|
||||||
|
nesting = true
|
||||||
|
}
|
||||||
|
|
||||||
|
# /dev/net/tun: в эталоне — сырые lxc.* строки. Штатная декларация dev0:.
|
||||||
|
device_passthrough {
|
||||||
|
path = "/dev/net/tun"
|
||||||
|
}
|
||||||
|
|
||||||
|
# roles/pve_lxc всем контейнерам ставит cmode=shell (pve_lxc_cmode), провайдер
|
||||||
|
# это отслеживает через блок console.type — без него на следующем apply
|
||||||
|
# откатит на дефолт Proxmox tty. Обнаружено 2026-09-02 на emergency-bot,
|
||||||
|
# см. tofu/README.md.
|
||||||
|
console {
|
||||||
|
type = "shell"
|
||||||
|
}
|
||||||
|
|
||||||
|
network_interface {
|
||||||
|
name = "eth0"
|
||||||
|
bridge = "vmbr0"
|
||||||
|
firewall = true
|
||||||
|
}
|
||||||
|
|
||||||
|
startup {
|
||||||
|
order = 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "ovpn_mini" {
|
||||||
|
description = "Что проверять на узле после apply"
|
||||||
|
value = {
|
||||||
|
vmid = proxmox_virtual_environment_container.ovpn_mini.vm_id
|
||||||
|
node = proxmox_virtual_environment_container.ovpn_mini.node_name
|
||||||
|
verify = "ssh mini-pc sudo pct config ${proxmox_virtual_environment_container.ovpn_mini.vm_id}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# Сервис №4: vaultwarden (OLD vmid 140, mini-pc).
|
||||||
|
# Эталон снят 2026-09-02: ssh mini-pc sudo pct config 140 / cat /etc/pve/lxc/140.conf.
|
||||||
|
# Данные критичны: SQLite /opt/vaultwarden/data/db.sqlite3 (плюс config.json,
|
||||||
|
# rsa_key.pem, icon_cache/). Переносятся вручную на шаге 4.4, а не этим
|
||||||
|
# ресурсом — контейнер создаётся пустым, данные переезжают отдельной
|
||||||
|
# процедурой (см. docs/ai/migration-tofu.md, п.5.4).
|
||||||
|
#
|
||||||
|
# vm_id 154. TMPIP 192.168.1.13 отработал на шагах 4.3 и 4.4; cutover выполнен
|
||||||
|
# 2026-09-02, адрес боевой — 192.168.1.24. Данные перенесены и сверены:
|
||||||
|
# integrity_check ok, 1 users / 314 ciphers совпали с боевыми.
|
||||||
|
# OLD (140) остановлен и остаётся откатом минимум неделю, до 2026-09-09
|
||||||
|
# (docs/ai/migration-tofu.md, инвариант №2).
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
resource "proxmox_virtual_environment_container" "vaultwarden" {
|
||||||
|
node_name = "mini-pc"
|
||||||
|
vm_id = 154
|
||||||
|
unprivileged = true
|
||||||
|
start_on_boot = true
|
||||||
|
started = true
|
||||||
|
tags = ["tofu"]
|
||||||
|
|
||||||
|
initialization {
|
||||||
|
hostname = "vaultwarden"
|
||||||
|
|
||||||
|
ip_config {
|
||||||
|
ipv4 {
|
||||||
|
address = "192.168.1.24/24"
|
||||||
|
gateway = "192.168.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dns {
|
||||||
|
servers = ["1.1.1.1"]
|
||||||
|
}
|
||||||
|
|
||||||
|
user_account {
|
||||||
|
keys = [trimspace(file(pathexpand("~/.ssh/id_ed25519_homelab.pub")))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operating_system {
|
||||||
|
template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst"
|
||||||
|
type = "debian"
|
||||||
|
}
|
||||||
|
|
||||||
|
cpu {
|
||||||
|
cores = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
memory {
|
||||||
|
dedicated = 1024
|
||||||
|
swap = 512
|
||||||
|
}
|
||||||
|
|
||||||
|
disk {
|
||||||
|
datastore_id = "local-lvm"
|
||||||
|
size = 16
|
||||||
|
}
|
||||||
|
|
||||||
|
# Эталон: features: nesting=1,keyctl=1 плюс /dev/fuse, проброшенный вручную
|
||||||
|
# двумя строками в /etc/pve/lxc/140.conf (lxc.cgroup2.devices.allow c 10:229
|
||||||
|
# rwm + lxc.mount.entry) — Proxmox API сырые lxc.* ключи не принимает,
|
||||||
|
# поэтому pve-vaultwarden.yml добавляет их через lineinfile.
|
||||||
|
#
|
||||||
|
# ЗАМЕНА ЭТОГО ОБХОДА — features.fuse, а НЕ device_passthrough.
|
||||||
|
# device_passthrough (dev0:) нужен только для сырых character-устройств
|
||||||
|
# вида /dev/net/tun. Для /dev/fuse у PVE есть штатный флаг, и именно он
|
||||||
|
# проверен на пилоте VMID 199 — результат был
|
||||||
|
# `features: fuse=1,keyctl=1,nesting=1`, см. tofu/pilot.tf.example:17-21
|
||||||
|
# и tofu/README.md. Формулировка «device_passthrough для каждого устройства»
|
||||||
|
# в migration-tofu.md п.4.2 слишком широкая; она уточнена там же.
|
||||||
|
#
|
||||||
|
# Следствие: pct config нового контейнера покажет
|
||||||
|
# `features: fuse=1,keyctl=1,nesting=1` — текстуально иначе, чем эталон,
|
||||||
|
# но это тот же эффект штатным механизмом вместо обхода.
|
||||||
|
features {
|
||||||
|
nesting = true
|
||||||
|
keyctl = true
|
||||||
|
fuse = true
|
||||||
|
}
|
||||||
|
|
||||||
|
# roles/pve_lxc всем контейнерам ставит cmode=shell (pve_lxc_cmode), провайдер
|
||||||
|
# это отслеживает через блок console.type — без него на следующем apply
|
||||||
|
# откатит на дефолт Proxmox tty (найдено на emergency-bot, см. tofu/README.md).
|
||||||
|
console {
|
||||||
|
type = "shell"
|
||||||
|
}
|
||||||
|
|
||||||
|
network_interface {
|
||||||
|
name = "eth0"
|
||||||
|
bridge = "vmbr0"
|
||||||
|
firewall = true
|
||||||
|
}
|
||||||
|
|
||||||
|
startup {
|
||||||
|
order = 40
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "vaultwarden" {
|
||||||
|
description = "Что проверять на узле после apply"
|
||||||
|
value = {
|
||||||
|
vmid = proxmox_virtual_environment_container.vaultwarden.vm_id
|
||||||
|
node = proxmox_virtual_environment_container.vaultwarden.node_name
|
||||||
|
verify = "ssh mini-pc sudo pct config ${proxmox_virtual_environment_container.vaultwarden.vm_id}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Значения приходят из корневого .env через Make (цели tofu-* в ansible/Makefile),
|
||||||
|
# который перекладывает PROXMOX_* в TF_VAR_*. Отдельного файла с секретами нет.
|
||||||
|
|
||||||
|
variable "pve_endpoint" {
|
||||||
|
description = "URL Proxmox API. Цели tofu-* направляют его в локальный конец SSH-туннеля."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Аутентификация ---------------------------------------------------------
|
||||||
|
# Ровно один из двух способов, выбор делает Makefile:
|
||||||
|
#
|
||||||
|
# токен ansible@pve — обычный режим. Не может features кроме nesting,
|
||||||
|
# device passthrough и bind mount каталога хоста.
|
||||||
|
# root@pam + пароль — привилегированный режим. Проверка в Proxmox буквальная
|
||||||
|
# (`$authuser eq 'root@pam'`), поэтому токен, даже
|
||||||
|
# принадлежащий root, её не проходит — нужен именно пароль.
|
||||||
|
|
||||||
|
variable "pve_api_token" {
|
||||||
|
description = "Токен в формате user@realm!tokenid=secret. Пустая строка — не использовать."
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "pve_username" {
|
||||||
|
description = "Пользователь для парольной аутентификации, обычно root@pam. Пустая строка — не использовать."
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "pve_password" {
|
||||||
|
description = "Пароль root@pam. Пустая строка — не использовать."
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "pve_insecure" {
|
||||||
|
description = "Не проверять TLS-сертификат Proxmox"
|
||||||
|
type = bool
|
||||||
|
default = true
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.6"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
proxmox = {
|
||||||
|
source = "bpg/proxmox"
|
||||||
|
version = "~> 0.84"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user