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:
co-authored by
Claude Sonnet 5
parent
5e27ba2513
commit
d2e1e6876a
@@ -0,0 +1,68 @@
|
||||
# AI Context
|
||||
|
||||
## Назначение
|
||||
|
||||
Этот набор документов фиксирует стабильный, подтвержденный репозиторием контекст
|
||||
HomeLab. Он дополняет краткий рабочий контракт в [`AGENTS.md`](../../AGENTS.md) и
|
||||
не заменяет канонические Ansible inventory/vars или операционные заметки Obsidian.
|
||||
|
||||
HomeLab управляется как Ansible-first control plane: Proxmox VE/LXC, сетевой
|
||||
транспорт, сервисы, резервное копирование, обновления и мониторинг описываются в
|
||||
`ansible/`. Прямые изменения на серверах допустимы только для read-only диагностики
|
||||
или break-glass восстановления с последующим переносом желаемого состояния в Ansible.
|
||||
|
||||
## Возможности
|
||||
|
||||
- создание и настройка LXC через Proxmox API или `pct` по SSH;
|
||||
- управление сервисами через Docker/systemd и Docker Compose/systemd;
|
||||
- OpenVPN-транспорт и SSH ProxyJump через `ru-vps`;
|
||||
- Caddy reverse proxy для публичных сервисов;
|
||||
- PBS и offsite restic backups с аудитом свежести;
|
||||
- управляемые обновления по схеме backup/audit -> update -> health check;
|
||||
- активный Uptime Kuma и сохраненный, но замороженный Prometheus stack;
|
||||
- локальный Grimmory MCP с ручной синхронизацией книг в Obsidian.
|
||||
|
||||
## Форма системы
|
||||
|
||||
- `ansible/Makefile` является основной ручной точкой входа.
|
||||
- `ansible/inventory/hosts.yml` задает хосты, группы и индивидуальные адреса.
|
||||
- `ansible/inventory/group_vars/all/services.yml` содержит сводный реестр сервисов,
|
||||
но пока программно управляет только генерацией reverse proxy.
|
||||
- `ansible/playbooks/` содержит операционные entry points, `ansible/roles/` - роли.
|
||||
- `tools/grimmory-mcp/` является отдельным Node.js stdio MCP-процессом.
|
||||
- Obsidian vault хранит решения, текущую эксплуатационную картину и журнал работ.
|
||||
|
||||
## Карта репозитория
|
||||
|
||||
| Путь | Назначение |
|
||||
|---|---|
|
||||
| `ansible/Makefile` | Проверки, deploy, update и защищенные операции |
|
||||
| `ansible/inventory/` | Канонические хосты, группы, общие и host-specific vars |
|
||||
| `ansible/playbooks/` | Операционные Ansible entry points |
|
||||
| `ansible/roles/` | Переиспользуемые роли и сохраненный monitoring stack |
|
||||
| `ansible/ssh_config` | SSH users, keys, ports и ProxyJump |
|
||||
| `.opencode/agents/` | Read-only специализированные агенты HomeLab |
|
||||
| `tools/grimmory-mcp/` | Grimmory API и Obsidian sync integration |
|
||||
| `archive/2026-07-proxmox-migration/` | История до Ansible control plane, не active source |
|
||||
|
||||
## Ключевые ограничения
|
||||
|
||||
- Не хранить secrets в Git или Obsidian.
|
||||
- Не считать `--check --diff` полной симуляцией Proxmox provisioning.
|
||||
- Не запускать замороженный Prometheus stack без отдельного решения.
|
||||
- Не считать generic roles `lxc_docker_host` и `compose_service` подключенными к
|
||||
production: активные service playbooks пока остаются источником поведения.
|
||||
- Не исправлять обнаруженный технический долг в рамках несвязанной задачи.
|
||||
- Не редактировать archive, generated files, installed Galaxy collections или
|
||||
`node_modules` как active implementation.
|
||||
|
||||
## Документы
|
||||
|
||||
- [`architecture.md`](architecture.md) - observed architecture и data/control flows.
|
||||
- [`tech-stack.md`](tech-stack.md) - runtimes, dependencies и команды.
|
||||
- [`edge-cases.md`](edge-cases.md) - failure modes, safety gaps и coverage.
|
||||
- [`plan.md`](plan.md) - только подтвержденная активная работа.
|
||||
- [`migration-tofu.md`](migration-tofu.md) - пошаговый план перехода provisioning
|
||||
LXC на OpenTofu по схеме blue-green.
|
||||
- [`legacy-warning.md`](legacy-warning.md) - границы legacy/frozen/prototype кода.
|
||||
- [`links.md`](links.md) - официальные version-relevant references.
|
||||
@@ -0,0 +1,142 @@
|
||||
# Архитектура
|
||||
|
||||
## Контекст и точки входа
|
||||
|
||||
Репозиторий является control plane, а не приложением с единым runtime. Оператор
|
||||
запускает цели `ansible/Makefile`; Make загружает локальные secrets, применяет safety
|
||||
gates и вызывает playbooks. `ansible.cfg` выбирает inventory, roles и локально
|
||||
установленные Galaxy collections.
|
||||
|
||||
Канонические источники:
|
||||
|
||||
- хосты и группы: `ansible/inventory/hosts.yml`;
|
||||
- общий доступ и сеть: `ansible/inventory/group_vars/all/main.yml`;
|
||||
- сводные сведения о сервисах: `ansible/inventory/group_vars/all/services.yml`;
|
||||
- SSH transport: `ansible/ssh_config`;
|
||||
- ручные операции: `ansible/Makefile`.
|
||||
|
||||
Программные потребители реестра:
|
||||
|
||||
- `playbooks/reverse-proxy.yml` — сборка Caddyfile;
|
||||
- `playbooks/ru-vps-base.yml` — стек Caddy и его закреплённый образ;
|
||||
- `playbooks/pve-backup-jobs.yml` — списки VMID заданий PBS (поле `backup.job`);
|
||||
- `roles/backup_audit` — VMID для аудита (флаг `monitoring.backup_audit_vmid`);
|
||||
- `playbooks/validate.yml` — сверка реестра с фактическим состоянием Proxmox.
|
||||
|
||||
Остальное (`pve-*.yml`, `status.yml`, monitoring, SSH config) по-прежнему
|
||||
дублирует значения и должно меняться согласованно.
|
||||
|
||||
## Топология
|
||||
|
||||
- `ru-vps`: public VPS, qdevice, Caddy, OpenVPN server и SSH JumpHost.
|
||||
- `cloud-pc`, `mini-pc`: Proxmox VE nodes.
|
||||
- `lxc_infra`: PBS, OpenVPN gateway и service LXC.
|
||||
- `monitoring`: CT 146 на `cloud-pc`; Uptime Kuma активен, Prometheus stack заморожен.
|
||||
|
||||
Актуальные VMID, placement, IP, ports, domains и backup policy не копируются сюда:
|
||||
их нужно читать из `homelab_services` и сверять с `hosts.yml`.
|
||||
|
||||
## Provisioning Flow
|
||||
|
||||
1. `make deploy-<service>` загружает ignored `.env` из корня репозитория.
|
||||
2. `playbooks/pve-<service>.yml` создает или сверяет LXC.
|
||||
3. Часть playbooks вызывает `roles/pve_lxc` через Proxmox API.
|
||||
4. Остальные подключаются по SSH к PVE node и выполняют `pct create/set/start`.
|
||||
5. Следующий play настраивает LXC, runtime, systemd и health checks.
|
||||
|
||||
Два provisioner-пути имеют разные check-mode и ownership guards. Нельзя переносить
|
||||
service между ними как косметический рефакторинг.
|
||||
|
||||
## Service Runtime
|
||||
|
||||
- Большинство сервисов используют systemd units вокруг `docker run`.
|
||||
- Grimmory использует Compose и отдельный `DOCKER-USER` firewall unit.
|
||||
- Uptime Kuma и сохраненный monitoring stack используют legacy `docker-compose`.
|
||||
- `lxc_docker_host` и `compose_service` описывают будущий общий паттерн, но ни один
|
||||
production playbook их пока не вызывает.
|
||||
|
||||
## Network Flow
|
||||
|
||||
`ansible_ssh_common_args` подключает `ansible/ssh_config`. Обычный путь управления:
|
||||
|
||||
```text
|
||||
controller -> ru-vps:3422 -> 192.168.1.x target
|
||||
```
|
||||
|
||||
`pbs` и `ovpn-mini` являются direct-LAN исключениями. LXC обычно управляются как
|
||||
`root`, а PVE nodes и `ru-vps` - как service account `ansible`.
|
||||
|
||||
OpenVPN site tunnel:
|
||||
|
||||
```text
|
||||
ru-vps 10.78.0.1:8443/tcp <-> ovpn-mini 10.78.0.2 -> 192.168.1.0/24
|
||||
```
|
||||
|
||||
Публичный service flow:
|
||||
|
||||
```text
|
||||
Internet -> Caddy on ru-vps -> OpenVPN -> ovpn-mini -> LAN service
|
||||
```
|
||||
|
||||
Потеря `ru-vps` или site tunnel одновременно влияет на public upstreams и обычный
|
||||
SSH management path.
|
||||
|
||||
## Reverse Proxy
|
||||
|
||||
`playbooks/reverse-proxy.yml` выбирает записи `homelab_services` с блоком `proxy`,
|
||||
валидирует metadata и Caddyfile, обновляет marked blocks и проверяет upstreams.
|
||||
Ansible управляет Caddyfile, но не установкой и lifecycle контейнера Caddy на `ru-vps`.
|
||||
Grimmory OPDS/KOReader routes имеют специальные headers и отключение compression;
|
||||
их нельзя упрощать без device compatibility tests.
|
||||
|
||||
## Backup и Update
|
||||
|
||||
`playbooks/pve-backup-jobs.yml` задает cluster-level PBS schedules. Offsite restic
|
||||
profiles выполняются systemd timers и используют application-aware SQLite backup или
|
||||
MariaDB dump. `roles/backup_audit` проверяет freshness, `restic check` и выборочные
|
||||
restores, после чего атомарно пишет textfile metrics.
|
||||
|
||||
Update playbooks выполняют:
|
||||
|
||||
```text
|
||||
fresh backup/audit -> reapply service declaration -> health verification
|
||||
```
|
||||
|
||||
Backup является prerequisite для ручного recovery, а не автоматическим rollback.
|
||||
|
||||
## Monitoring
|
||||
|
||||
Uptime Kuma в CT 146 является активным monitoring UI. Его роль останавливает и
|
||||
отключает `homelab-monitoring`, сохраняя конфигурацию и данные Prometheus stack.
|
||||
`make monitoring` защищен `CONFIRM=1` и предназначен только для отдельно принятого
|
||||
решения о восстановлении старого stack.
|
||||
|
||||
Exporter roles, groups и backup metrics остаются в репозитории. Hardcoded Prometheus
|
||||
targets могут расходиться с inventory, пока stack заморожен.
|
||||
|
||||
## Grimmory MCP
|
||||
|
||||
`tools/grimmory-mcp/src/server.js` запускает локальный stdio MCP. Grimmory API
|
||||
используется read-only; authentication POST не меняет library data. Явно вызванные
|
||||
sync tools читают API, опционально загружают cover, атомарно обновляют managed sections
|
||||
Obsidian notes и запускают внешний vault index script.
|
||||
|
||||
Sync не является общей транзакцией: full sync может закончиться после частичного
|
||||
набора успешных book updates. Vault path и credential file защищены отдельными
|
||||
проверками, описанными в [`edge-cases.md`](edge-cases.md).
|
||||
|
||||
## Testing Boundaries
|
||||
|
||||
- Ansible имеет static lint и syntax checks, но не имеет Molecule/idempotence harness.
|
||||
- `check.yml` проверяет reachability и expected IP, а не service health.
|
||||
- `status.yml` формирует наблюдательный отчет и не является failing health gate.
|
||||
- Grimmory MCP имеет Node test suite.
|
||||
- Gitea Actions workflow существует, но runner и Actions не активированы.
|
||||
|
||||
## Неизвестно
|
||||
|
||||
- Текущие runtime versions Proxmox VE, OpenVPN, Caddy и restic не закреплены repo manifests.
|
||||
- UI-only состояние AdGuard, Uptime Kuma monitors и часть service credentials не может
|
||||
быть установлена из репозитория.
|
||||
- Наличие DHCP reservation для Grimmory и намеренность отсутствия backup CT 148
|
||||
требуют проверки оператором.
|
||||
@@ -0,0 +1,104 @@
|
||||
# Edge Cases и риски
|
||||
|
||||
## Proxmox Provisioning
|
||||
|
||||
- Handled: ряд service playbooks проверяет hostname существующего VMID перед
|
||||
изменением контейнера.
|
||||
- Gap: generic `roles/pve_lxc` не содержит общего foreign-VMID guard; часть callers
|
||||
и direct `pct` playbooks также не выполняет ownership assertion.
|
||||
- Gap: некоторые `pct start` tasks считают любой return code `255` допустимым, что
|
||||
может скрыть ошибку, не связанную с already-running state.
|
||||
- Unknown: `--check --diff` не моделирует Proxmox module calls и command-heavy
|
||||
playbooks полностью. `make dry-*` является preview, а не isolated simulation.
|
||||
|
||||
## Validation Semantics
|
||||
|
||||
- `playbooks/check.yml` проверяет Ansible reachability и `expected_lan_ip`.
|
||||
- `playbooks/status.yml` подавляет многие command errors и отображает DOWN/failed
|
||||
состояния в отчете; успешный exit code не означает healthy infrastructure.
|
||||
- `make lint` запускает ansible-lint и yamllint из корня репозитория и включает
|
||||
all-playbook syntax loop, то есть воспроизводит CI.
|
||||
- CI workflow не исполняется без включенных Gitea Actions и runner.
|
||||
- Ansible roles не имеют Molecule, idempotence или integration test harness.
|
||||
|
||||
## Network Failure Domains
|
||||
|
||||
- Потеря `ru-vps` нарушает обычный ProxyJump к большинству hosts и public Caddy.
|
||||
- Потеря OpenVPN site tunnel нарушает private upstream path public services.
|
||||
- Потеря Mihomo одновременно затрагивает egress Hermes, внешние Uptime Kuma probes,
|
||||
emergency Telegram и Gyro notifications.
|
||||
- Normal SSH использует `StrictHostKeyChecking=accept-new`; emergency и Gyro paths
|
||||
требуют заранее проверенных pinned host keys.
|
||||
- Proxmox API TLS validation по шаблону `.env.example` отключен по умолчанию.
|
||||
|
||||
## Secrets и Privilege
|
||||
|
||||
- Handled: ignored `.env`, Vault, `0600`, `no_log` и external password files не
|
||||
позволяют хранить ожидаемые secrets в tracked config.
|
||||
- Constraint: никогда не переносить значения из ignored/archived `.env` в docs,
|
||||
output или commits.
|
||||
- Risk: `bootstrap-pve-api-token.yml` вращает privileged token и переписывает весь
|
||||
корневой `.env` целиком, а не построчно. Теряются `MONITORING_*`, `EMERGENCY_*` и
|
||||
`PROXMOX_ROOT_PASSWORD`. С 2026-09-02 у задачи `backup: true`, но восстанавливать
|
||||
придётся вручную. Запускать только как отдельную осознанную операцию.
|
||||
- Constraint: строки в `.env` пишутся с префиксом `export`, и это не стиль:
|
||||
`bootstrap-monitoring-pve-token.yml` ищет их через `regexp: "^export NAME="`.
|
||||
Убрать префикс — значит получить дубликаты строк вместо обновления.
|
||||
- Risk: LXC управляются как root, shell hosts используют `ansible` с passwordless sudo.
|
||||
|
||||
## Backups и Updates
|
||||
|
||||
- Handled: SQLite backup API + integrity check, atomic MariaDB dump, per-profile
|
||||
`flock`, PBS/restic freshness audit и selective restore.
|
||||
- Gap: update failures не запускают automatic rollback; backup только обеспечивает
|
||||
возможность ручного recovery.
|
||||
- Gap: PBS audit проверяет freshness, но не выполняет restore или PBS data integrity
|
||||
drill. Обычный `restic check` не читает все data packs.
|
||||
- Gap: recurring Grimmory audit проверяет non-empty SQL dump, но не импортирует его
|
||||
во временную MariaDB.
|
||||
- Gap: CT 148 `emergency-bot` не имеет backup/monitoring в service registry.
|
||||
- Constraint: code-only downgrade Grimmory после Flyway migration запрещен; нужен
|
||||
previous image вместе с pre-upgrade database backup.
|
||||
- Concurrency: нет repository-wide lock от одновременных operator/update runs;
|
||||
preflight через `pgrep vzdump` имеет race до запуска нового backup.
|
||||
- Handled: storage-level `prune-backups` on PVE storage `pbs` removed
|
||||
declaratively; retention now runs centrally in PBS `prune-pbs`, so the PBS
|
||||
side is the only authority for PBS-backed retention. The weekly PBS-container
|
||||
backup on storage `backup` with `keep-last=2` remains an intentional
|
||||
exception.
|
||||
|
||||
## Monitoring
|
||||
|
||||
- Active Uptime Kuma и frozen Prometheus stack не должны запускаться как две
|
||||
параллельные monitoring architectures без отдельного решения.
|
||||
- Hardcoded Prometheus targets могут расходиться с inventory и service registry.
|
||||
- Central monitoring на `cloud-pc` не может независимо сообщить о полном отказе
|
||||
своего node без внешнего наблюдателя.
|
||||
|
||||
## Reverse Proxy и Firewall
|
||||
|
||||
- Handled: reverse proxy metadata, Caddyfile, container config и upstreams проходят
|
||||
validation до/после restart.
|
||||
- Constraint: Ansible не управляет lifecycle Caddy container на `ru-vps`.
|
||||
- Constraint: не упрощать Grimmory OPDS/KOReader handlers и не удалять его
|
||||
`DOCKER-USER` protection без эквивалентных compatibility/security checks.
|
||||
- Constraint: не включать cluster-wide PVE firewall без аудита всех guests с
|
||||
`firewall=1`.
|
||||
|
||||
## Grimmory MCP
|
||||
|
||||
- Handled и tested: pagination guards, optional 204/404 resources, concurrent auth
|
||||
promises, credential ownership/mode/no-follow checks, vault path containment,
|
||||
cover size/type validation, atomic note writes и preservation of user markers.
|
||||
- Gap: full sync не транзакционен и может завершиться с частично обновленным vault.
|
||||
- Gap: нет mutex для concurrent sync/index rebuild.
|
||||
- Gap: поиск existing note для каждой книги повторно сканирует весь notes directory.
|
||||
- External dependency: Python и vault-owned index script должны существовать и
|
||||
завершиться в timeout; они не управляются этим репозиторием.
|
||||
|
||||
## Требующие проверки состояния
|
||||
|
||||
- Завершена ли initial UI configuration AdGuard.
|
||||
- Создана ли DHCP reservation/exclusion для Grimmory `192.168.1.34`.
|
||||
- Настроены ли Gyro Vault secrets; tracked configuration не подтверждает их наличие.
|
||||
- Является ли отсутствие backup для CT 148 намеренным решением.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Legacy и fragile boundaries
|
||||
|
||||
Этот файл не является backlog. Он предотвращает случайную замену active behavior
|
||||
более новым, старым или внешне похожим кодом без отдельного решения.
|
||||
|
||||
## Исторический Archive
|
||||
|
||||
- Path: `archive/2026-07-proxmox-migration/`.
|
||||
- Evidence: каталог содержит прежние NixOS, Docker Compose, GitOps, ZeroTier и другие
|
||||
pre-Ansible материалы.
|
||||
- Constraint: не редактировать и не возвращать файлы из archive как active config.
|
||||
- Decision: accepted historical reference; active implementation создается в `ansible/`.
|
||||
|
||||
ZeroTier удален из active infrastructure 11 июля 2026 года. Active inventory, roles
|
||||
и playbooks его не содержат.
|
||||
|
||||
## Frozen Monitoring Stack
|
||||
|
||||
- Paths: `ansible/roles/monitoring_server/`, `monitoring_exporter/`,
|
||||
`monitoring_blackbox/`, `ansible/playbooks/monitoring.yml`.
|
||||
- Evidence: `ansible/Makefile` помечает target `monitoring` как frozen и требует
|
||||
`CONFIRM=1`; role `uptime_kuma` останавливает `homelab-monitoring`.
|
||||
- Constraint: наличие кода не означает, что Prometheus stack активен.
|
||||
- Decision: defer; Uptime Kuma является active monitoring до нового решения.
|
||||
|
||||
## Prototype Roles
|
||||
|
||||
- Paths: `ansible/roles/lxc_docker_host/`, `ansible/roles/compose_service/`.
|
||||
- Evidence: `ansible/roles/README.md`. `compose_service` с 2026-09-02 вызывается из
|
||||
`playbooks/ru-vps-base.yml` (стек Caddy); `lxc_docker_host` по-прежнему не вызывается
|
||||
ни одним playbook.
|
||||
- Constraint: не считать direct `pve-*.yml` dead code и не мигрировать service как
|
||||
opportunistic cleanup. Миграция меняет runtime, pull, firewall и recreation semantics.
|
||||
- Decision: defer; выполнять отдельно по одному service с backup и health validation.
|
||||
|
||||
Empty `ansible/roles/base`, `docker` и `ufw` являются остатками ранней структуры, а
|
||||
не active reusable roles.
|
||||
|
||||
## Duplicated Service Facts
|
||||
|
||||
- Paths: service registry, `pve-*.yml`, `pve-backup-jobs.yml`, backup audit defaults,
|
||||
monitoring templates, status playbook и SSH config.
|
||||
- Evidence: реестр потребляют reverse proxy, `ru-vps-base.yml`, backup jobs, backup
|
||||
audit и `validate.yml`; `pve-*.yml`, `status.yml`, monitoring и SSH config всё ещё
|
||||
дублируют значения.
|
||||
- Constraint: изменение VMID/IP/image/backup/monitoring требует сверки оставшихся
|
||||
consumers. `make validate` ловит расхождение реестра с Proxmox по hostname, IP,
|
||||
cores, memory и swap, но не по образам, бэкапам и SSH config.
|
||||
- Decision: accepted risk до отдельной migration/validator задачи.
|
||||
|
||||
## Partial Ownership
|
||||
|
||||
- Caddy installation/container lifecycle на `ru-vps` и provisioning PBS CT 120 не
|
||||
управляются репозиторием.
|
||||
- Hermes playbook подготавливает runtime/proxy, но не deploy самого Hermes application.
|
||||
- UI state Uptime Kuma и AdGuard не полностью декларативен.
|
||||
- Constraint: не заявлять полную reproducibility этих компонентов без проверки
|
||||
внешнего состояния.
|
||||
|
||||
## Compatibility Layers
|
||||
|
||||
- Gitea/Vaultwarden `caddy_legacy_regexp` удаляет старые Caddy sections. Не удалять
|
||||
поля до подтвержденного успешного reverse-proxy migration run.
|
||||
- Grimmory OPDS/KOReader headers и compression behavior являются device compatibility
|
||||
contract, а dual v1/v2 API handling MCP соответствует deployed Grimmory v3.2.4.
|
||||
- OpenVPN использует static-key configuration. OpenVPN 2.6 считает этот режим
|
||||
deprecated; миграция на TLS требует отдельного network change plan.
|
||||
|
||||
## Superseded Planning Documents
|
||||
|
||||
- `current-task.md` - исторический Prometheus plan, не active task.
|
||||
- `tasks/grimmory-deployment-plan.md` - смешивает план и deployment record; текущее
|
||||
состояние проверять по Ansible.
|
||||
- Constraint: не выполнять оставшиеся пункты этих документов автоматически.
|
||||
- Decision: preserve as history; новые approved tasks записывать в `plan.md`.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Официальные ссылки
|
||||
|
||||
Ссылки подобраны только для контрактов, которые нельзя надежно вывести из локального
|
||||
кода. Если runtime version не закреплена, это указано явно.
|
||||
|
||||
| Тема | Официальная ссылка | Применимость |
|
||||
|---|---|---|
|
||||
| Ansible check/diff | [Check and diff mode](https://docs.ansible.com/projects/ansible-core/devel/playbook_guide/playbooks_checkmode.html) | Объясняет partial simulation и modules без check-mode; rolling docs, repository lower bound `>=2.19` |
|
||||
| Proxmox Ansible module | [`community.proxmox.proxmox`](https://docs.ansible.com/ansible/latest/collections/community/proxmox/proxmox_module.html) | API provisioning; installed Galaxy version не закреплена |
|
||||
| Proxmox LXC API | [LXC API endpoint](https://pve.proxmox.com/pve-docs/api-viewer/index.html#/nodes/{node}/lxc) | Контракт API role; live PVE version требует проверки |
|
||||
| `pct` | [`pct(1)`](https://pve.proxmox.com/pve-docs/pct.1.html) | Direct-SSH provisioners и diagnostics; rolling PVE manual |
|
||||
| OpenVPN | [OpenVPN 2.6 manual](https://build.openvpn.net/man/openvpn-2.6/openvpn.8.html) | Site/laptop tunnel directives; peer versions неизвестны, static-key mode deprecated в 2.6 |
|
||||
| Caddy validation | [`caddy validate`](https://caddyserver.com/docs/command-line#caddy-validate) | Используется reverse proxy playbook; Caddy version не закреплена |
|
||||
| Caddy reverse proxy | [`reverse_proxy`](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy) | Public upstream и Grimmory handlers |
|
||||
| restic integrity | [Checking integrity and consistency](https://restic.readthedocs.io/en/stable/045_working_with_repos.html#checking-integrity-and-consistency) | Различает metadata check и чтение data packs; runtime version неизвестна |
|
||||
| restic restore | [Restore](https://restic.readthedocs.io/en/stable/050_restore.html) | Selective L2 restore в backup audit |
|
||||
| Uptime Kuma | [README 1.23.16](https://github.com/louislam/uptime-kuma/blob/1.23.16/README.md) | Совпадает с pinned active image version |
|
||||
| MCP TypeScript SDK | [Server guide 1.30.0](https://github.com/modelcontextprotocol/typescript-sdk/blob/1.30.0/docs/server.md) | Совпадает с exact package version и stdio server implementation |
|
||||
| Grimmory API | [OpenAPI v3.2.4](https://github.com/grimmory-tools/grimmory/releases/download/v3.2.4/openapi.json) | Совпадает с deployed version; live docs newer и API объявлен unstable |
|
||||
| Grimmory release | [Release v3.2.4](https://github.com/grimmory-tools/grimmory/releases/tag/v3.2.4) | Release/migration review перед изменением app image |
|
||||
@@ -0,0 +1,788 @@
|
||||
# Переход provisioning LXC на OpenTofu
|
||||
|
||||
Пошаговый план миграции. Рассчитан на исполнителя, который этот разговор не
|
||||
видел: всё нужное либо здесь, либо по ссылкам на файлы репозитория.
|
||||
|
||||
Стратегия — **blue-green**: боевые контейнеры не импортируются в Tofu и не
|
||||
переконфигурируются на месте. Вместо этого рядом создаётся новый контейнер,
|
||||
туда переносятся данные, затем переключается адрес, а старый контейнер
|
||||
некоторое время стоит остановленным как откат.
|
||||
|
||||
---
|
||||
|
||||
## 0. Как начать сессию по этому плану
|
||||
|
||||
Открыть Claude Code в корне репозитория и дать примерно такой промпт,
|
||||
подставив нужный сервис:
|
||||
|
||||
Работаем по docs/ai/migration-tofu.md — переход provisioning LXC на OpenTofu
|
||||
по схеме blue-green. Прочитай его целиком, а также tofu/README.md и
|
||||
AGENTS.md.
|
||||
|
||||
Делаем сервис №1 из раздела 5 (emergency-bot, CT 148). Идём строго по
|
||||
процедуре раздела 4, по шагам, не забегая вперёд.
|
||||
|
||||
Правила:
|
||||
- разрушающие шаги (pct stop, pct destroy, tofu-destroy, смена адреса)
|
||||
выполняешь только после моего явного подтверждения;
|
||||
- старый контейнер не удаляем, он остаётся откатом;
|
||||
- make validate должен быть зелёным до и после;
|
||||
- если реальность разошлась с планом — останавливайся и говори, не
|
||||
придумывай обход.
|
||||
|
||||
Между сервисами сессию имеет смысл начинать заново: контекст одного переезда
|
||||
следующему не нужен, а свежий контекст надёжнее.
|
||||
|
||||
---
|
||||
|
||||
## 1. Что уже сделано — не переделывать
|
||||
|
||||
- `tofu/` с провайдером `bpg/proxmox`, цели `make tofu-init | tofu-plan |
|
||||
tofu-apply CONFIRM=1 | tofu-destroy CONFIRM=1`.
|
||||
- Аутентификация: `root@pam` по паролю из корневого `.env`
|
||||
(`PROXMOX_ROOT_USER`, `PROXMOX_ROOT_PASSWORD`). Выбор режима автоматический,
|
||||
печатается в stderr. Подробности и матрица возможностей — `tofu/README.md`.
|
||||
- Цели `tofu-*` сами поднимают SSH-туннель через `ru-vps`: провайдер ходит в
|
||||
API по HTTPS и ProxyJump не умеет.
|
||||
- Реестр `homelab_services` программно потребляют: `reverse-proxy.yml`,
|
||||
`ru-vps-base.yml`, `pve-backup-jobs.yml` (списки VMID по `backup.job`),
|
||||
`roles/backup_audit` (VMID по `monitoring.backup_audit_vmid`), `validate.yml`.
|
||||
- `make validate` — read-only gate, сверяет реестр с `pct config` по hostname,
|
||||
IP, cores, memory, swap. Падает при расхождении.
|
||||
- `make lint` воспроизводит CI (линтеры из корня + syntax-check всех плейбуков).
|
||||
|
||||
### Что проверено на пилоте (VMID 199, снесён)
|
||||
|
||||
В режиме `root@pam` Tofu выставляет декларативно всё, что нужно этой
|
||||
инфраструктуре:
|
||||
|
||||
dev0: deny-write=0,path=/dev/net/tun,uid=0,gid=0,mode=0660
|
||||
features: fuse=1,keyctl=1,nesting=1
|
||||
mp0: data:199/vm-199-disk-1.raw,mp=/opt/pilot-data,size=4G
|
||||
|
||||
Повторный `plan` даёт `No changes`. Следствия:
|
||||
|
||||
- правка `/etc/pve/lxc/<vmid>.conf` через `lineinfile` (девять контейнеров)
|
||||
заменяется декларацией в Tofu, но КАКОЙ именно — зависит от устройства:
|
||||
`/dev/fuse` — штатным флагом `features { fuse = true }`, `/dev/net/tun` —
|
||||
блоком `device_passthrough`. Оба варианта видны в выводе пилота выше:
|
||||
`features: fuse=1,...` и `dev0: ...path=/dev/net/tun`;
|
||||
- шаг `pct set <vmid> --features nesting=1,keyctl=1` больше не нужен;
|
||||
- `lifecycle { ignore_changes = [features] }` не нужен;
|
||||
- apply одной фазой.
|
||||
|
||||
**Важно:** всё это работает ТОЛЬКО под `root@pam` по паролю. API-токен, даже
|
||||
принадлежащий root, проверку не проходит — в Proxmox она буквальная
|
||||
(`$authuser eq 'root@pam'`, `PVE/LXC.pm:1658`), а при токенной аутентификации
|
||||
`$authuser` равен полному `user@realm!tokenname` (`PVE/HTTPServer.pm:86`).
|
||||
|
||||
### Ключевое упрощение
|
||||
|
||||
**Если при переезде сохранить IP, меняется только VMID.** А все потребители
|
||||
VMID уже выводят его из реестра автоматически (backup jobs, backup audit).
|
||||
Поэтому cutover — это правка одного поля `vmid` в `services.yml`, а не
|
||||
синхронная правка шести файлов. Ради этого и делалась генерация.
|
||||
|
||||
---
|
||||
|
||||
## 2. Инварианты
|
||||
|
||||
1. **Последовательным обязан быть только cutover.** Инвариант изначально
|
||||
звучал как «один сервис за раз»; 2026-09-02 он уточнён после параллельного
|
||||
прогона шести сервисов. Разделение такое:
|
||||
- **Параллелится безопасно:** подготовка (снятие эталона, описание в Tofu),
|
||||
создание новых контейнеров (один пакетный `apply`, Tofu сам разводит
|
||||
ресурсы) и конфигурация на временных адресах (шаг 4.3). Всё это не
|
||||
трогает боевые контейнеры и полностью откатывается точечным
|
||||
`tofu destroy` нужного ресурса.
|
||||
- **Остаётся строго последовательным:** перенос данных (4.4), переключение
|
||||
адреса (4.5) и правка реестра (4.6) — по одному сервису, с явным
|
||||
подтверждением человека на каждый разрушающий шаг.
|
||||
Причины, по которым `apply` физически не параллелится: состояние Tofu одно
|
||||
и локальное, а цели `make tofu-*` поднимают SSH-туннель на фиксированный
|
||||
порт 18006 с фиксированным control-сокетом. Два одновременных прогона
|
||||
столкнутся.
|
||||
2. **Старый контейнер останавливается, но не удаляется** — минимум неделю. Это
|
||||
единственный быстрый откат.
|
||||
3. `make validate` зелёный до и после каждого переезда.
|
||||
4. Перед стартом каждого сервиса — свежий бэкап PBS этого VMID.
|
||||
5. VMID и IP выведенных контейнеров не переиспользовать сразу: в PBS остаются
|
||||
цепочки по VMID, в кэшах — адреса.
|
||||
6. **CT 120 `pbs` не мигрировать.** Это цель бэкапов, на которую опирается план
|
||||
отката всех остальных. Он `unmanaged` и таким остаётся.
|
||||
7. Не менять одновременно provisioning и рантайм сервиса без необходимости.
|
||||
Если переводишь `docker run` на `compose_service` — это отдельное
|
||||
осознанное решение по конкретному сервису, а не часть переезда по умолчанию.
|
||||
|
||||
---
|
||||
|
||||
## 3. Предпосылки перед первым переездом
|
||||
|
||||
- [x] Вывод `memoir-bot` — live-шаги оператора выполнены 2026-09-02: deploy key
|
||||
`SecondBrain` отозван, монитор в Uptime Kuma снят, `pct stop 142`
|
||||
выполнен (подтверждено `pct list` на mini-pc). Это была репетиция
|
||||
удаления старого контейнера. `pct destroy 142` сознательно отложен —
|
||||
см. [`plan.md`](plan.md) и общий инвариант о паузе перед удалением.
|
||||
- [x] `make validate` — зелёный (проверено 2026-09-02, 12 сервисов, drift нет).
|
||||
- [x] `make backup-audit` — прогнан 2026-09-02, все хосты `ok`, без ошибок.
|
||||
- [x] `tofu/terraform.tfstate` — решено оставить только локальным на время
|
||||
переезда сервиса №1. Осознанный риск: state небольшой, при потере можно
|
||||
re-import всё созданное заново. Постоянное решение (например, offsite
|
||||
restic-профиль) — отдельная задача, не блокирует старт.
|
||||
- [x] Свободное место проверено. На 2026-09-02: cloud-pc `data` — 816 ГиБ
|
||||
свободно, mini-pc `local-lvm` — 116 ГиБ. Запаса хватает на любой сервис,
|
||||
включая grimmory (64 ГиБ).
|
||||
|
||||
### Пул временных адресов и VMID
|
||||
|
||||
Свободны на 2026-09-02: IP `192.168.1.6-9, 11-19, 21, 22, 26, 33, 36-40`,
|
||||
VMID `151+` и `199`. Временный адрес обязан лежать в `192.168.1.5-40` —
|
||||
только этот диапазон ru-vps маршрутизирует в LAN через `tun0`, иначе новый
|
||||
контейнер будет недоступен по ProxyJump.
|
||||
|
||||
Шаблон `local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst` присутствует на
|
||||
обеих нодах — проверено.
|
||||
|
||||
---
|
||||
|
||||
## 4. Процедура переезда одного сервиса
|
||||
|
||||
Обозначения: `OLD` — текущий VMID, `NEW` — временный VMID, `IP` — боевой адрес,
|
||||
`TMPIP` — временный адрес.
|
||||
|
||||
### 4.1 Подготовка
|
||||
|
||||
1. `make validate` и `make backup-audit` — оба зелёные.
|
||||
2. Снять эталон: `ssh <node> sudo pct config OLD` — сохранить вывод. Это
|
||||
источник правды для того, что нужно воспроизвести.
|
||||
3. Запустить свежий бэкап: `ssh <node> sudo vzdump OLD --storage pbs --mode snapshot`.
|
||||
- **Успех проверять наличием снапшота, а не кодом возврата.** Исторически
|
||||
на 2026-09-02 client-side prune давал `missing Datastore.Modify|Datastore.Prune`,
|
||||
из-за чего `vzdump` печатал `Backup of VM ... failed` и возвращал ненулевой
|
||||
код после успешной выгрузки. Это объяснение для старых логов; сейчас
|
||||
client-side prune из плейбуков убран, поэтому любой новый non-zero надо
|
||||
считать проблемой. Проверять так:
|
||||
`ssh cloud-pc 'sudo pvesm list pbs | grep ct/<OLD>'`. Подробности —
|
||||
[`plan.md`](plan.md), раздел про prune.
|
||||
- **Если у сервиса Docker с fuse-overlayfs, а rootfs на каталоговом
|
||||
хранилище (`data`) — сначала остановить сервис.** Такой rootfs не
|
||||
поддерживает снапшоты, vzdump уходит в режим `suspend` с rsync и падает
|
||||
на `var/lib/docker/fuse-overlayfs/.../merged`: Permission denied.
|
||||
Проверено на gitea 2026-09-02: с остановленным сервисом бэкап проходит.
|
||||
У сервисов на `local-lvm` (vaultwarden) проблемы нет — там снапшот.
|
||||
- **У сервиса с bind mount в бэкап попадает только rootfs.** vzdump пишет
|
||||
`excluding bind mount point mpN (...) from backup (not a volume)`. До
|
||||
переезда это касалось gitea: его репозитории и БД в PBS не попадали
|
||||
вообще. После перехода на volume попадают.
|
||||
|
||||
### 4.2 Описание в Tofu
|
||||
|
||||
4. Завести ресурс в `tofu/` (например `tofu/services.tf`), воспроизведя
|
||||
эталон: `vm_id = NEW`, hostname как у боевого, `TMPIP`, cores, memory, swap,
|
||||
rootfs на том же datastore и того же размера, `features`, декларацию для
|
||||
каждого устройства из `lxc.mount.entry`, `mount_point` для каждого `mpN`,
|
||||
`startup.order`, `start_on_boot`, `unprivileged`, `console { type = "shell" }`
|
||||
(эталон `cmode` — см. ниже).
|
||||
- **`/dev/fuse` — это `features.fuse`, а НЕ `device_passthrough`.**
|
||||
Ловушка: в эталоне `/dev/fuse` выглядит как пара сырых строк
|
||||
(`lxc.cgroup2.devices.allow: c 10:229 rwm` + `lxc.mount.entry`), и её
|
||||
хочется механически перевести в `device_passthrough`. Так делать не надо:
|
||||
`device_passthrough` (`dev0:` в PVE 8.2+) предназначен для сырых
|
||||
character-устройств вида `/dev/net/tun`, а у `/dev/fuse` есть штатный
|
||||
флаг PVE, проверенный на пилоте. См. `tofu/pilot.tf.example:17-21`.
|
||||
Следствие: `pct config` нового контейнера покажет `features: fuse=1,...`,
|
||||
то есть будет текстуально отличаться от эталона при том же эффекте —
|
||||
это ожидаемо, а не drift.
|
||||
- **Bind mount заменять на volume.** Если у сервиса `mpN: /host/path,mp=...`
|
||||
(сейчас так только у gitea), в новом контейнере это должен быть
|
||||
`mount_point { volume = "<datastore>", size = "...", path = "..." }`.
|
||||
Данные переносятся на шаге 4.4, а не монтированием того же каталога:
|
||||
два контейнера, пишущие в один каталог, повредят данные.
|
||||
- **`console { type = "shell" }` обязателен.** `roles/pve_lxc` всем
|
||||
контейнерам ставит `cmode: shell`; провайдер отслеживает это через блок
|
||||
`console`, и без явного объявления следующий `tofu-plan` предложит
|
||||
откатить его на дефолт Proxmox `tty` — тот же класс drift, что и
|
||||
`keyctl` в гибридной схеме. Подробности и история находки —
|
||||
`tofu/README.md`.
|
||||
5. `make tofu-plan` — убедиться, что план ровно `1 to add`.
|
||||
6. `make tofu-apply CONFIRM=1`.
|
||||
7. Сверить: `ssh <node> sudo pct config NEW` против эталона из шага 2. Отличаться
|
||||
должны только `vmid` и адрес (плюс явно выписанные Proxmox-дефолты вида
|
||||
`cpulimit`, `protection`, `template`, `tty` — эталон их просто не
|
||||
показывал, реальное поведение то же самое). `cmode: shell` должен совпасть
|
||||
с эталоном уже на этом шаге, если блок `console` объявлен в шаге 4 — правкой
|
||||
постфактум через `pct set` не чинить, это создаёт drift (см. шаг 4 выше и
|
||||
`tofu/README.md`).
|
||||
|
||||
### 4.3 Настройка
|
||||
|
||||
8. Добавить временную запись в `ansible/inventory/hosts.yml`: `<name>-new` с
|
||||
`ansible_host: TMPIP` и `expected_lan_ip: TMPIP`, и такую же запись в
|
||||
`ansible/ssh_config` (Host-блок + в агрегированные строки ProxyJump, root,
|
||||
общие опции).
|
||||
- Во время cutover использовалась временная группа `lxc_migration_new`;
|
||||
она не входила в `servers`, поэтому незавершённый переезд не краснил
|
||||
`make check` и `make status`.
|
||||
- **Транспорт задавался только в `group_vars/lxc_migration_new/main.yml`, а
|
||||
не блоком `vars:` в самом `hosts.yml`.** У Ansible inline-переменные
|
||||
группы из inventory-файла имеют приоритет НИЖЕ, чем `group_vars/all/`,
|
||||
поэтому `ansible_user: ansible` оттуда перебивает inline `root`, и
|
||||
подключение падает с `Permission denied`. Каталог `group_vars/<группа>/`
|
||||
— наоборот, выше `all`. Найдено 2026-09-02.
|
||||
|
||||
9. Прогнать конфигурационную часть существующего плейбука против нового
|
||||
контейнера:
|
||||
|
||||
ansible-playbook playbooks/pve-<name>.yml \
|
||||
-e pve_config_target=<name>-new --limit <name>-new
|
||||
|
||||
**Одного `--limit <name>-new` НЕДОСТАТОЧНО, и это не особенность отдельного
|
||||
сервиса.** Ansible пересекает лимит с паттерном play, а не подменяет его.
|
||||
Паттерн конфигурационного play — литеральное имя боевого хоста (`hosts:
|
||||
gitea`), пересечение с `gitea-new` пусто, и play молча получает `hosts (0)`.
|
||||
Проверено 2026-09-02 через `--list-hosts` на всех плейбуках сразу: ноль
|
||||
хостов во ВСЕХ play, а не только в создающих. Прошлая сессия
|
||||
(emergency-bot) сочла это особенностью того сервиса — на самом деле это
|
||||
общее свойство.
|
||||
|
||||
Поэтому у конфигурационных play заведён переопределяемый таргет:
|
||||
|
||||
hosts: "{{ pve_config_target | default('<name>') }}"
|
||||
|
||||
Он есть в `pve-docker-test.yml`, `pve-gitea.yml`, `pve-vaultwarden.yml`,
|
||||
`pve-grimmory.yml`, `gyro.yml` и `uptime-kuma.yml`. По умолчанию поведение
|
||||
не меняется — без `-e` паттерн равен прежнему литералу.
|
||||
|
||||
`--limit` при этом всё равно обязателен: он отсекает play создания
|
||||
контейнера и play-стражи, которые таргетят ноду (`cloud-pc`/`mini-pc`) или
|
||||
`localhost`. Без него плейбук пойдёт делать `pct create` и править
|
||||
`/etc/pve/lxc/<OLD>.conf` на боевом VMID.
|
||||
|
||||
**Перед каждым прогоном сверяться с `--list-hosts`.** Ожидание: у play
|
||||
создания и стражей `hosts (0)`, у конфигурационного — ровно `hosts (1):
|
||||
<name>-new`. Это и есть предохранитель, а не формальность.
|
||||
|
||||
Особые случаи:
|
||||
- `monitoring` — конфигурации в `pve-monitoring.yml` нет вообще, там только
|
||||
создание. Настраивает `playbooks/uptime-kuma.yml` (роль `uptime_kuma`).
|
||||
`playbooks/monitoring.yml` (замороженный Prometheus) НЕ запускать.
|
||||
- `gyro` — конфигурация в `playbooks/gyro.yml`, нужен пароль Ansible Vault
|
||||
(`make gyro` / `--ask-vault-pass`). Роль читает `host_vars/gyro/`.
|
||||
Во время cutover использовался временный host_vars shim; после перевода CT
|
||||
156 на боевой IP он удалён.
|
||||
|
||||
10. Проверить, что сервис поднялся на `TMPIP` (его health-эндпоинт или порт).
|
||||
У сервисов с данными новый контейнер на этом шаге работает на ПУСТОМ
|
||||
состоянии — данные приезжают только на шаге 4.4. Пустой Gitea, пустой
|
||||
Vaultwarden с экраном создания учётки, пустая Uptime Kuma, свежая схема
|
||||
Flyway у grimmory — это ожидаемый результат шага 4.3, а не поломка.
|
||||
Поднимать новую копию на боевых данных до остановки старой НЕЛЬЗЯ: две
|
||||
живые копии над одной SQLite или одной MariaDB повредят состояние.
|
||||
|
||||
### 4.4 Перенос данных
|
||||
|
||||
11. Остановить сервис на СТАРОМ контейнере (`systemctl stop <unit>`), сам
|
||||
контейнер пока не трогать.
|
||||
12. Финальная синхронизация. Способ зависит от сервиса — см. раздел 5.
|
||||
13. Запустить сервис на новом, проверить на `TMPIP`.
|
||||
|
||||
### 4.5 Переключение
|
||||
|
||||
14. `ssh <node> sudo pct stop OLD`.
|
||||
15. В Tofu поменять адрес нового контейнера с `TMPIP` на `IP`, `make tofu-apply CONFIRM=1`.
|
||||
16. Перезапустить контейнер, если адрес не подхватился на живую.
|
||||
- **Если конфигурация сервиса содержит его собственный адрес — прогнать
|
||||
плейбук заново сразу после смены адреса.** Из пакета 2026-09-02 это
|
||||
касается только `grimmory` (`ports: <ip>:6060:6060` и правила
|
||||
`GRIMMORY-FILTER`): его compose всё ещё содержит TMPIP, и сервис не
|
||||
поднимется, пока файл не перегенерируют. Проверять просто:
|
||||
`ss -ltn` на новом контейнере — если сокет висит на конкретном адресе,
|
||||
а не на `0.0.0.0`/`*`, повторный прогон обязателен. У gitea,
|
||||
vaultwarden и uptime-kuma сокеты wildcard, им это не нужно.
|
||||
Прогон уже без `-e pve_config_target`: адрес стал боевым, и имя хоста
|
||||
само резолвится в новый контейнер.
|
||||
17. Health-check на боевом `IP`.
|
||||
18. Если сервис публичный (`proxy` в реестре) — `make reverse-proxy` и проверить
|
||||
домен снаружи. При сохранённом IP апстрим не меняется, но прогон подтвердит.
|
||||
19. Обновить `~/.ssh/known_hosts` — host key нового контейнера другой:
|
||||
`ssh-keygen -R <IP>` и `ssh-keygen -R <name>`. В `ssh_config` стоит
|
||||
`StrictHostKeyChecking accept-new`, который принимает НОВЫЕ ключи, но не
|
||||
изменившиеся, поэтому без чистки подключение будет отвергнуто.
|
||||
- **Заодно закрыть ControlMaster: `ssh -F ansible/ssh_config -O exit
|
||||
<host>`.** Мастер переживает cutover живым, но его туннель ведёт в уже
|
||||
остановленный контейнер; новые сессии мультиплексируются поверх мёртвого
|
||||
соединения и виснут с `Connection timed out during banner exchange`.
|
||||
Симптом выглядит как сетевая проблема, хотя контейнер полностью доступен
|
||||
с ноды. Разовый обход для диагностики — `-o ControlPath=none`.
|
||||
Найдено 2026-09-02 на docker-test.
|
||||
- У gitea SSH host-ключи git-over-ssh лежат в переносимых данных
|
||||
(`<data>/ssh/ssh_host_*`), поэтому порт 2222 после переезда отдаёт ТОТ ЖЕ
|
||||
ключ и `known_hosts` git-клиентов остаётся валидным. Менять нужно только
|
||||
ключ sshd самого контейнера (порт 22).
|
||||
- **Про gyro и gitea: ранее записанное предупреждение неверно, проверено
|
||||
2026-09-02.** Утверждалось, что `roles/gyro` пинит host key
|
||||
`[192.168.1.25]:2222` и после переезда gitea сломается. На самом деле
|
||||
задача `Remove obsolete Gitea known host` стоит с `state: absent` — она
|
||||
УДАЛЯЕТ устаревшую запись, а не создаёт её. Пинится
|
||||
`gyro_git_known_hosts_name: github.com`, и `gyro_repo_url` —
|
||||
`git@github.com:...`. Gyro клонирует с GitHub, к gitea отношения не
|
||||
имеет. Никаких дополнительных действий после переезда gitea не нужно.
|
||||
|
||||
### 4.5a Offsite restic: профиль надо переустановить на новом контейнере
|
||||
|
||||
**Шаг, которого в плане не было. Найден 2026-09-02 на vaultwarden.**
|
||||
|
||||
Если offsite-профиль restic выполняется ВНУТРИ контейнера сервиса (а не на
|
||||
ноде), то на новом контейнере его нет: там нет ни `restic`, ни `rclone`, ни
|
||||
systemd-таймера. Бэкап тихо перестаёт выполняться, и `make backup-audit` этого
|
||||
не ловит — он ставит только свой таймер аудита.
|
||||
|
||||
Проверять так:
|
||||
|
||||
ssh <name> 'command -v restic rclone; systemctl list-timers --all | grep restic'
|
||||
|
||||
Чинить так:
|
||||
|
||||
make offsite-restic EXTRA="--limit <name>"
|
||||
|
||||
Затем обязательно проверить всю цепочку, а не только наличие таймера:
|
||||
|
||||
ssh <name> 'systemctl start homelab-restic-offsite-<name>.service'
|
||||
ssh <name> 'journalctl -u homelab-restic-offsite-<name>.service -n 30 --no-pager'
|
||||
|
||||
Успех выглядит как подключение к репозиторию с уже существующими снапшотами и
|
||||
`Finished ... Deactivated successfully`.
|
||||
|
||||
Кого это касается:
|
||||
|
||||
- `vaultwarden` — профиль внутри LXC. Сделано и проверено 2026-09-02.
|
||||
- `grimmory` — профиль тоже внутри LXC (`hosts: grimmory`). Понадобится то же.
|
||||
- `gitea` — профиль выполняется на cloud-pc и указывает на `/opt/data/gitea`.
|
||||
Там задача другая: после переезда на volume этот путь исчезает, профиль надо
|
||||
переписать на выполнение внутри контейнера, по образцу vaultwarden.
|
||||
|
||||
### 4.6 Реестр и потребители
|
||||
|
||||
20. В `ansible/inventory/group_vars/all/services.yml` поменять `vmid: OLD` на
|
||||
`vmid: NEW`. Если менялся `provisioner` — поправить и его на `tofu`.
|
||||
21. Убрать временные записи `<name>-new` из `hosts.yml` и `ssh_config`.
|
||||
22. Прогнать зависящие цели: `make backup-jobs` (списки VMID выведутся заново),
|
||||
`make backup-audit` (перерендерит скрипт аудита с новым VMID).
|
||||
23. `make validate` — должен быть зелёный.
|
||||
24. `make status` — контейнер UP, юниты active.
|
||||
|
||||
### 4.7 Завершение
|
||||
|
||||
25. Оставить `OLD` остановленным минимум неделю.
|
||||
26. Затем `pct destroy OLD` и решить судьбу его цепочки бэкапов в PBS.
|
||||
27. Удалить из `playbooks/pve-<name>.yml` часть, создающую контейнер (первый play
|
||||
с `pct create` / ролью `pve_lxc`, правками `/etc/pve/lxc/*.conf` и
|
||||
`pct set --features`). Оставить конфигурационную часть.
|
||||
|
||||
---
|
||||
|
||||
## 5. Порядок сервисов и специфика
|
||||
|
||||
Порядок выбран по возрастанию риска. Не менять без причины.
|
||||
|
||||
| # | Сервис | VMID | Узел | Почему здесь |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `emergency-bot` | ~~148~~ **151** | mini-pc | ПЕРЕЕХАЛ 2026-09-02. Полностью шаблонный, персистентных данных нет |
|
||||
| 2 | `docker-test` | ~~145~~ **152** | cloud-pc | ПЕРЕЕХАЛ 2026-09-02 |
|
||||
| 3 | `gitea` | ~~141~~ **153** | cloud-pc | ПЕРЕЕХАЛ 2026-09-02; bind mount → volume, данные впервые попали в PBS |
|
||||
| 4 | `vaultwarden` | ~~140~~ **154** | mini-pc | ПЕРЕЕХАЛ 2026-09-02 |
|
||||
| 5 | `monitoring` | ~~146~~ **155** | cloud-pc | ПЕРЕЕХАЛ 2026-09-02; замороженный стек не переносился |
|
||||
| 6 | `gyro` | ~~150~~ **156** | mini-pc | CUTOVER COMPLETED 2026-09-02; CT156 live on 192.168.1.35, CT150 held as rollback |
|
||||
| 7 | `grimmory` | ~~149~~ **157** | cloud-pc | ПЕРЕЕХАЛ 2026-09-02; MariaDB через dump/restore |
|
||||
| 8 | `adguard` | ~~144~~ **158** | mini-pc | ПЕРЕЕХАЛ 2026-09-03; данные /opt/adguard, DNS-провал ~2 мин |
|
||||
| 9 | `mihomo` | ~~143~~ **159** | mini-pc | ПЕРЕЕХАЛ 2026-09-03; первый боевой device_passthrough /dev/net/tun |
|
||||
| 10 | `ovpn-mini` | ~~132~~ **160** | mini-pc | ПЕРЕЕХАЛ 2026-09-03; туннель-петля при cutover, см. 5.10 |
|
||||
| — | `hermes-ai` | 147 | cloud-pc | заморожен и недостижим по SSH, см. раздел 7 |
|
||||
| — | `pbs` | 120 | cloud-pc | не мигрировать |
|
||||
|
||||
**Все мигрируемые сервисы (1-10) переехали на OpenTofu.** Осталось: `hermes-ai`
|
||||
(заморожен), `pbs` (не мигрируется). Пост-миграционная уборка раздела 6
|
||||
(удаление `roles/pve_lxc`, стрип creation plays у 8 плейбуков, правки
|
||||
`architecture.md`) — отдельная задача, ещё не сделана; частично разблокирована.
|
||||
|
||||
### Специфика по сервисам
|
||||
|
||||
**1. `emergency-bot`.** ПЕРЕЕХАЛ 2026-09-02: OLD 148 остановлен, NEW 151 боевой
|
||||
на 192.168.1.32. Данных нет вообще: `roles/emergency_bot` разворачивает
|
||||
`emergency_bot.py.j2`, `.env.j2` и юнит из шаблонов. Шаг 4.4 пропущен целиком.
|
||||
Бэкапа у него нет (`backup: none`) и это не мешает — он воспроизводим из
|
||||
репозитория. Нужны `EMERGENCY_*` в корневом `.env`. Хороший первый заход
|
||||
именно потому, что ошибиться почти негде — тем не менее, реальность разошлась
|
||||
с планом в двух местах, оба задокументированы там, где их искать:
|
||||
|
||||
- **`playbooks/pve-emergency-bot.yml` не содержит конфигурационной части.**
|
||||
У этого сервиса она в отдельном `playbooks/emergency-access.yml`, причём
|
||||
два из четырёх play там таргетят `mini-pc` и `ru-vps`, а не сам контейнер
|
||||
(доверие reverse-SSH через `authorized_key` с `exclusive: true` — полная
|
||||
замена ключа, не добавление). Шаг 4.3.9 в его буквальном виде
|
||||
(`--limit <name>-new` на существующем плейбуке) для этого сервиса не
|
||||
работает: `hosts:` там — жёсткие имена, не группа, и полный прогон против
|
||||
`mini-pc`/`ru-vps` до cutover преждевременно переключил бы доверие. Кроме
|
||||
того, `emergency_bot.py.j2` использует Telegram `getUpdates` (не webhook) —
|
||||
запуск второй копии с тем же `EMERGENCY_BOT_TOKEN`, что и боевая, ловит
|
||||
409 conflict. Решение для шага 4.3 при следующем сервисе с похожей
|
||||
архитектурой: деплоить только play(-и), таргетящие сам новый контейнер,
|
||||
через одноразовый playbook с `hosts: <name>-new` и заведомо нерабочим,
|
||||
но валидным по формату токеном/credential — так проверяется факт деплоя
|
||||
(юнит стартует, зависимости стоят, egress работает) без конфликта с боевым
|
||||
инстансом. Полный прогон с реальными credentials — уже после cutover,
|
||||
когда исходный playbook с жёстким `hosts: <name>` сам начинает резолвиться
|
||||
в новый контейнер (адрес не менялся, значит `--limit` не нужен вообще).
|
||||
- **`cmode` — провайдер отслеживает его через блок `console`, но по
|
||||
умолчанию блок не объявлен.** Подробности и правильная декларация —
|
||||
`tofu/README.md` и шаг 4.2 выше.
|
||||
|
||||
**2. `docker-test` (145).** Тоже без данных. Проверяет проброс `/dev/fuse`
|
||||
через `features.fuse` на реальном сервисе (не через `device_passthrough` —
|
||||
см. уточнение в шаге 4.2).
|
||||
|
||||
**3. `gitea` (141 -> 153).** Главный приз и самый содержательный шаг.
|
||||
- Было: `mp0: /opt/data/gitea,mp=/opt/gitea/data` — bind mount каталога ноды.
|
||||
**Стало и подтверждено на живом контейнере 2026-09-02:**
|
||||
`mp0: data:153/vm-153-disk-1.raw,mp=/opt/gitea/data,size=32G` — независимый
|
||||
volume. Внутри виден как отдельная ФС `/dev/loop10`, ext4, 32 ГиБ.
|
||||
Ради этого миграция и затевалась.
|
||||
- Данные: `/opt/data/gitea` на cloud-pc (2.3 ГБ) → внутрь нового контейнера.
|
||||
Каталоги `git/` (репозитории), `gitea/` (в т.ч. SQLite `gitea/gitea.db`,
|
||||
~37 МиБ) и `ssh/`. На ноде всё принадлежит `101000:101000` — это host-side
|
||||
отображение uid 1000 внутри unprivileged LXC.
|
||||
Последовательность составлена по итогам разведки, но НЕ исполнялась:
|
||||
|
||||
ssh gitea 'systemctl stop gitea' # OLD, адрес ещё боевой
|
||||
ssh cloud-pc "sudo sqlite3 /opt/data/gitea/gitea/gitea.db \
|
||||
'PRAGMA integrity_check;'" # ожидается ok
|
||||
|
||||
# tar по SSH: rsync не годится, его нет на свежем контейнере,
|
||||
# а tar есть в любом Debian
|
||||
ssh cloud-pc 'sudo tar -C /opt/data/gitea -cf - .' \
|
||||
| ssh gitea-new 'tar -C /opt/gitea/data -xf -'
|
||||
|
||||
# volume создан с нуля, поэтому владельца выставить явно ИЗНУТРИ
|
||||
# контейнера — так результат не зависит от offset idmap
|
||||
ssh gitea-new 'chown -R 1000:1000 /opt/gitea/data'
|
||||
ssh gitea-new "sqlite3 /opt/gitea/data/gitea/gitea.db 'PRAGMA integrity_check;'"
|
||||
ssh gitea-new 'systemctl start gitea'
|
||||
|
||||
Копирование внутри одного узла упирается в скорость диска, не сети: порядка
|
||||
1-3 минут на 2.3 ГБ. Общее окно простоя `git.ada-dev.ru` — 5-10 минут.
|
||||
- Offsite restic-профиль `gitea` выполняется **на cloud-pc**, а не внутри LXC,
|
||||
и указывает на `/opt/data/gitea`. После переезда на volume этот путь исчезнет
|
||||
— профиль в `playbooks/offsite-restic-yadisk.yml` придётся переписать на
|
||||
новый источник. **Не забыть**, иначе offsite-бэкап Gitea тихо перестанет
|
||||
работать.
|
||||
- ~~`roles/gyro` пинит SSH host key gitea — обновить.~~ НЕВЕРНО, снято
|
||||
2026-09-02: там `state: absent` (удаление устаревшей записи), а пинится
|
||||
github.com. Подробности — в шаге 4.5.19.
|
||||
- Публичный домен `git.ada-dev.ru` — после cutover `make reverse-proxy`.
|
||||
|
||||
**4. `vaultwarden` (140 -> 154).** SQLite `/opt/vaultwarden/data/db.sqlite3`,
|
||||
публичный домен `pass.ada-dev.ru`.
|
||||
|
||||
Разведка 2026-09-02: каталог данных 2.9 МБ; `db.sqlite3` с живыми `-wal`/`-shm`,
|
||||
`icon_cache`, `rsa_key.pem`, `config.json`. Каталогов `attachments/` и `sends/`
|
||||
нет, эти возможности не используются. **`config.json` содержит ADMIN_TOKEN и
|
||||
SMTP**, Ansible ими не управляет; они переедут вместе с каталогом. Это
|
||||
ожидаемо, но файл не должен попасть в репозиторий.
|
||||
|
||||
Offsite-профиль restic здесь выполняется ВНУТРИ контейнера
|
||||
(`hosts: vaultwarden`, `offsite_source_path: /opt/vaultwarden/data`), а не на
|
||||
ноде, и адрес при переезде не меняется, поэтому **правки профиля не требуется**
|
||||
в отличие от gitea.
|
||||
|
||||
Оба контейнера на mini-pc, поэтому копирование идёт через `pct exec` без
|
||||
промежуточного файла. Последовательность составлена по итогам разведки, но НЕ
|
||||
исполнялась:
|
||||
|
||||
ssh mini-pc 'sudo pgrep -x vzdump' # ожидается пусто
|
||||
ssh mini-pc 'sudo pct exec 140 -- systemctl stop vaultwarden'
|
||||
|
||||
# .backup сливает WAL в один файл и заодно проверяет источник
|
||||
ssh mini-pc "sudo pct exec 140 -- sqlite3 -cmd 'PRAGMA busy_timeout=30000;' \
|
||||
/opt/vaultwarden/data/db.sqlite3 \".backup '/opt/vaultwarden/data/db.sqlite3.migrate'\""
|
||||
ssh mini-pc 'sudo pct exec 140 -- sqlite3 \
|
||||
/opt/vaultwarden/data/db.sqlite3.migrate "PRAGMA integrity_check;"'
|
||||
|
||||
ssh mini-pc 'sudo sh -c "pct exec 140 -- tar --exclude=./db.sqlite3 \
|
||||
--exclude=./db.sqlite3-wal --exclude=./db.sqlite3-shm \
|
||||
-cf - -C /opt/vaultwarden/data . | pct exec 154 -- tar -xf - -C /opt/vaultwarden/data"'
|
||||
ssh mini-pc 'sudo pct exec 154 -- mv /opt/vaultwarden/data/db.sqlite3.migrate \
|
||||
/opt/vaultwarden/data/db.sqlite3'
|
||||
ssh mini-pc 'sudo pct exec 154 -- sqlite3 /opt/vaultwarden/data/db.sqlite3 \
|
||||
"PRAGMA integrity_check;"'
|
||||
ssh mini-pc 'sudo pct exec 140 -- rm /opt/vaultwarden/data/db.sqlite3.migrate'
|
||||
ssh mini-pc 'sudo pct exec 154 -- systemctl restart vaultwarden'
|
||||
|
||||
Объём крошечный, простой на данных порядка полуминуты: время съедают
|
||||
stop/restart и проверки, а не копирование.
|
||||
|
||||
**5. `monitoring` (146 -> 155).** Замороженный Prometheus-стек в новый
|
||||
контейнер не разворачивать.
|
||||
|
||||
**Данные Uptime Kuma лежат НЕ в `/opt/monitoring`**, как утверждалось раньше.
|
||||
Разведка 2026-09-02: это два независимых каталога верхнего уровня.
|
||||
|
||||
| Путь | Размер | Что это |
|
||||
|---|---|---|
|
||||
| `/opt/uptime-kuma` | 27 МБ | активный сервис, переносить целиком |
|
||||
| `/opt/uptime-kuma/data/kuma.db` | 25.6 МиБ | мониторы и уведомления |
|
||||
| `/opt/monitoring` | 1.7 ГБ | замороженный стек, почти всё — TSDB Prometheus |
|
||||
|
||||
Переносить нужно только `/opt/uptime-kuma`. TSDB замороженного стека переносить
|
||||
незачем: читать его в новом контейнере будет нечему.
|
||||
|
||||
Конфигурация нового контейнера — `playbooks/uptime-kuma.yml`. У
|
||||
`pve-monitoring.yml` конфигурационной части нет вообще, только создание.
|
||||
`playbooks/monitoring.yml` не запускать.
|
||||
|
||||
Последовательность составлена по итогам разведки, но НЕ исполнялась:
|
||||
|
||||
ssh cloud-pc 'sudo pct exec 146 -- sqlite3 /opt/uptime-kuma/data/kuma.db \
|
||||
"PRAGMA wal_checkpoint(TRUNCATE);"'
|
||||
ssh cloud-pc 'sudo pct exec 146 -- systemctl stop uptime-kuma'
|
||||
ssh cloud-pc 'sudo pct exec 146 -- sqlite3 /opt/uptime-kuma/data/kuma.db \
|
||||
"PRAGMA integrity_check;"'
|
||||
ssh cloud-pc 'sudo sh -c "pct exec 146 -- tar -C /opt -cpf - uptime-kuma \
|
||||
| pct exec 155 -- tar -C /opt -xpf -"'
|
||||
ssh cloud-pc 'sudo pct exec 155 -- sqlite3 /opt/uptime-kuma/data/kuma.db \
|
||||
"PRAGMA integrity_check;"'
|
||||
ssh cloud-pc 'sudo pct exec 155 -- systemctl start uptime-kuma'
|
||||
|
||||
**Вторую копию нельзя поднимать на боевых мониторах до остановки первой:**
|
||||
Uptime Kuma активно пробит сервисы и шлёт уведомления, две копии дадут
|
||||
дубликаты алертов.
|
||||
|
||||
Уменьшение контейнера — отдельное решение, не часть переезда. Цифры для него,
|
||||
снятые 2026-09-02: боевой CT 146 занимает 173 МБ RAM из 4096 и 4.7 ГБ из 24;
|
||||
новый CT 155 без Prometheus-стека — 184 МБ RAM и 1.8 ГБ диска.
|
||||
|
||||
В контейнере нет `curl` — для ручных HTTP-проверок использовать `wget`.
|
||||
|
||||
**6. `gyro` (150 -> 156).** CUTOVER COMPLETED 2026-09-02. CT 156 живёт на
|
||||
неизменном production IP `192.168.1.35`, provisioned by Tofu. CT 150
|
||||
остановлен и удерживается как rollback минимум на неделю; его PBS chain не
|
||||
удалялся. Файл фаервола `156.fw` уже копия `150.fw`, cluster firewall по-
|
||||
прежнему disabled. Timer `gyro.timer` active, last oneshot succeeded.
|
||||
Данных для переноса не было, и TMPIP-specific bind/re-run не требовались.
|
||||
|
||||
**7. `grimmory` (149 -> 157).** Самый болезненный, и единственный, где
|
||||
конфигурационный play пришлось чинить.
|
||||
|
||||
**Найденный баг и его исправление (2026-09-02).** В конфигурационном play
|
||||
боевой адрес был зашит в трёх местах, из-за чего play невозможно было
|
||||
прогнать против любого другого контейнера:
|
||||
|
||||
- `ports: "192.168.1.34:6060:6060"` в compose — Docker не биндит чужой адрес и
|
||||
роняет `grimmory.service` ещё до старта приложения;
|
||||
- правила `GRIMMORY-FILTER` (`--ctorigdst 192.168.1.34`) — фильтровали бы не
|
||||
тот адрес;
|
||||
- URL задачи `Wait for Grimmory health endpoint`. **Это было опаснее всего:**
|
||||
задача выполняется на самом целевом хосте, поэтому даже после починки
|
||||
биндинга она уходила бы по сети в БОЕВОЙ grimmory, получала 200 и давала
|
||||
ложно-положительный результат, ничего не проверив в новом контейнере.
|
||||
|
||||
Исправлено через `grimmory_bind_ip: "{{ expected_lan_ip }}"` — ту же идиому,
|
||||
что использует `roles/uptime_kuma`. На боевом хосте переменная равна
|
||||
192.168.1.34, поэтому рендер не изменился: `--check --diff` с `--limit
|
||||
grimmory` даёт `changed=0`, включая обе задачи, которые пишут адрес в файлы.
|
||||
|
||||
**Следствие для шага 4.5, которого в плане не было.** Grimmory — единственный
|
||||
из шести сервисов пакета, кто биндится на конкретный адрес (`ss -ltn` на новых
|
||||
контейнерах: gitea `0.0.0.0:3000`, vaultwarden `0.0.0.0:80`, uptime-kuma
|
||||
`*:3001`, grimmory `192.168.1.16:6060`). После смены адреса на боевой compose
|
||||
всё ещё будет содержать TMPIP, и сервис не поднимется. Плейбук ОБЯЗАН быть
|
||||
прогнан заново сразу после cutover — см. шаг 4.5.16.
|
||||
|
||||
**Состояние после конфигурации на TMPIP (проверено):** оба юнита active,
|
||||
оба контейнера healthy, `health=200` на 192.168.1.16, прогон идемпотентен
|
||||
(`changed=0`).
|
||||
|
||||
**Образы совпали с боевым по digest** — `grimmory/grimmory:v3.2.4@sha256:dfa7afdf…`
|
||||
и `lscr.io/linuxserver/mariadb:11.4.8@sha256:91de7f70…`. Это требование
|
||||
`legacy-warning.md`: code-only downgrade после Flyway-миграции запрещён.
|
||||
|
||||
**Flyway: схемы сошлись.** На боевом и на новом контейнере одинаково —
|
||||
`MAX(version)=144`, 142 миграции, все успешны. То есть тот же образ на пустой
|
||||
базе приходит ровно к боевой версии схемы, и restore дампа новых миграций не
|
||||
вызовет. Холодный старт с нуля занимает около 5 минут — health-check ретраится,
|
||||
это нормально.
|
||||
|
||||
**Перенос данных.** `/opt/grimmory` — 311 МБ: `books/` 139 МБ (библиотека),
|
||||
`data/` 5.6 МБ (обложки), `mariadb/` 167 МБ. Файлы MariaDB копировать НЕЛЬЗЯ,
|
||||
нужен dump/restore. Рабочий рецепт дампа уже есть в
|
||||
`tasks/offsite-restic-profile.yml` (`mariadb-dump --single-transaction
|
||||
--routines --events`). Последовательность составлена по итогам разведки, но НЕ
|
||||
исполнялась:
|
||||
|
||||
# приложение стоп, MariaDB оставить живой
|
||||
ssh grimmory 'cd /opt/grimmory && docker compose stop grimmory'
|
||||
ssh grimmory 'set -a; . /opt/grimmory/.env; set +a; \
|
||||
docker exec -e MYSQL_PWD="$DB_PASSWORD" grimmory-mariadb mariadb-dump \
|
||||
--user=grimmory --single-transaction --routines --events \
|
||||
--databases grimmory > /opt/grimmory/backup-staging/grimmory.sql'
|
||||
|
||||
# библиотека и обложки
|
||||
ssh cloud-pc 'sudo sh -c "pct exec 149 -- tar -C /opt/grimmory -cpf - books data \
|
||||
| pct exec 157 -- tar -C /opt/grimmory -xpf -"'
|
||||
|
||||
# дамп на новый и restore
|
||||
ssh cloud-pc 'sudo sh -c "pct exec 149 -- cat /opt/grimmory/backup-staging/grimmory.sql \
|
||||
| pct exec 157 -- tee /opt/grimmory/backup-staging/grimmory.sql >/dev/null"'
|
||||
ssh grimmory-new 'cd /opt/grimmory && docker compose stop grimmory'
|
||||
ssh grimmory-new 'set -a; . /opt/grimmory/.env; set +a; \
|
||||
docker exec -i -e MYSQL_PWD="$DB_PASSWORD" grimmory-mariadb mariadb \
|
||||
--user=grimmory grimmory < /opt/grimmory/backup-staging/grimmory.sql'
|
||||
ssh grimmory-new 'systemctl restart grimmory'
|
||||
|
||||
**Дамп не содержит `--add-drop-database`.** На новом контейнере схема уже
|
||||
создана Flyway с нуля, поэтому перед restore её нужно либо очистить, либо
|
||||
добавить эту опцию в дамп. Отдельно стоит знать, что restore-путь в этом
|
||||
репозитории НИКОГДА не проверялся: `edge-cases.md` отмечает, что аудит
|
||||
проверяет непустоту дампа, но не импортирует его.
|
||||
|
||||
**Эталон для сверки после restore** (снят с боевого 2026-09-02):
|
||||
`book 31`, `author 30`, `book_file 40`, `book_metadata 31`, `category 87`,
|
||||
`reading_sessions 68`, `shelf 3`, `library 1`, `users 1`, `opds_user_v2 2`,
|
||||
`koreader_user 1`, `flyway_schema_history 142`.
|
||||
|
||||
**После cutover — проверка OPDS на живой читалке, а не только curl'ом.**
|
||||
Быстрая проверка заголовков:
|
||||
|
||||
curl -sS -D- -o /dev/null https://books.ada-dev.ru/api/v1/opds # atom+xml, без Content-Encoding
|
||||
curl -sS -D- -o /dev/null https://books.ada-dev.ru/api/v1/opds/search.opds
|
||||
curl -sS https://books.ada-dev.ru/api/v1/healthcheck
|
||||
|
||||
Затем в KOReader: добавить каталог, увидеть список полок, скачать книгу
|
||||
целиком, проверить синхронизацию прогресса.
|
||||
|
||||
**8. `adguard` (144 → 158).** ПЕРЕЕХАЛ 2026-09-03. OLD 144 остановлен (откат
|
||||
≥ неделя, до ~2026-09-10). Данные `/opt/adguard/{conf,work}` (~313 МБ,
|
||||
`conf/AdGuardHome.yaml` несёт хэш пароля, DNS rewrites, upstream, клиентов)
|
||||
перенесены целиком через `pct exec … tar` (оба контейнера на mini-pc).
|
||||
|
||||
- **Свежий AdGuard на пустом `conf/` не проходит health-гейт плейбука.** Он
|
||||
уходит в setup-wizard: порт 3000 отдаёт 302, порт 80 — connection reset, а
|
||||
`pve-adguard.yml` ждёт `http://127.0.0.1/` `[200,302]` и падает после 24
|
||||
ретраев. Поэтому для adguard данные пред-заливаются ДО шага 4.3 (config play),
|
||||
а не после. После пред-заливки прогон идемпотентен (`ok=13 changed=0`),
|
||||
фильтрация подтверждена: `doubleclick.net → 0.0.0.0`.
|
||||
- Ноды используют DNS роутера (192.168.1.1), контейнеры — 1.1.1.1 (из
|
||||
`pct config` `nameserver`). Провал .28 задел только DHCP-клиентов LAN и
|
||||
рабочую станцию (у неё fallback на роутер). Окно ~2 мин, ночью. Роутер не
|
||||
трогали.
|
||||
- Сокеты wildcard (`0.0.0.0`), повторный прогон после смены адреса не нужен.
|
||||
|
||||
**9. `mihomo` (143 → 159).** ПЕРЕЕХАЛ 2026-09-03. OLD 143 остановлен (откат
|
||||
≥ неделя). Первый боевой сервис с **двумя механизмами проброса устройств**:
|
||||
`/dev/fuse` через `features.fuse`, `/dev/net/tun` через блок
|
||||
`device_passthrough` (`dev0: path=/dev/net/tun,mode=0660`) — оба под root@pam,
|
||||
проверены здесь на живом сервисе (до этого `device_passthrough` был только на
|
||||
пилоте).
|
||||
|
||||
- Данные `/opt/mihomo` (~80 КБ): `config/config.yaml`, `config/cache.db`,
|
||||
`config/providers/main.yaml`. `config.yaml` содержит URL подписки прокси-
|
||||
провайдера — секрет, копируется `pct exec`, в репозиторий не попадает.
|
||||
Задача «Install default mihomo config if missing» идёт с `force: false`,
|
||||
перенесённый конфиг не перетирается.
|
||||
- Все сокеты wildcard (`0.0.0.0`) — повторный прогон после смены адреса не
|
||||
нужен. Прокси реально проверен: `curl -x .27:7890 …/generate_204 → 204`.
|
||||
- `ru-vps-mihomo-harden.yml` повторного прогона НЕ требует: он таргетит
|
||||
`hosts: ru-vps`, работает со скриптом `/usr/local/sbin/ru-vps-mihomo-harden`
|
||||
на ru-vps, адрес mihomo нигде в нём не зашит, и он за `CONFIRM`-гейтом.
|
||||
- Зависимые (`bash_config_proxy_*` у hermes-ai, `emergency_telegram_proxy`,
|
||||
`uptime_kuma_http_proxy`, правило gyro-фаервола `OUT ACCEPT 192.168.1.27:7890`,
|
||||
`prometheus.yml.j2`) все ссылаются на .27 — адрес сохранён, правок не нужно.
|
||||
|
||||
**10. `ovpn-mini` (132 → 160).** ПЕРЕЕХАЛ 2026-09-03 из локальной сети. OLD 132
|
||||
остановлен (откат ≥ неделя). `/dev/net/tun` через `device_passthrough`.
|
||||
`features` — только `nesting` (как в эталоне). Конфигурации в `pve-ovpn-mini.yml`
|
||||
нет — шлюз настраивает `openvpn-vps-mini.yml` (роль `openvpn_gateway`, группа
|
||||
`vpn_openvpn`). Единственные данные — `/etc/openvpn/homelab/static.key`, общий
|
||||
с ru-vps; LAN-адрес ovpn-mini ни в одном шаблоне роли не фигурирует
|
||||
(masquerade по `-o eth0`), поэтому туннель не зависит от смены адреса.
|
||||
|
||||
- **Петля транспорта при cutover.** `-F ansible/ssh_config` до нод PVE идёт
|
||||
ProxyJump через ru-vps, а ru-vps достаёт LAN ЧЕРЕЗ туннель, который
|
||||
терминирует ovpn-mini. `make tofu-*` строит SSH-туннель к PVE API тем же
|
||||
путём. `pct stop 132` кладёт туннель → `make tofu-apply` больше не достаёт
|
||||
API. Разрыв: сначала поднять шлюз на НОВОМ контейнере ещё на TMPIP
|
||||
(одноразовый плейбук `hosts: ru-vps` slurp ключа + `hosts: <name>-new` роль
|
||||
`openvpn_gateway`), туннель встаёт с TMPIP-адреса → `make tofu-apply` снова
|
||||
работает → сменить адрес на боевой → повторный `openvpn-vps-mini.yml`
|
||||
(идемпотентен, `changed=0` на обоих концах). Прямой доступ к нодам во время
|
||||
провала — `ssh -o ProxyJump=none <ansible-user>@192.168.1.{5,10}` из LAN.
|
||||
- **`ssh_config`: у `ovpn-mini` теперь `ProxyJump none`** (в индивидуальном
|
||||
Host-блоке, побеждает по «первое значение опции»). Путь через ru-vps
|
||||
закольцовывался бы на его же туннель. Из LAN хост доступен напрямую; вне
|
||||
LAN управление — консоль ноды (`pct exec`) или заранее поднятый туннель.
|
||||
- **НЕ запускать OLD 132 как диагностику.** Его `pct config` всё ещё держит
|
||||
`ip=192.168.1.23/24`; параллельный старт с CT 160 даёт конфликт .23 и роняет
|
||||
туннель на 1-2 мин (проверено случайно 2026-09-03, восстановилось само).
|
||||
- `make openvpn-check` — 7/7 ok. Кворум кластера не затронут (qdevice ходит
|
||||
напрямую нода → ru-vps:5403, не через туннель).
|
||||
- Косметика: `polkit.service` на CT 160 в `failed` (`status=217/USER` —
|
||||
минимальный LXC-шаблон без нужного окружения polkit). На OpenVPN не влияет,
|
||||
не чинилось.
|
||||
|
||||
---
|
||||
|
||||
## 6. После завершения всех переездов
|
||||
|
||||
- Удалить `roles/pve_lxc` — он станет не нужен.
|
||||
- Убрать из `services.yml` поле `provisioner` со значениями `pct_ssh`/`pve_lxc`
|
||||
либо заменить на `tofu`.
|
||||
- Обновить `docs/ai/architecture.md`: раздел «Provisioning Flow» описывает два
|
||||
пути через `pct` и API — оба исчезнут.
|
||||
- Обновить `legacy-warning.md`: пункт про два provisioner-пути потеряет смысл.
|
||||
- Расширить `validate.yml`: сейчас он сверяет hostname, IP, cores, memory, swap.
|
||||
После миграции имеет смысл добавить features, устройства и mount points —
|
||||
ровно те поля, которыми теперь управляет Tofu.
|
||||
- Решить, нужен ли `roles/lxc_docker_host` — он до сих пор ни к чему не
|
||||
подключён.
|
||||
|
||||
---
|
||||
|
||||
## 7. Известные проблемы вне миграции
|
||||
|
||||
Не блокируют переезд, но про них надо знать.
|
||||
|
||||
- **`hermes-ai` (147) недостижим по SSH.** Прозрачный прокси заворачивает в
|
||||
`hermes-tun` всё, что не пришло с `lo`, включая ответные пакеты входящих
|
||||
соединений (`ip rule` 9002). Ansible до хоста не достучится, управление —
|
||||
только через `pct exec`. Сервис заморожен, `make check` показывает его DOWN
|
||||
ожидаемо. Мигрировать не раньше, чем починится сеть.
|
||||
- **`resticprofile-check@profile-default` на ru-vps падает еженедельно**:
|
||||
профиль `default` без репозитория. Реальный `profile-services` работает.
|
||||
Шум в секции FAILED отчёта `make status`.
|
||||
- **Gitea runner** зарегистрирован и опрашивает Gitea, но метка `ru-vps` не
|
||||
совпадает с `runs-on: ubuntu-latest` в workflow, поэтому CI не выполняется.
|
||||
Раннер монтирует `/var/run/docker.sock` и `/opt/services` на запись — перед
|
||||
включением CI это стоит пересмотреть.
|
||||
- **`homelab_pve_egress_ip` динамический.** При смене домашнего адреса qdevice
|
||||
замолчит. Видно в секции CLUSTER QUORUM отчёта `make status`.
|
||||
- **`bootstrap-pve-api-token.yml` перезаписывает корневой `.env` целиком** —
|
||||
вместе с `PROXMOX_ROOT_PASSWORD`, `MONITORING_*` и `EMERGENCY_*`. У задачи
|
||||
есть `backup: true`, но восстанавливать придётся руками.
|
||||
- Устаревшие правила UFW на ru-vps (`3128`, `1080`, `993`, `7892`) — за
|
||||
переключателем `zt_cleanup_unrelated_stale_rules` в
|
||||
`playbooks/ru-vps-zerotier-decommission.yml`, по умолчанию выключен.
|
||||
|
||||
---
|
||||
|
||||
## 8. Откат
|
||||
|
||||
**До шага 4.5.14** (пока старый контейнер работает): просто не переключаться.
|
||||
Удалить новый контейнер `make tofu-destroy CONFIRM=1` или точечно.
|
||||
|
||||
**После переключения, но до `pct destroy`:** остановить новый, вернуть адрес
|
||||
старому не нужно — он не менялся, `pct start OLD` возвращает всё как было.
|
||||
Затем откатить `vmid` в `services.yml` и прогнать `make backup-jobs`,
|
||||
`make backup-audit`, `make validate`.
|
||||
|
||||
**После `pct destroy OLD`:** только восстановление из PBS. Именно поэтому
|
||||
шаг 4.1.3 (свежий бэкап) обязателен, а шаг 4.7.25 (неделя ожидания) не
|
||||
сокращается.
|
||||
+485
@@ -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.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Технологический стек
|
||||
|
||||
## Control Plane
|
||||
|
||||
| Компонент | Роль | Declared / resolved evidence |
|
||||
|---|---|---|
|
||||
| Nix | Воспроизводимый dev shell | `flake.nix`, locked nixpkgs revision в `flake.lock` |
|
||||
| Python | Ansible controller runtime | `pkgs.python3` в `flake.nix`; точная версия зависит от lock |
|
||||
| ansible-core | Inventory, playbooks, roles | `>=2.19` в `requirements.txt`; locked Nix package на момент аудита 2.21.3 |
|
||||
| proxmoxer | Proxmox API client | `>=2.3`; locked Nix package на момент аудита 2.3.0 |
|
||||
| requests | HTTP dependency | `>=2.31`; точная Nix version определяется `flake.lock` |
|
||||
| ansible.posix | POSIX modules | `>=2.0.0`; installed version не закреплена |
|
||||
| community.proxmox | Proxmox modules | `>=2.0.0`; installed version не закреплена |
|
||||
| community.general | Общие modules | `>=10.0.0`; installed version не закреплена |
|
||||
| ansible-lint | Static validation | Nix package; CI отдельно pin `25.8.2` |
|
||||
| yamllint | YAML validation | Nix package; CI отдельно pin `1.37.1` |
|
||||
|
||||
Nix shell также содержит Git, jq, OpenSSH, curl и GNU Make. Galaxy collections не
|
||||
устанавливаются автоматически и живут в ignored `ansible/collections/`.
|
||||
|
||||
## Managed Runtime
|
||||
|
||||
- Proxmox VE/LXC и PBS являются внешними runtime systems; их версии не заданы manifest.
|
||||
- Debian LXC templates и Docker/systemd используются service playbooks.
|
||||
- Caddy, OpenVPN и restic устанавливаются/используются на managed hosts; точные
|
||||
runtime versions репозиторий не фиксирует.
|
||||
- Active update-managed container images обычно закреплены `tag@sha256:digest` в
|
||||
`ansible/inventory/group_vars/all/services.yml` и service playbooks.
|
||||
- Исключения: frozen Prometheus images закреплены только тегами, а `docker-test`
|
||||
использует smoke image без declared digest.
|
||||
|
||||
## Grimmory MCP
|
||||
|
||||
| Компонент | Version | Evidence |
|
||||
|---|---|---|
|
||||
| Node.js | `>=22` | `tools/grimmory-mcp/package.json` |
|
||||
| `@modelcontextprotocol/sdk` | `1.30.0` | package manifest и lockfile |
|
||||
| Zod | `3.25.76` | package manifest и lockfile |
|
||||
| Grimmory API compatibility | deployed `v3.2.4` | service registry и compatibility code |
|
||||
|
||||
Node.js/npm не входят в `flake.nix`; их нужно предоставлять отдельно.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
nix develop
|
||||
ansible-galaxy collection install \
|
||||
-r ansible/requirements.yml \
|
||||
-p ansible/collections
|
||||
make -C ansible help
|
||||
```
|
||||
|
||||
`make -C ansible setup` остается venv fallback, но Nix является предпочтительным
|
||||
контроллерным окружением.
|
||||
|
||||
## Validation
|
||||
|
||||
```bash
|
||||
make -C ansible inventory
|
||||
make -C ansible docs
|
||||
make -C ansible lint
|
||||
nix develop -c sh -c \
|
||||
'cd ansible && for f in playbooks/*.yml; do ansible-playbook --syntax-check "$f"; done'
|
||||
npm test --prefix tools/grimmory-mcp
|
||||
```
|
||||
|
||||
`make check`, `make status`, `make backup-audit` и `make openvpn-check` обращаются к
|
||||
живой инфраструктуре и не являются локальными unit tests. Документационное изменение
|
||||
не требует их запуска.
|
||||
|
||||
## CI
|
||||
|
||||
`.gitea/workflows/lint.yml` описывает yamllint, ansible-lint и syntax-check всех
|
||||
playbooks. Комментарии в самом workflow фиксируют, что Gitea Actions отключены и
|
||||
runner не зарегистрирован; автоматического gate сейчас нет. MCP tests в workflow
|
||||
не включены.
|
||||
Reference in New Issue
Block a user