From d2e1e6876a2565d1dc7c824863bc2e19f30f78ac Mon Sep 17 00:00:00 2001 From: Dmitry Date: Thu, 3 Sep 2026 07:06:10 +0300 Subject: [PATCH] docs: AI project context (docs/ai) and repository documentation refresh - docs/ai/: stable, repo-verified context - README, architecture, tech-stack, edge-cases, plan (confirmed active work only), migration-tofu (the blue-green OpenTofu migration runbook and per-service findings), legacy-warning, links. - AGENTS.md: slimmed to a working contract that points at docs/ai instead of restating it; CLAUDE.md is an adapter that @-includes it. - README.md, ansible/README.md, ansible/roles/README.md, roles/lxc_docker_host/README.md: bring wording in line with the current control plane (Makefile entry point, registry, tofu, memoir-bot gone). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_012uoq5AVK8mkBgg83Mq6o5V --- AGENTS.md | 443 ++++--------- README.md | 15 +- ansible/README.md | 95 +-- ansible/roles/README.md | 17 +- ansible/roles/lxc_docker_host/README.md | 4 +- docs/ai/README.md | 68 ++ docs/ai/architecture.md | 142 +++++ docs/ai/edge-cases.md | 104 ++++ docs/ai/legacy-warning.md | 75 +++ docs/ai/links.md | 20 + docs/ai/migration-tofu.md | 788 ++++++++++++++++++++++++ docs/ai/plan.md | 485 +++++++++++++++ docs/ai/tech-stack.md | 76 +++ 13 files changed, 1955 insertions(+), 377 deletions(-) create mode 100644 docs/ai/README.md create mode 100644 docs/ai/architecture.md create mode 100644 docs/ai/edge-cases.md create mode 100644 docs/ai/legacy-warning.md create mode 100644 docs/ai/links.md create mode 100644 docs/ai/migration-tofu.md create mode 100644 docs/ai/plan.md create mode 100644 docs/ai/tech-stack.md diff --git a/AGENTS.md b/AGENTS.md index 075cd35..b618161 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,361 +1,154 @@ # 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) -- 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) +Стабильный контекст проекта находится в [`docs/ai/`](docs/ai/README.md). Не +загружай весь набор по умолчанию: читай только документы, относящиеся к задаче. -## 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. -- **Update** after making changes — document non-obvious decisions, new patterns, lessons learned. -- Key files: `HomeLab.md`, `Notes/Текущее состояние HomeLab после миграции на Proxmox.md`, `Log.md`. +Основные заметки: `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. -Окружение собрано в 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 -nix develop # или один раз: direnv allow - -# Один раз на клон: galaxy-коллекции -ansible-galaxy collection install -r ansible/requirements.yml -p ansible/collections +nix develop # или один раз: direnv allow +ansible-galaxy collection install \ + -r ansible/requirements.yml \ + -p ansible/collections # один раз на clone ``` -Дальше всё управление идёт через `make` из каталога `ansible/`: +Работай через Make из `ansible/` или с `make -C ansible` из root: ```bash -cd ansible -make help # список всех целей — начинать отсюда -make check # связность и ожидаемые IP -make status # сводное состояние всей инфраструктуры (read-only) -make lint # ansible-lint + yamllint -make docs # актуальная таблица хостов из inventory - -make deploy-gitea # playbooks/pve-gitea.yml, .env подхватывается сам -make dry-gitea # то же в режиме --check --diff -make update-gitea # бэкап -> обновление -> health-check +make -C ansible help +make -C ansible check +make -C ansible status EXTRA="--limit '!gyro'" +make -C ansible inventory +make -C ansible docs +make -C ansible lint ``` -`make` без аргументов печатает `help`. Опасные цели (`mihomo-harden`, `monitoring`, -`update-all`) требуют явного `CONFIRM=1`. Дополнительные флаги — через `EXTRA`: +Локальный syntax-check всех playbooks, соответствующий CI: ```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`, поэтому для него нужен -`--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` и защищает опасные цели. +Grimmory MCP требует отдельный Node.js `>=22` runtime: ```bash -make check # связность и ожидаемые IP -make status # read-only сводка: хосты, юниты, диски, VPN, бэкапы -make deploy- # playbooks/pve-.yml -make dry- # то же с --check --diff -make update- # 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 # разложить публичный ключ на все хосты +npm install --prefix tools/grimmory-mcp +npm test --prefix tools/grimmory-mcp ``` -Обновления всегда идут в порядке: свежий бэкап/аудит -> обновление -> health-check. -Образы закреплены по `tag@sha256:digest`; floating-теги и авто-апдейтеры не используются. -## Proxmox API Tasks +## Safety Contract -Плейбукам `pve-*.yml` нужны переменные Proxmox API. Держи их в `ansible/.env` -(файл в `.gitignore`, шаблон — `.env.example`): +- Сначала выполняй smallest safe local validation, затем bounded live check только + когда он нужен задаче. +- `make dry-` использует `--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 -PROXMOX_HOST= -PROXMOX_USER= -PROXMOX_TOKEN_ID= -PROXMOX_TOKEN_SECRET= -PROXMOX_VALIDATE_CERTS=false -``` +## Secrets -`make` подхватывает `.env` сам и падает с внятным сообщением, если его нет — -руками `. ./.env` делать не нужно: +- Никогда не коммить и не цитировать реальные passwords, tokens, private keys, + `.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 -make bootstrap-pve-token # прямо с ноды, нужен sudo -make bootstrap-monitoring-token # отдельный read-only токен для мониторинга -``` - -## 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 сами разберут шаги. +Для read-only reconnaissance, Ansible safety review, syntax validation, network/log +diagnostics, backup audit и Obsidian context используй специализированные агенты из +`.opencode/agents/`. Большие logs и command outputs передавай соответствующему +read-only summarizer и всегда ограничивай `--since`, `-n` или `--tail`. diff --git a/README.md b/README.md index a4bbbd7..aefee91 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ Активная инфраструктура домашней лаборатории управляется через Ansible. Каноничные инструкции для людей и агентов — в [AGENTS.md](./AGENTS.md). +Стабильный архитектурный контекст и риски — в [docs/ai/](./docs/ai/README.md). ## Быстрый старт @@ -32,7 +33,8 @@ make deploy-gitea # применить make update-gitea # бэкап -> обновление -> health-check ``` -`.env` с Proxmox-токенами подхватывается автоматически. Опасные цели требуют `CONFIRM=1`. +Секреты лежат в `.env` в **корне репозитория** (в `.gitignore`); его подхватывают +и Ansible, и OpenTofu. Опасные цели требуют `CONFIRM=1`. ## SSH руками @@ -49,12 +51,13 @@ Include /home/ada/Documents/Projects/HomeLab/infras/ansible/ssh_config - `ansible/inventory/group_vars/all/services.yml` — реестр сервисов (VMID, IP, порты, домены, образы) - `flake.nix` — dev-окружение - `.gitea/workflows/lint.yml` — CI: yamllint, ansible-lint, syntax-check +- `docs/ai/` — архитектура, stack, edge cases и legacy boundaries для агентов - `archive/2026-07-proxmox-migration/` — исторические NixOS/Docker конфиги, только как справка ## Grimmory MCP -`tools/grimmory-mcp/` содержит read-only интеграцию с Grimmory API для OpenCode -и явно вызываемые инструменты синхронизации с Obsidian. +`tools/grimmory-mcp/` содержит read-only интеграцию с Grimmory API для OpenCode. +Явно вызываемые sync tools записывают сгенерированные заметки и обложки в Obsidian. ```bash npm install --prefix tools/grimmory-mcp @@ -62,6 +65,6 @@ npm run configure --prefix tools/grimmory-mcp npm test --prefix tools/grimmory-mcp ``` -OpenCode регистрирует сервер глобально. После перезапуска OpenCode используй -`/grimmory-sync` для обновления заметок книг в `90 Library/Books` и обложек -в `99 System/Export/Grimmory/Covers`. +Глобальная регистрация MCP в OpenCode выполняется вне этого репозитория. После +настройки используй `/grimmory-sync` для обновления заметок книг в +`90 Library/Books` и обложек в `99 System/Export/Grimmory/Covers`. diff --git a/ansible/README.md b/ansible/README.md index 5c25375..a449224 100644 --- a/ansible/README.md +++ b/ansible/README.md @@ -12,8 +12,10 @@ Ansible is the control plane for HomeLab infrastructure changes. ## Layout - `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. - `roles/` — reusable configuration units. +- `Makefile` — canonical manual entry point and safety gates. ## Current Groups @@ -24,38 +26,43 @@ Ansible is the control plane for HomeLab infrastructure changes. - `monitoring_exporters` — hosts exposing Node Exporter metrics. - `monitoring_smart_exporters` — Proxmox nodes exposing SMART metrics. - `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. ## First Checks -Install control-node dependencies locally: +From the repository root, use the Nix environment and install Galaxy collections once per clone: ```bash -python3 -m venv .venv -. .venv/bin/activate -pip install -r requirements.txt -ansible-galaxy collection install -r requirements.yml -p collections +nix develop +ansible-galaxy collection install -r ansible/requirements.yml -p ansible/collections ``` -Run from `ansible/`: +Run manual operations through Make from `ansible/`: ```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 -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 -.venv/bin/ansible-playbook playbooks/vaultwarden-update.yml -.venv/bin/ansible-playbook playbooks/gitea-update.yml -.venv/bin/ansible-playbook playbooks/adguard-update.yml -.venv/bin/ansible-playbook playbooks/mihomo-update.yml -.venv/bin/ansible-playbook playbooks/grimmory-update.yml +make update-vaultwarden +make update-gitea +make update-adguard +make update-mihomo +make update-grimmory ``` 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: ```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`. 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 -cp .env.example .env -. ./.env -.venv/bin/ansible-playbook playbooks/pve-ovpn-mini.yml +cp ../.env.example ../.env # секреты живут в корне репозитория +make env-check +make dry-ovpn-mini +make deploy-ovpn-mini ``` Or bootstrap the token from `mini-pc` with sudo: ```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: ```bash -.venv/bin/ansible-playbook playbooks/bootstrap-monitoring-pve-token.yml +make bootstrap-monitoring-token ``` OpenVPN transport: ```bash -.venv/bin/ansible-playbook playbooks/openvpn-vps-mini.yml -K -.venv/bin/ansible-playbook playbooks/openvpn-check.yml +make openvpn +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 -. ./.env -.venv/bin/ansible-playbook playbooks/pve-monitoring.yml -.venv/bin/ansible-playbook playbooks/monitoring.yml +make deploy-monitoring +make uptime-kuma ``` -`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 -`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: @@ -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. ```bash -. ./.env -.venv/bin/ansible-playbook playbooks/pve-gyro.yml -.venv/bin/ansible-playbook playbooks/gyro.yml --ask-vault-pass +make gyro ``` -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. ```bash -.venv/bin/ansible-playbook playbooks/uptime-kuma.yml +make uptime-kuma ``` ## 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. ```bash -. ./.env -.venv/bin/ansible-playbook playbooks/pve-emergency-bot.yml -.venv/bin/ansible-playbook playbooks/emergency-access.yml +make deploy-emergency-bot +make emergency-access ``` 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: ```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: ```bash -ansible-playbook playbooks/.yml -K +make play- EXTRA="-K" ``` ## Workflow diff --git a/ansible/roles/README.md b/ansible/roles/README.md index 625ba80..fce0549 100644 --- a/ansible/roles/README.md +++ b/ansible/roles/README.md @@ -36,12 +36,23 @@ systemd-юнит `Type=oneshot` с `docker compose up -d --remove-orphans`, Подробности и пример плейбука Gitea на новых ролях: [`compose_service/README.md`](compose_service/README.md). -**Статус:** роли созданы и проверены синтаксически, но пока не подключены ни -к одному живому сервису. Перевод `pve-*.yml` на них — отдельный этап. +**Статус:** `compose_service` подключён в `playbooks/ru-vps-base.yml` (стек +Caddy на ru-vps) — это его первый и пока единственный потребитель. +`lxc_docker_host` не подключён нигде. Перевод `pve-*.yml` на обе роли — +отдельный этап. ## Источник данных Факты о сервисах (vmid, узел, адрес, порты, домен, образы с digest, ресурсы, бэкап, мониторинг, порядок автозапуска) собраны в реестре `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) по-прежнему +дублирует значения и должно меняться согласованно. diff --git a/ansible/roles/lxc_docker_host/README.md b/ansible/roles/lxc_docker_host/README.md index 72fb62a..539b5ae 100644 --- a/ansible/roles/lxc_docker_host/README.md +++ b/ansible/roles/lxc_docker_host/README.md @@ -5,8 +5,8 @@ Docker»: пакеты, проверка `/dev/fuse`, `storage-driver: fuse-over запуск демона и базовый UFW. Роль вынесена из повторяющихся блоков `playbooks/pve-*.yml` -(gitea, vaultwarden, mihomo, adguard, memoir-bot, docker-test, grimmory, -hermes-ai) — суммарно около 350 строк копипасты. +(gitea, vaultwarden, mihomo, adguard, docker-test, grimmory, hermes-ai) — +суммарно около 350 строк копипасты. ## Что делает diff --git a/docs/ai/README.md b/docs/ai/README.md new file mode 100644 index 0000000..fd17475 --- /dev/null +++ b/docs/ai/README.md @@ -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. diff --git a/docs/ai/architecture.md b/docs/ai/architecture.md new file mode 100644 index 0000000..3b4a7b2 --- /dev/null +++ b/docs/ai/architecture.md @@ -0,0 +1,142 @@ +# Архитектура + +## Контекст и точки входа + +Репозиторий является 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. + +Остальное (`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-` загружает ignored `.env` из корня репозитория. +2. `playbooks/pve-.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 заморожен. + +## 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 + требуют проверки оператором. diff --git a/docs/ai/edge-cases.md b/docs/ai/edge-cases.md new file mode 100644 index 0000000..511cc7a --- /dev/null +++ b/docs/ai/edge-cases.md @@ -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 намеренным решением. diff --git a/docs/ai/legacy-warning.md b/docs/ai/legacy-warning.md new file mode 100644 index 0000000..68a947b --- /dev/null +++ b/docs/ai/legacy-warning.md @@ -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`. diff --git a/docs/ai/links.md b/docs/ai/links.md new file mode 100644 index 0000000..83a3872 --- /dev/null +++ b/docs/ai/links.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 | diff --git a/docs/ai/migration-tofu.md b/docs/ai/migration-tofu.md new file mode 100644 index 0000000..bbae14f --- /dev/null +++ b/docs/ai/migration-tofu.md @@ -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/.conf` через `lineinfile` (девять контейнеров) + заменяется декларацией в Tofu, но КАКОЙ именно — зависит от устройства: + `/dev/fuse` — штатным флагом `features { fuse = true }`, `/dev/net/tun` — + блоком `device_passthrough`. Оба варианта видны в выводе пилота выше: + `features: fuse=1,...` и `dev0: ...path=/dev/net/tun`; +- шаг `pct set --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 sudo pct config OLD` — сохранить вывод. Это + источник правды для того, что нужно воспроизвести. +3. Запустить свежий бэкап: `ssh 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/'`. Подробности — + [`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 = "", 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 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`: `-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-.yml \ + -e pve_config_target=-new --limit -new + + **Одного `--limit -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('') }}" + + Он есть в `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/.conf` на боевом VMID. + + **Перед каждым прогоном сверяться с `--list-hosts`.** Ожидание: у play + создания и стражей `hosts (0)`, у конфигурационного — ровно `hosts (1): + -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 `), сам + контейнер пока не трогать. +12. Финальная синхронизация. Способ зависит от сервиса — см. раздел 5. +13. Запустить сервис на новом, проверить на `TMPIP`. + +### 4.5 Переключение + +14. `ssh sudo pct stop OLD`. +15. В Tofu поменять адрес нового контейнера с `TMPIP` на `IP`, `make tofu-apply CONFIRM=1`. +16. Перезапустить контейнер, если адрес не подхватился на живую. + - **Если конфигурация сервиса содержит его собственный адрес — прогнать + плейбук заново сразу после смены адреса.** Из пакета 2026-09-02 это + касается только `grimmory` (`ports: :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 ` и `ssh-keygen -R `. В `ssh_config` стоит + `StrictHostKeyChecking accept-new`, который принимает НОВЫЕ ключи, но не + изменившиеся, поэтому без чистки подключение будет отвергнуто. + - **Заодно закрыть ControlMaster: `ssh -F ansible/ssh_config -O exit + `.** Мастер переживает cutover живым, но его туннель ведёт в уже + остановленный контейнер; новые сессии мультиплексируются поверх мёртвого + соединения и виснут с `Connection timed out during banner exchange`. + Симптом выглядит как сетевая проблема, хотя контейнер полностью доступен + с ноды. Разовый обход для диагностики — `-o ControlPath=none`. + Найдено 2026-09-02 на docker-test. + - У gitea SSH host-ключи git-over-ssh лежат в переносимых данных + (`/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 'command -v restic rclone; systemctl list-timers --all | grep restic' + +Чинить так: + + make offsite-restic EXTRA="--limit " + +Затем обязательно проверить всю цепочку, а не только наличие таймера: + + ssh 'systemctl start homelab-restic-offsite-.service' + ssh 'journalctl -u homelab-restic-offsite-.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. Убрать временные записи `-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-.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 -new` на существующем плейбуке) для этого сервиса не + работает: `hosts:` там — жёсткие имена, не группа, и полный прогон против + `mini-pc`/`ru-vps` до cutover преждевременно переключил бы доверие. Кроме + того, `emergency_bot.py.j2` использует Telegram `getUpdates` (не webhook) — + запуск второй копии с тем же `EMERGENCY_BOT_TOKEN`, что и боевая, ловит + 409 conflict. Решение для шага 4.3 при следующем сервисе с похожей + архитектурой: деплоить только play(-и), таргетящие сам новый контейнер, + через одноразовый playbook с `hosts: -new` и заведомо нерабочим, + но валидным по формату токеном/credential — так проверяется факт деплоя + (юнит стартует, зависимости стоят, egress работает) без конфликта с боевым + инстансом. Полный прогон с реальными credentials — уже после cutover, + когда исходный playbook с жёстким `hosts: ` сам начинает резолвиться + в новый контейнер (адрес не менялся, значит `--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: -new` роль + `openvpn_gateway`), туннель встаёт с TMPIP-адреса → `make tofu-apply` снова + работает → сменить адрес на боевой → повторный `openvpn-vps-mini.yml` + (идемпотентен, `changed=0` на обоих концах). Прямой доступ к нодам во время + провала — `ssh -o ProxyJump=none @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 (неделя ожидания) не +сокращается. diff --git a/docs/ai/plan.md b/docs/ai/plan.md new file mode 100644 index 0000000..f78e5c8 --- /dev/null +++ b/docs/ai/plan.md @@ -0,0 +1,485 @@ +# План работ + +## Активные задачи + +### Вывод 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[''].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/.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/.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 -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 -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 ` или разовым `-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 @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__legacy_provisioning_enabled=true`) — как у `pve-gyro.yml`. + Их handlers иначе сделали бы `pct start ` на боевом адресе. +- **Ошибка сессии:** диагностический `pct start 132` при живом CT 160 дал + конфликт IP .23 и провал туннеля на 1-2 мин (восстановилось само). OLD + контейнеры трогать нельзя, их `pct config` держит боевой адрес. +- Косметика: `polkit.service` на CT 160 в `failed` (`status=217/USER`, + минимальный LXC-шаблон). На OpenVPN не влияет. + +**НЕ сделано (осознанно):** уборка раздела 6 `migration-tofu.md` — удаление +`roles/pve_lxc` (ещё используется creation-play пяти сервисов батча 2026-09-02), +стрип creation plays, правки `architecture.md`/`legacy-warning.md`, расширение +`validate.yml`. Это отдельная задача; переездом сервисов 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. diff --git a/docs/ai/tech-stack.md b/docs/ai/tech-stack.md new file mode 100644 index 0000000..c081b09 --- /dev/null +++ b/docs/ai/tech-stack.md @@ -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 +не включены.