lint / yamllint + ansible-lint + syntax-check (push) Canceled after 0s
playbooks/dashboard.yml deploys Homepage as a second compose stack on the monitoring LXC (CT 155) next to Uptime Kuma and renders its config from homelab_services: one tile per service, link to its UI, grouped by Proxmox node. Adding a service to the registry is enough — no second service list. - new registry consumer: playbooks/dashboard.yml + playbooks/templates/homepage-*.j2 - homelab_dashboard_* vars in group_vars/all/services.yml (top-level, like homelab_reverse_proxy_*); image pinned by digest, floating tag needs an explicit -e dashboard_allow_floating_tag=true - bootstrap-dashboard-pve-token.yml: read-only homepage@pve!dashboard token (PVEAuditor) for the Proxmox widget, secret in the root .env as DASHBOARD_PVE_* - Makefile: dashboard, dry-dashboard, bootstrap-dashboard-token - container binds the LAN address only (192.168.1.30:8082), not published via Caddy - docs: architecture.md Monitoring section, plan.md active task, consumer lists Deployed to CT 155 on 2026-09-03: container healthy, http://192.168.1.30:8082/ returns 200, `make dashboard` idempotent, `make validate` and `make lint` green. Pending operator steps: `make bootstrap-dashboard-token` (blocked in the agent session as credential creation) and an Uptime Kuma status page with slug homelab. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KbuZrUoevfBgCpf5DCF4DG
547 lines
41 KiB
Markdown
547 lines
41 KiB
Markdown
# План работ
|
||
|
||
## Активные задачи
|
||
|
||
### Дашборд-обзор инфраструктуры (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.
|