Files
infra/docs/ai/plan.md
T
DmitryandClaude Sonnet 5 4b54e44116
lint / yamllint + ansible-lint + syntax-check (push) Canceled after 0s
feat: road-warrior OpenVPN profile for the phone (Android, static key)
playbooks/openvpn-phone.yml (make openvpn-phone) stands up a separate
point-to-point static-key instance on ru-vps: tcp/9444, tun2, 10.80.0.0/30,
homelab-openvpn-phone unit, NAT 10.80.0.0/30 -> LAN via tun0. The client
profile (with the secret) lands in ansible/generated/ada-phone.ovpn
(gitignored). Android client: "OpenVPN for Android" (Arne Schwabe) — the
official OpenVPN Connect does not support static-key configs.

- homelab_vpn_client_routes in group_vars/all/main.yml: shared surgical
  route list for both road-warrior profiles; not the whole /24, since the
  phone's home network is almost certainly 192.168.1.0/24 too
- openvpn-laptop.yml reuses that list instead of its own literal copy
- both playbooks: local profile write moved from `become: false` to
  `vars: {ansible_connection: local, ansible_become: false}` — the keyword
  did not suppress the inherited ansible_become on delegate_to: localhost

Deployed and verified on ru-vps 2026-09-03: service active, tun2 up, ufw
9444/tcp, NAT rule present, make openvpn-phone idempotent (changed=0 on
rerun), 192.168.1.30:8082 reachable from ru-vps. openvpn-laptop.yml is
still not applied on the live host.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbuZrUoevfBgCpf5DCF4DG
2026-09-03 09:33:18 +03:00

568 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План работ
## Активные задачи
### Road-warrior OpenVPN для телефона
Выполнено 2026-09-03. `playbooks/openvpn-phone.yml` (`make openvpn-phone`)
поднимает на ru-vps отдельный point-to-point static-key инстанс:
порт 9444/tcp, `tun2`, сеть 10.80.0.0/30, юнит `homelab-openvpn-phone`,
NAT `10.80.0.0/30 -> LAN` через `tun0`. Профиль (с секретом) —
`ansible/generated/ada-phone.ovpn` (gitignored). Клиент — «OpenVPN for
Android» (Arne Schwabe); официальный OpenVPN Connect static key не умеет.
Прогон идемпотентен (третий заход `changed=0`). Проверено: сервис active,
`tun2` up, UFW 9444/tcp, NAT-правило на месте, с ru-vps `192.168.1.30:8082`
достижим. Маршруты клиента вынесены в `homelab_vpn_client_routes`
(`group_vars/all/main.yml`) — общий список с `openvpn-laptop.yml`, не весь
`/24` (домашняя сеть телефона тоже 192.168.1.0/24).
Побочно: в `openvpn-laptop.yml` и `openvpn-phone.yml` локальная запись
профиля переведена с `become: false` на `vars: {ansible_connection: local,
ansible_become: false}` — ключ `become: false` на `delegate_to: localhost`
не подавлял унаследованный `ansible_become: true` (sudo password required).
`openvpn-laptop.yml` на живом ru-vps по-прежнему не применялся.
### Дашборд-обзор инфраструктуры (Homepage)
Решение от 2026-09-03: поднять стартовую страницу-обзор всей инфры (список
сервисов + переходы + виджеты Proxmox/Uptime Kuma).
Выбор: **Homepage** (gethomepage.dev) вторым compose-стеком на LXC
`monitoring` (CT 155) рядом с Uptime Kuma; доступ только LAN/OpenVPN, наружу
не публикуется. Конфиг Homepage **генерируется из `homelab_services`**
`playbooks/dashboard.yml` + шаблоны `playbooks/templates/homepage-*.j2`,
новый потребитель реестра. Плитка на сервис: `href` = `https://<proxy.domain>`
для публичных, иначе `http(s)://<ip>:<порт>`; беспортовые — в группу Headless
с ICMP ping.
**Развёрнуто на CT 155 2026-09-03.** Контейнер `homelab-homepage` (`Up healthy`),
`homelab-homepage.service` enabled, `http://192.168.1.30:8082/` -> 200. Прогон
`make dashboard` идемпотентен (третий заход `changed=0`), `make validate` и
`make lint` зелёные. Образ закреплён по digest
`sha256:753eeb0c…1e1bdd`. Порт 8082 (3000 в модели реестра занят замороженной
Grafana, 3001 — Kuma) -> контейнерный 3000.
Состав:
- `playbooks/dashboard.yml`, шаблоны `homepage-{settings,services,widgets,bookmarks}.yaml.j2`,
`homepage-compose.yml.j2`, `homepage.env.j2`;
- `playbooks/bootstrap-dashboard-pve-token.yml` — read-only токен
`homepage@pve!dashboard` (роль `PVEAuditor`), пишет `DASHBOARD_PVE_*` в корневой `.env`;
- реестр: блок `homelab_dashboard_*` в `group_vars/all/services.yml`, запись в
списках потребителей (`services.yml`, `AGENTS.md`, `architecture.md`);
- `Makefile`: цели `dashboard`, `dry-dashboard`, `bootstrap-dashboard-token`.
Оставшиеся шаги оператора:
1. `make bootstrap-dashboard-token` — выпустить PVE-токен для виджета Proxmox.
(В сессии-агенте был заблокирован auto-mode-классификатором как создание
credential; репозиторная часть готова, плейбук идемпотентен и сам допишет
`DASHBOARD_PVE_*` в корневой `.env`.) После — `make dashboard` заново, чтобы
`homepage.env` подхватил секрет. До этого плитка «Proxmox кластер» показывает
ошибку авторизации — не блокер.
2. Добавить те же три ключа-заглушки в `.env.example` (в сессии путь `.env*`
заблокирован в permissions).
3. В UI Uptime Kuma опубликовать Status Page со slug `homelab` — иначе виджет
сводки up/down показывает ошибку. Не блокер.
### Вывод memoir-bot (CT 142)
Решение от 2026-09-02: сервис не используется и выводится из эксплуатации.
Repository-часть выполнена: удалены playbook, host, запись реестра, VMID из backup
job и backup audit, SSH host-блок, проверки status, Prometheus target и no_proxy
Uptime Kuma. Оговорка "Memoir Bot строится локально" убрана из документации -
теперь единственные исключения из digest pinning это frozen Prometheus и docker-test.
Live-шаги оператора выполнены 2026-09-02: deploy key `SecondBrain` отозван,
monitor в Uptime Kuma снят, `pct stop 142` выполнен (подтверждено `pct list` на
mini-pc). Это заодно репетиция удаления старого контейнера перед первым
blue-green переездом (`migration-tofu.md`).
Оставшиеся шаги:
1. Выдержать паузу (по аналогии с общим инвариантом blue-green - минимум
неделю), затем `pct destroy 142`.
2. Решить судьбу цепочки бэкапов VMID 142 в PBS.
3. Не переиспользовать VMID 142 и `192.168.1.26` сразу.
### Регрессия переезда: update-плейбуки зашивают старые VMID
Обнаружено 2026-09-02 сразу после cutover.
`playbooks/vaultwarden-update.yml` и `playbooks/grimmory-update.yml` теперь берут
backup VMID из registry, а не из жёстко прошитых чисел. Это закрывает старую
регрессию со «страховочным бэкапом» на остановленном контейнере.
Аудит также нашёл и исправил две проблемы в Gitea: небезопасный импорт legacy
provisioning без явного выключателя и запуск `homelab-restic-offsite-gitea` на
`cloud-pc` вместо `gitea` (профиль теперь живёт в CT 153). Теперь consumer status
следует реальному backup unit на `gitea`, а `homelab-backup-audit-gitea` остаётся
на `cloud-pc`.
Обновлено 2026-09-03: `adguard-update.yml` и `mihomo-update.yml` изначально
читают VMID из реестра (`homelab_services['<svc>'].vmid`), поэтому после переезда
adguard→158 и mihomo→159 они автоматически указывают на новые контейнеры —
правок не потребовалось.
`make update-vaultwarden`, `make update-grimmory`, `make update-gitea`,
`make update-adguard`, `make update-mihomo` и `make update-all` не запускались
во время проверки.
### Task 3 — PBS storage-level prune removed declaratively
Выполнено 2026-09-02: storage-level `prune-backups` на PVE storage `pbs` удалён
декларативно, retention authority остался в PBS `prune-pbs`.
Локальное недельное PBS-container backup на storage `backup` с
`keep-last=2` — намеренное исключение и не трогалось.
### Кворум кластера: qdevice не голосует
Обнаружено 2026-09-02. `pvecm status`: `Expected votes: 3`, `Total votes: 2`,
флаги узлов `A,NV,NMW` (NV = Not-Voted), `corosync-qdevice: Connect timeout`.
Причина: `corosync.conf` указывает арбитр по публичному адресу ru-vps
(`host: 157.22.231.198`), а UFW пускал 5403/tcp только из `10.122.62.0/24`
сети ZeroTier, выведенной в июле 2026. Арбитр отвалился молча.
Последствие: у двухнодового кластера нет третьего голоса. Кворум держится лишь
пока живы обе ноды; отказ любой из них оставляет выжившую с `1 < 2`.
Решение: чинить ПРЯМОЙ путь нода -> ru-vps:5403, а не заворачивать арбитр в
OpenVPN. Туннель терминируется в `ovpn-mini` (CT 132 на mini-pc), поэтому при
падении mini-pc арбитр исчез бы вместе с ним — защищён был бы только отказ
cloud-pc. Плюс `ovpn-mini` стоит в плане на blue-green переезд, и его
пересоздание роняло бы кворум.
Сделано: `homelab_pve_egress_ip` в `group_vars/all/main.yml`, правила UFW в
`playbooks/ru-vps-base.yml` (открыть 5403 с этого адреса, удалить правило для
`10.122.62.0/24`), проверка кворума добавлена в `playbooks/status.yml` — секция
CLUSTER QUORUM, чтобы повторный отказ не был снова молчаливым.
ВЫПОЛНЕНО 2026-09-02. После прогона `make ru-vps-base`: `Total votes: 3`,
флаги узлов сменились с `A,NV,NMW` на `A,V,NMW`, qdevice отдаёт голос.
Кластер снова имеет арбитра.
Открытым остаётся динамический `homelab_pve_egress_ip`: адрес зафиксирован
статически, при его смене qdevice снова замолчит. Отличие от прошлого раза в
том, что теперь это видно в секции CLUSTER QUORUM отчёта `make status`.
### Вывод ZeroTier с ru-vps
Решение от 2026-09-02: выводить полностью. Проверено — ZT-интерфейса на хосте
нет, маршрутов через него нет, на ZT-адресах никто не слушает; контейнер
`zerotier` подключён в никуда. Мёртв и `ssh-zt22.service` (sshd на порту 22 для
setup qdevice) — порт 22 не слушает никто.
ВЫПОЛНЕНО 2026-09-02 через `playbooks/ru-vps-zerotier-decommission.yml`
(`make zerotier-decommission CONFIRM=1`). Контейнер снят, `ssh-zt22.service`
отключён, правила UFW для интерфейса `zt6q3dmi2d`, `9993/udp`, `9001` и подсети
`10.122.62.0/24` удалены. Сброшено состояние failed у `ssh-zt22.service` и
фантомного `homelab-pve-routes.service`. Прогон идемпотентен, публичные сервисы
и кворум не пострадали.
Намеренно НЕ удалено: каталог `/opt/services/ru-vps/zerotier`, данные
`/opt/data/zerotier`, файл `/etc/ssh/sshd_config_zt22`. `docker compose down`
выполняется без `-v`, identity узла сохранена. Удаление — отдельный шаг.
Правила UFW для `3128/tcp` (squid не запущен), `1080/tcp` (danted слушает 1081),
`993/tcp` и `7892/tcp` оставлены: они не наследие ZeroTier. В плейбуке есть
переключатель `zt_cleanup_unrelated_stale_rules`, по умолчанию выключен.
### resticprofile-check@profile-default падает еженедельно
Обнаружено 2026-09-02 при чистке упавших юнитов на ru-vps.
Fatal: Please specify repository location (-r or --repository-file)
check on profile 'default': exit status 1
Профиль `default` в `/opt/services/ru-vps/resticprofile/profiles.toml` не имеет
репозитория — это профиль-заготовка, для которого не должно быть таймера
проверки. Реальный `profile-services` бэкапится и проверяется штатно, так что
это шум, а не потеря бэкапов.
Значение: юнит постоянно висит в секции FAILED SYSTEMD UNITS отчёта
`make status` и притупляет внимание к настоящим отказам. Стек resticprofile на
ru-vps в Ansible не описан, поэтому чинится либо вручную
(`systemctl disable --now resticprofile-check@profile-default.timer`), либо
вместе со взятием стека под управление.
Прочие упавшие юниты на ru-vps — `ifup@eth0.service` и `networking.service`;
не разбирались, хост при этом полностью работоспособен.
### Gitea Actions: документация устарела
Проверено 2026-09-02. Контейнер `gitea-runner-gitea-runner-1` на ru-vps работает
и опрашивает Gitea, то есть раннер ЗАРЕГИСТРИРОВАН. Утверждения в
`.gitea/workflows/lint.yml` и `tech-stack.md` об обратном неверны.
CI при этом всё равно не выполняется: у раннера метка `ru-vps`, а workflow
требует `runs-on: ubuntu-latest`. Отдельный вопрос перед включением CI: раннер
монтирует `/var/run/docker.sock` и `/opt/services` на запись, то есть любой
workflow получает root над публичной VPS.
### hermes-ai недостижим по SSH: прозрачный прокси съедает обратный путь
Обнаружено 2026-09-02. CT 147 запущен, sshd слушает `*:22`, UFW разрешает 22/tcp
из `192.168.1.0/24` и `10.78.0.0/30`, но хост не отвечает ни с ru-vps, ни с
cloud-pc (то есть и из самой LAN). `make check` показывает его unreachable.
Причина видна в `ip rule` внутри контейнера:
9002: not from all iif lo lookup 2022
Правило заворачивает в таблицу прозрачного прокси (`hermes-tun`) всё, что не
пришло с `lo`, включая ответные пакеты входящих соединений. SYN доходит, SYN-ACK
уходит в туннель — соединение не устанавливается. Исключения для трафика,
пришедшего с eth0, в конфигурации нет.
Побочное следствие: `playbooks/pve-hermes-ai.yml` больше не может отработать —
Ansible не достучится до хоста. Управлять контейнером можно только через
`pct exec` с cloud-pc. Плейбук, судя по всему, отработал один раз и запер себя:
включение прокси не рвёт уже установленную сессию, только новые.
Не чиню: сервис признан малополезным и заморожен (см. ниже). Но `make check` и
`make status` будут показывать его DOWN, и это ожидаемо, а не новая поломка.
### Hermes AI (CT 147) - осознанно заморожен
Решение от 2026-09-02: сервис признан малополезным, но контейнер остается.
`playbooks/pve-hermes-ai.yml` разворачивает runtime, Docker и transparent TUN proxy
через mihomo, но не разворачивает приложение Hermes - это не недоделка, а принятое
состояние. Не предлагать "дописать деплой Hermes" как opportunistic cleanup.
### Секреты переехали в корень репозитория
Решение от 2026-09-02. `.env` и `.env.example` перенесены из `ansible/` в корень:
их потребляет не только Ansible, но и OpenTofu, а держать два файла или ходить в
соседний подкаталог неудобно.
`ENV_FILE` в `ansible/Makefile` теперь абсолютный (`$(REPO_ROOT)/.env`), поэтому
цели работают из любого cwd. Переименование `.env.example` заведено в индекс git,
чтобы файл не потерялся при коммите. Оба пути покрыты `.gitignore`.
Принято решение хранить пароль `root@pam` в `.env` (а не спрашивать его при
запуске). Компромисс осознанный: пользователь `ansible` и так имеет passwordless
sudo на нодах, то есть эффективный root в автоматизации уже был; пароль добавляет
не новый класс доступа, а секрет с худшими свойствами — он же логин в веб-интерфейс
и консоль, его нельзя ограничить по scope и нельзя отозвать иначе, чем сменив
пароль root на нодах.
Переменные: `PROXMOX_ROOT_USER` (по умолчанию `root@pam`) и
`PROXMOX_ROOT_PASSWORD`. Их читают ТОЛЬКО цели `tofu-*`; Ansible ими не
пользуется. Если пароль не задан или равен `replace-me`, Tofu идёт токеном
`ansible@pve`. Выбранный режим печатается в stderr перед запуском.
### Пилот OpenTofu: граница возможностей API-токена
Выполнено 2026-09-02. Каталог `tofu/`, цели `make tofu-*`, подробности и матрица
возможностей — в [`../../tofu/README.md`](../../tofu/README.md).
Кратко, проверено на живом кластере контейнером VMID 199:
- API-токен создаёт LXC со всеми ресурсами, сетью, rootfs, `nesting`, startup и
тегами; `plan` идемпотентен.
- **Mount point как volume на datastore токеном создаётся.** Это снимает вопрос
по `mp0` у gitea: bind mount каталога хоста требует root@pam, а volume — нет.
- `keyctl`, `fuse`, `mount` и `device_passthrough` требуют root@pam (HTTP 403).
Это ограничение Proxmox, а не Tofu: Ansible упирается в то же самое, поэтому в
репозитории уже есть `pct set --features` по SSH и правка
`/etc/pve/lxc/<vmid>.conf`.
- Гибрид Tofu+Ansible требует `lifecycle { ignore_changes = [features] }`, иначе
Tofu откатывает выставленный извне `keyctl` и ломает Docker в контейнере.
- Tofu не умеет ProxyJump: вне LAN нужен SSH-туннель, он встроен в цели `tofu-*`.
РЕЖИМ ВЫБРАН 2026-09-02: root@pam по паролю. Проверено на пилоте — `dev0`,
`features: fuse=1,keyctl=1,nesting=1` и volume mount point выставляются
декларативно, повторный `plan` даёт `No changes`. Значит при переезде каждого
сервиса из его `pve-*.yml` можно убирать `pct set --features` и правку
`/etc/pve/lxc/<vmid>.conf`; `ignore_changes` не нужен.
Токен, принадлежащий root@pam, ограничение НЕ обходит: `$authuser` при токенной
аутентификации равен полному `user@realm!tokenname` (`PVE/HTTPServer.pm:86`), а
проверка в `PVE/LXC.pm:1658` сравнивает строку с `root@pam` буквально. Права
токену добавлять бесполезно. Единственная альтернатива гибриду — пароль root@pam,
то есть системный пароль root узлов Proxmox.
Гибридная схема (Tofu создаёт, Ansible доводит по SSH) отвергнута в пользу
полной декларативности. Она остаётся запасным вариантом, если пароль root
решат из `.env` убрать.
Пилотный контейнер VMID 199 `tofu-pilot` снесён (подтверждено 2026-09-02:
`pct list` на cloud-pc его не показывает).
## Переход на OpenTofu
Пошаговый план миграции provisioning на Tofu вынесен в отдельный документ:
[`migration-tofu.md`](migration-tofu.md). Там же порядок сервисов, специфика
каждого и процедура отката.
Сервис №1 `emergency-bot` переехал 2026-09-02: OLD (VMID 148) остановлен
(откат минимум неделю, затем `pct destroy`), боевой — NEW (VMID 151,
`tofu/services.tf`), адрес не менялся (192.168.1.32). Реестр обновлён
(`vmid: 151`, `provisioner: tofu`), `make validate` и `make status` зелёные.
Две находки, важные для следующих сервисов, задокументированы в
`migration-tofu.md` (раздел 5.1) и `tofu/README.md`: конфигурационная часть
сервиса может жить в отдельном playbook с play, таргетящими другие хосты (не
подходит под буквальный `--limit <name>-new`), и провайдер отслеживает
`cmode` через блок `console`, который нужно объявлять явно с самого начала.
### Пакетный переезд сервисов 2-7 (параллельно)
Решение от 2026-09-02: последовательный переезд по одному сервису слишком
медленный. Инвариант «один сервис за раз» уточнён в `migration-tofu.md`:
последовательным обязан быть только cutover, а подготовка, создание и
конфигурация на временных адресах параллелятся безопасно.
**Создано и проверено 2026-09-02.** Один пакетный `tofu apply` (`6 to add,
0 to change, 0 to destroy` — уже переехавший emergency-bot дрейфа не дал):
| Сервис | OLD | NEW | Узел | TMPIP |
|---|---|---|---|---|
| docker-test | 145 | 152 | cloud-pc | 192.168.1.11 |
| gitea | 141 | 153 | cloud-pc | 192.168.1.12 |
| vaultwarden | 140 | 154 | mini-pc | 192.168.1.13 |
| monitoring | 146 | 155 | cloud-pc | 192.168.1.14 |
| gyro | 150 | 156 | mini-pc | 192.168.1.15 |
| grimmory | 149 | 157 | cloud-pc | 192.168.1.16 |
`pct config` всех шести сверен с эталонами: cpu, память, swap, диск, features,
startup, `cmode: shell` совпали. Запас на нодах после создания: cloud-pc
12.3 ГиБ RAM и 849 ГиБ на `data`, mini-pc 8.8 ГиБ RAM и 117 ГиБ на `local-lvm`.
`make validate` зелёный до и после — новые контейнеры в реестре пока не
числятся, это ожидаемо до шага 4.6.
Что подтвердилось на живых контейнерах впервые:
- **`/dev/fuse` через `features.fuse` эквивалентен старому обходу.** Устройство
присутствует в 152, 153, 154, 157 и отсутствует в 156 (у gyro его и не
должно быть). Это снимает вопрос, который до сих пор был помечен как
непроверенный: пилот проверял `device_passthrough` только на `/dev/net/tun`.
- **Bind mount gitea заменён на volume декларативно:**
`mp0: data:153/vm-153-disk-1.raw,mp=/opt/gitea/data,size=32G`. Ради этого
миграция и затевалась.
- **Пустой `features` у gyro сохраняется.** В `pct config 156` строки
`features` нет вообще, `nesting` не подкрался.
Две ловушки, найденные при этом заходе и почищенные в коде:
1. **`--limit <name>-new` не перенацеливает play, а обнуляет его** — и это
общее свойство, а не особенность emergency-bot, как считалось раньше.
Проверено `--list-hosts`: ноль хостов во ВСЕХ play всех плейбуков.
Заведён переопределяемый таргет `pve_config_target`, поведение по
умолчанию не изменилось. Подробности — `migration-tofu.md`, шаг 4.3.9.
2. **Inline `vars:` у группы в inventory-файле проигрывают `group_vars/all/`.**
Из-за этого Ansible ходил в новые контейнеры под `ansible@` вместо `root`
и получал `Permission denied`. Транспорт вынесен в
`group_vars/lxc_migration_new/main.yml`.
Попутно `roles/uptime_kuma` научился не падать на хосте, где замороженного
юнита `homelab-monitoring` никогда не было: заморозка теперь выполняется
только при его наличии.
Ещё один потребитель, которого не было в плане: `playbooks/vaultwarden-update.yml`
жёстко зашивает VMID 140, включая `vzdump "140"`. Его нужно поправить на шаге
4.6 вместе с реестром.
**Конфигурация на временных адресах (шаг 4.3) выполнена для пяти сервисов.**
Все прогоны идемпотентны — второй заход даёт `changed=0`. Боевые контейнеры не
затронуты: проверено после прогонов, на каждом боевом хосте активен ровно свой
сервис.
| Сервис | Итог | Проверка |
|---|---|---|
| docker-test 152 | OK | Docker active, storage-driver `fuse-overlayfs`, `hello-world` проходит, версия Docker совпала с боевой |
| gitea 153 | OK | HTTP 200, порт 2222 слушает, volume — отдельная ФС `/dev/loop10` ext4 32 ГиБ |
| vaultwarden 154 | OK | HTTP 200, контейнер healthy, база свежая 278 КБ без `icon_cache` |
| monitoring 155 | OK | HTTP 302→200, `kuma.db` создан на месте, `/opt/monitoring` отсутствует |
| grimmory 157 | OK после починки | оба юнита active, health 200, Flyway 144 — как на боевом |
| gyro 156 | OK | конфигурация и cutover завершены 2026-09-02 |
Digest'ы образов на всех пяти совпали с боевыми побайтово.
**Баг в `pve-grimmory.yml`, найденный при этом.** Боевой адрес был зашит в
конфигурационном play трижды: в `ports` compose, в правилах `GRIMMORY-FILTER`
и в URL health-check. Первое роняло `grimmory.service` на любом другом адресе.
Третье опаснее: задача выполняется на целевом хосте, поэтому после починки
биндинга она уходила бы в БОЕВОЙ grimmory, получала 200 и давала
ложно-положительный результат. Исправлено через
`grimmory_bind_ip: "{{ expected_lan_ip }}"`; на боевом хосте рендер не
изменился, `--check --diff` даёт `changed=0`.
**Следствие для cutover.** Grimmory — единственный из шести, кто биндится на
конкретный адрес. После смены адреса его плейбук обязан быть прогнан заново,
иначе compose останется с временным адресом. Добавлено в шаг 4.5.16.
**Ещё одно ошибочное утверждение снято.** План предупреждал, что `roles/gyro`
пинит SSH host key gitea и после переезда сломается. Проверено дословно: там
`state: absent` — задача УДАЛЯЕТ устаревшую запись, а пинится `github.com`,
и `gyro_repo_url` ведёт на GitHub. Переезд gitea на gyro не влияет.
Legacy `pve-gyro.yml` теперь по умолчанию завершает все три plays, когда registry
говорит `provisioner: tofu`; для intentional legacy rollback/recovery нужен
явный override `-e pve_gyro_legacy_provisioning_enabled=true`. Это не normal
deployment.
**Cutover выполнен 2026-09-02 для пяти сервисов.** Все прошли по процедуре
раздела 4: свежий бэкап, перенос данных со сверкой, остановка старого
контейнера, смена адреса, правка реестра, перегенерация заданий бэкапа.
| Сервис | OLD → NEW | Сверка данных |
|---|---|---|
| docker-test | 145 → 152 | данных нет; Docker на fuse-overlayfs, hello-world проходит |
| vaultwarden | 140 → 154 | 1 users, 314 ciphers; integrity_check ok |
| monitoring | 146 → 155 | kuma.db совпал по sha256 и размеру; редирект на /dashboard |
| gitea | 141 → 153 | 64 repos, 5 users, 203 actions; integrity_check ok |
| grimmory | 149 → 157 | book=31 author=30 book_file=40 category=87 reading_sessions=68; Flyway 142/144 без новых миграций |
Публичные домены снаружи: `pass.ada-dev.ru` 200, `git.ada-dev.ru` 200,
`books.ada-dev.ru` health 200 (OPDS отдаёт штатный `401 Basic realm="Booklore
OPDS"` — это требование авторизации, а не поломка; в базе 2 учётки OPDS).
`make validate`, `make lint` зелёные, `make backup-jobs` перегенерировал списки
VMID сам из реестра.
**Смена адреса выполняется провайдером на месте.** Проверено на всех пяти:
`0 added, 1 changed, 0 destroyed`. Это снимало главный риск — пересоздание
контейнера с данными.
Старые контейнеры остановлены и целы: 140, 141, 145, 146, 149. Не удалять
минимум неделю, до 2026-09-09.
### Находки cutover, которых не было в плане
1. **Данные gitea никогда не попадали в PBS.** `vzdump` печатает
`excluding bind mount point mp0 ('/opt/gitea/data') from backup (not a
volume)` — снапшоты `ct/141` содержали только rootfs. Защищал данные лишь
offsite-профиль restic. Переезд на volume это чинит: теперь данные в бэкапе.
Побочно это самый весомый аргумент за миграцию, которого в плане не было.
2. **`vzdump` падает, пока работает Docker с fuse-overlayfs на rootfs в
каталоговом хранилище.** rootfs на `data` снапшоты не поддерживает, vzdump
уходит в режим `suspend` с rsync и спотыкается о
`var/lib/docker/fuse-overlayfs/.../merged`: Permission denied. У vaultwarden
этого нет — его rootfs на `local-lvm`, там настоящий снапшот. Лечится
остановкой сервиса перед бэкапом.
3. **Offsite-профиль restic надо переустанавливать на новом контейнере**, если
он выполняется внутри LXC. Сделано для vaultwarden и grimmory, проверено
прогоном юнита. Профиль gitea переписан с ноды внутрь контейнера
(`playbooks/offsite-restic-yadisk.yml`), старый таймер на cloud-pc отключён.
4. **`lost+found` на новом volume ломает restic.** Свежая ext4 создаёт этот
каталог с владельцем uid 0 хоста, внутри unprivileged LXC он
`nobody:nogroup` и нечитаем; restic отдаёт exit 3, юнит падает каждую ночь,
хотя снапшот сохраняется. Добавлен в исключения профиля gitea.
5. **SSH host-ключи gitea перенеслись вместе с данными.** Ключ на порту 2222
совпал с боевым (`AAAAC3NzaC1lZDI1NTE5AAAAIDE+iqCj`), поэтому у git-клиентов
`known_hosts` остался валидным. Обновлять пришлось только ключ sshd
контейнера на порту 22.
6. **Зависший ControlMaster даёт таймаут на баннере после cutover.** Мастер
остаётся живым, но его туннель ведёт в остановленный контейнер, и новые
сессии мультиплексируются поверх мёртвого соединения. Лечится
`ssh -O exit <host>` или разовым `-o ControlPath=none`.
7. **`systemctl start grimmory` — no-op.** Юнит `Type=oneshot` с
`RemainAfterExit=yes` уже числится активным, нужен `restart`.
8. **`MYSQL_ROOT_PASSWORD` из `.env` не подошёл к MariaDB** (`Access denied for
user 'root'@'localhost'`). Restore выполнен пользователем `grimmory`, у него
`ALL PRIVILEGES` на свою базу — этого достаточно для DROP/CREATE DATABASE.
### Gyro cutover verified
`gyro` завершён как cutover on 2026-09-02: CT 156 живёт на неизменном
production IP `192.168.1.35` и provisioned by Tofu. CT 150 остановлен и
удерживается как rollback минимум на неделю; его PBS snapshot от
`2026-09-02T19:40:56Z` сохранён, цепочку PBS не удаляли. Файл фаервола
`156.fw` уже копия `150.fw`, Datacenter firewall по-прежнему disabled. Timer
`gyro.timer` active, last oneshot success. Данных для переноса не было, и
TMPIP-specific bind/re-run не потребовались.
Удаление старого CT или его backup chain не выполнялось.
### Пакетный переезд сервисов 8-10: adguard, mihomo, ovpn-mini
Выполнено 2026-09-03 автономно, одной сессией. Этим OpenTofu-переезд всех
мигрируемых LXC завершён (осталось: `hermes-ai` заморожен, `pbs` не мигрируется).
| Сервис | OLD → NEW | Узел | Боевой IP (не менялся) | Свежий PBS перед стартом |
|---|---|---|---|---|
| adguard | 144 → **158** | mini-pc | 192.168.1.28 | `ct/144/2026-09-02T21:06:12Z` |
| mihomo | 143 → **159** | mini-pc | 192.168.1.27 | `ct/143/2026-09-02T21:12:49Z` |
| ovpn-mini | 132 → **160** | mini-pc | 192.168.1.23 | `ct/132/2026-09-02T21:17:55Z` |
OLD 144/143/132 остановлены и удерживаются как откат минимум неделю
(до ~2026-09-10). PBS-цепочки не трогали; у NEW 158/159/160 сняты первые
свои снапшоты (`…T21:37-39Z`), чтобы `backup-audit` был чист сразу.
`make validate`, `make lint`, `make tofu-plan` (`No changes`), `make openvpn-check`
(7/7) — зелёные. Внешние домены `git/pass/books.ada-dev.ru` — 200.
**Смена адреса — провайдером на месте** (`0 added, 1 changed, 0 destroyed`),
пересоздания контейнера с данными нет — как и на сервисах 2-7.
Специфика и находки перенесены в
[`migration-tofu.md`](migration-tofu.md) раздел 5 (пункты 8-10). Кратко:
- **adguard:** свежий AdGuard на пустом `conf/` не проходит health-гейт
плейбука (setup-wizard, порт 80 не слушает), поэтому данные заливаются ДО
config play, а не после. DNS-провал ~2 мин, ночью; ноды на DNS роутера,
контейнеры на 1.1.1.1.
- **mihomo:** первый боевой `device_passthrough` для `/dev/net/tun`
(`dev0: path=/dev/net/tun,mode=0660`). `ru-vps-mihomo-harden.yml` повторно
прогонять не нужно. Все зависимые ссылки на .27 остаются валидны.
- **ovpn-mini — петля транспорта.** `-F ansible/ssh_config` и `make tofu-*`
ходят к PVE через ru-vps → туннель, который терминирует сам ovpn-mini.
`pct stop 132` рвёт этот путь и `make tofu-apply` не достаёт API. Разрыв:
поднять шлюз на НОВОМ контейнере ещё на TMPIP (одноразовый плейбук), туннель
встаёт → tofu-apply работает → сменить адрес → повторный `openvpn-vps-mini.yml`
(идемпотентен). Прямой путь к нодам во время провала —
`ssh -o ProxyJump=none <user>@192.168.1.{5,10}` из LAN.
Совокупный провал туннеля (внешний reverse-proxy) ~10 мин.
- **`ssh_config`:** у `ovpn-mini` выставлен `ProxyJump none` (было — через
ru-vps). Путь через ru-vps закольцовывался бы на его же туннель.
- Три creation-play (`pve-adguard.yml`, `pve-mihomo.yml`, `pve-ovpn-mini.yml`)
за гейтом `provisioner != 'tofu'` (`meta: end_play`, override
`-e pve_<svc>_legacy_provisioning_enabled=true`) — как у `pve-gyro.yml`.
Их handlers иначе сделали бы `pct start <OLD>` на боевом адресе.
- **Ошибка сессии:** диагностический `pct start 132` при живом CT 160 дал
конфликт IP .23 и провал туннеля на 1-2 мин (восстановилось само). OLD
контейнеры трогать нельзя, их `pct config` держит боевой адрес.
- Косметика: `polkit.service` на CT 160 в `failed` (`status=217/USER`,
минимальный LXC-шаблон). На OpenVPN не влияет, не чинилось (можно
`systemctl mask polkit.service` — ничего от него не зависит).
**`onboot: 0` на всех остановленных OLD-контейнерах (2026-09-03).** Найдено:
132/140/141/142/143/144/145/146/148/149/150 стояли с `onboot: 1` и держат
боевые адреса в `pct config` — ребут любой ноды поднял бы их в конфликт с
живыми NEW (151-160). Снято через `pct set <id> --onboot 0`. Это временная
мера на окно отката: `pct destroy` (шаг 4.7) уберёт их целиком через неделю.
Обратимо (`--onboot 1`) для намеренного продвижения OLD обратно.
**Коммиты (2026-09-03).** Всё рабочее дерево (накопление сессий 2026-09-02/03)
разложено в 11 тематических коммитов на `main` (`22394cb`..`d2e1e68`),
не запушено. Границы — по инициативам: build-env, .env-в-корень+Make,
tofu scaffold, validate gate, registry-driven backups, вывод memoir-bot,
ru-vps base+кворум+ZeroTier, PBS storage prune, blue-green миграция на Tofu,
фикс VMID в update-плейбуках, docs. Несколько файлов (`services.yml`,
`status.yml`, `ssh_config`) несут вторичные правки других инициатив — отмечено
в телах коммитов.
**НЕ сделано (осознанно):** уборка раздела 6 `migration-tofu.md` — удаление
`roles/pve_lxc` (ещё используется creation-play пяти сервисов батча 2026-09-02
через `- role: pve_lxc`), стрип/полное гейчение creation plays, правки
`architecture.md`/`legacy-warning.md`, расширение `validate.yml` (features,
devices, mount points). Это отдельная задача; переездом сервисов 8-10
разблокирована (все 10 на Tofu), но исполнение — не сейчас.
Obsidian-заметки (`HomeLab.md`, `Notes/Текущее состояние…`, `Log.md`) по этому
переезду ещё не обновлены.
## Исторические планы
- [`../../current-task.md`](../../current-task.md) - исторический план Prometheus
monitoring. Он не описывает текущую активную архитектуру: Uptime Kuma активен,
Prometheus stack заморожен.
- [`../../tasks/grimmory-deployment-plan.md`](../../tasks/grimmory-deployment-plan.md)
- план и запись развертывания Grimmory; фактическое состояние проверять по inventory,
service registry и playbooks.
Выявленные риски и deferred work записаны в [`edge-cases.md`](edge-cases.md) и
[`legacy-warning.md`](legacy-warning.md), но не считаются утвержденным backlog.