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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uoq5AVK8mkBgg83Mq6o5V
This commit is contained in:
Dmitry
2026-09-03 07:06:10 +03:00
co-authored by Claude Sonnet 5
parent 5e27ba2513
commit d2e1e6876a
13 changed files with 1955 additions and 377 deletions
+485
View File
@@ -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['<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 не влияет.
**НЕ сделано (осознанно):** уборка раздела 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.