Files
DmitryandClaude Sonnet 5 d45391a261
lint / yamllint + ansible-lint + syntax-check (push) Canceled after 0s
Revert: drop the phone road-warrior OpenVPN attempt
Rolled back per the user's request. Three variants were tried on ru-vps
(static key; TLS peer-fingerprint p2p; server mode with push routes and an
inline <ca>). The server side worked each time, but the "OpenVPN for
Android" client consistently failed at config build ("Used 101 tries to
get current version of the profile"), which looks like an app/OS issue
rather than the config.

Repo: remove playbooks/openvpn-phone.yml, its Make target, and the shared
homelab_vpn_client_routes var; restore openvpn-laptop.yml to its prior
state (its pre-existing `become: false` on delegate_to: localhost is noted
in plan.md, left untouched). ru-vps teardown done out of band: unit, tun2,
ufw/nat rules for 9444 and 10.80.0.0/29, and /etc/openvpn/homelab-phone
removed; the site tunnel (homelab-openvpn, tun0) was not touched and is
verified active.

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

43 KiB
Raw Permalink Blame History

План работ

Активные задачи

Road-warrior OpenVPN для телефона — откачено

2026-09-03: пробовали поднять клиентский OpenVPN для телефона (Android), чтобы смотреть дашборд вне LAN. Перебрали три варианта на ru-vps (static key -> TLS peer-fingerprint p2p -> server-режим с push route и <ca>). На стороне ru-vps всё работало (тоннель вставал, TLS OK), но клиент «OpenVPN for Android» стабильно падал на «сборка конфигурации / Used 101 tries to get current version of the profile». По решению пользователя всё откачено: homelab-openvpn-phone, tun2, правила UFW/NAT для 9444 и 10.80.0.0/29, /etc/openvpn/homelab-phone сняты с ru-vps; playbooks/ openvpn-phone.yml, Make-цель и homelab_vpn_client_routes удалены из репозитория. Сайт-тоннель homelab-openvpn (tun0) не затрагивался.

Если возвращаться: скорее всего проблема на стороне приложения (battery optimization / сохранение профиля), а не конфига; либо собрать полноценный PKI через community.crypto и <ca>+remote-cert-tls server, либо попробовать WireGuard.

Предсуществующий баг, замеченный по ходу и НЕ тронутый: в openvpn-laptop.yml (в проде не применялся) задачи с delegate_to: localhost используют ключ become: false, который не подавляет унаследованный ansible_become: true — при запуске упрётся в «sudo: a password is required». Чинится vars: {ansible_connection: local, ansible_become: false}.

Дашборд-обзор инфраструктуры (Homepage)

Решение от 2026-09-03: поднять стартовую страницу-обзор всей инфры (список сервисов + переходы + виджеты Proxmox/Uptime Kuma).

Выбор: Homepage (gethomepage.dev) вторым compose-стеком на LXC monitoring (CT 155) рядом с Uptime Kuma; доступ только LAN/OpenVPN, наружу не публикуется. Конфиг Homepage генерируется из homelab_servicesplaybooks/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.

Кратко, проверено на живом кластере контейнером 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. Там же порядок сервисов, специфика каждого и процедура отката.

Сервис №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 раздел 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 - исторический план Prometheus monitoring. Он не описывает текущую активную архитектуру: Uptime Kuma активен, Prometheus stack заморожен.
  • ../../tasks/grimmory-deployment-plan.md
    • план и запись развертывания Grimmory; фактическое состояние проверять по inventory, service registry и playbooks.

Выявленные риски и deferred work записаны в edge-cases.md и legacy-warning.md, но не считаются утвержденным backlog.