diff --git a/ansible/inventory/group_vars/all/services.yml b/ansible/inventory/group_vars/all/services.yml index 3e62ab9..7b8d11d 100644 --- a/ansible/inventory/group_vars/all/services.yml +++ b/ansible/inventory/group_vars/all/services.yml @@ -13,9 +13,12 @@ # ansible/roles/*, ansible/inventory/hosts.yml). Ничего не выдумано. # Там, где факта в коде нет, стоит комментарий "# нет в коде", а не догадка. # -# КТО ЭТО ПОТРЕБЛЯЕТ (на текущем этапе) -# * ansible/playbooks/reverse-proxy.yml — итерируется по сервисам, -# у которых задан блок `proxy`, и собирает Caddyfile на ru-vps. +# КТО ЭТО ПОТРЕБЛЯЕТ +# * playbooks/reverse-proxy.yml — Caddyfile по блокам `proxy`. +# * playbooks/ru-vps-base.yml — стек Caddy и закреплённый образ. +# * playbooks/pve-backup-jobs.yml — списки VMID заданий по полю `backup.job`. +# * roles/backup_audit — VMID по флагу `monitoring.backup_audit_vmid`. +# * playbooks/validate.yml — сверка с фактическим состоянием Proxmox. # * Человек — как справочник вместо чтения семи playbook'ов по 200-400 строк. # # Существующие pve-*.yml пока НЕ читают этот реестр: их перевод на @@ -57,9 +60,18 @@ homelab_reverse_proxy_host: ru-vps homelab_reverse_proxy_dir: /opt/services/ru-vps/caddy homelab_reverse_proxy_caddyfile: /opt/services/ru-vps/caddy/Caddyfile -# ВНИМАНИЕ: сам контейнер Caddy на ru-vps ansible'ом НЕ управляется — -# в репозитории нет плейбука его установки. Управляется только Caddyfile. homelab_reverse_proxy_container: caddy +# Стек Caddy разворачивает playbooks/ru-vps-base.yml (роль compose_service). +# Содержимое Caddyfile принадлежит playbooks/reverse-proxy.yml — ru-vps-base +# его не трогает, только проверяет наличие. +# Образ закреплён по digest: до принятия в Ansible на ru-vps крутился +# floating-тег caddy:2-alpine (на момент фиксации — v2.11.4). +homelab_reverse_proxy_image: >- + caddy:2-alpine@sha256:5f5c8640aae01df9654968d946d8f1a56c497f1dd5c5cda4cf95ab7c14d58648 +# Systemd-юнит намеренно называется homelab-caddy, а не caddy: на ru-vps уже +# есть отключённый caddy.service от APT-пакета, и одноимённый unit в +# /etc/systemd/system его бы затенил. +homelab_reverse_proxy_unit: homelab-caddy # Адрес центрального хоста мониторинга (источник scrape для node-exporter). homelab_monitoring_host_ip: 192.168.1.30 @@ -99,20 +111,22 @@ homelab_services: # -------------------------------------------------------------------------- ovpn-mini: - vmid: 132 + vmid: 160 node: mini-pc ip: 192.168.1.23 hostname: ovpn-mini role: OpenVPN-шлюз в LAN (клиент ru-vps) - provisioner: pve_lxc # playbooks/pve-ovpn-mini.yml + provisioner: tofu # tofu/svc-ovpn-mini.tf, переезд 2026-09-03 (migration-tofu.md) lxc: - cores: 1 # из roles/pve_lxc/defaults - memory: 256 # из roles/pve_lxc/defaults - swap: 128 # из roles/pve_lxc/defaults + cores: 1 + memory: 256 + swap: 128 disk: local-lvm:8 - startup: order=30 # из roles/pve_lxc/defaults - features: "nesting=1" + startup: order=30 + features: "nesting=1" # keyctl/fuse намеренно не заданы unprivileged: true + # /dev/net/tun — блок device_passthrough в Tofu (dev0: path=/dev/net/tun) + # вместо прежних сырых lxc.* строк из pve-ovpn-mini.yml. Эффект тот же. devices: ["/dev/net/tun"] ports: - {name: openvpn, port: 8443, proto: tcp, note: "туннель к ru-vps 10.78.0.1"} @@ -129,19 +143,20 @@ homelab_services: # -------------------------------------------------------------------------- vaultwarden: - vmid: 140 + vmid: 154 node: mini-pc ip: 192.168.1.24 hostname: vaultwarden role: Менеджер паролей Vaultwarden - provisioner: pct_ssh # playbooks/pve-vaultwarden.yml + provisioner: tofu # tofu/svc-vaultwarden.tf, переезд 2026-09-02 (migration-tofu.md) lxc: cores: 2 memory: 1024 swap: 512 disk: local-lvm:16 startup: order=40 - features: "nesting=1,keyctl=1" + # fuse=1 — штатный флаг PVE вместо прежнего обхода сырыми lxc.* строками. + features: "fuse=1,keyctl=1,nesting=1" unprivileged: true ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst devices: ["/dev/fuse"] @@ -182,25 +197,30 @@ homelab_services: # -------------------------------------------------------------------------- gitea: - vmid: 141 + vmid: 153 node: cloud-pc ip: 192.168.1.25 hostname: gitea role: Git-хостинг Gitea - provisioner: pct_ssh # playbooks/pve-gitea.yml + provisioner: tofu # tofu/svc-gitea.tf, переезд 2026-09-02 (migration-tofu.md) lxc: cores: 2 memory: 2048 swap: 1024 disk: data:32 startup: order=50 - features: "nesting=1,keyctl=1" + # fuse=1 — штатный флаг PVE вместо прежнего обхода сырыми lxc.* строками. + features: "fuse=1,keyctl=1,nesting=1" unprivileged: true ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst devices: ["/dev/fuse"] mounts: - # mp0 на cloud-pc; каталог на хосте принадлежит uid/gid 101000 - - {host_path: /opt/data/gitea, container_path: /opt/gitea/data, host_uid: 101000, host_gid: 101000} + # Был bind mount каталога ноды /opt/data/gitea. С переезда 2026-09-02 — + # независимый volume на datastore: mp0 data:153/vm-153-disk-1.raw. + # Побочный выигрыш: bind mount vzdump ИСКЛЮЧАЛ из бэкапа ("not a + # volume"), то есть данные gitea в PBS никогда не попадали. Volume + # попадает. + - {volume: data, size: 32G, container_path: /opt/gitea/data} data_dir: /opt/gitea/data ports: - {name: http, port: 3000, proto: tcp} @@ -215,12 +235,13 @@ homelab_services: storage: pbs restic: profile: gitea - # restic-профиль выполняется на cloud-pc, а не внутри LXC - run_on: cloud-pc + # С переезда 2026-09-02 профиль выполняется ВНУТРИ LXC, как у + # vaultwarden: пути ноды больше не существуют, данные в volume. + run_on: gitea repository: rclone:yadisk:System/Backups/HomeLab/restic/gitea - source_path: /opt/data/gitea + source_path: /opt/gitea/data schedule: "*-*-* 04:15:00" - sqlite_db: /opt/data/gitea/gitea/gitea.db + sqlite_db: /opt/gitea/data/gitea/gitea.db monitoring: node_exporter: true blackbox: true @@ -238,54 +259,24 @@ homelab_services: status_code: [200] follow_redirects: none # соответствует `curl -fsS` без -L в старом плейбуке - # -------------------------------------------------------------------------- - memoir-bot: - vmid: 142 - node: mini-pc - ip: 192.168.1.26 - hostname: memoir-bot - role: Telegram-бот дневника в Obsidian-хранилище - provisioner: pct_ssh # playbooks/pve-memoir-bot.yml - lxc: - cores: 1 - memory: 512 - swap: 512 - disk: local-lvm:16 - startup: order=60 - features: "nesting=1,keyctl=1" - unprivileged: true - ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst - devices: ["/dev/fuse"] - data_dir: /srv/memoir-bot - ports: [] # портов не публикует, только исходящие подключения к Telegram - images: - - memoir-bot:local # собирается на месте `docker build`, digest отсутствует - runtime: docker-run-systemd # /etc/systemd/system/memoir-bot.service - backup: - kind: pbs - job: homelab-pbs-daily-mini - schedule: "02:40" - storage: pbs - monitoring: - node_exporter: true - blackbox: false - backup_audit_vmid: true - # -------------------------------------------------------------------------- mihomo: - vmid: 143 + vmid: 159 node: mini-pc ip: 192.168.1.27 hostname: mihomo role: Локальный прокси Mihomo + веб-интерфейс MetaCubeXD - provisioner: pct_ssh # playbooks/pve-mihomo.yml + provisioner: tofu # tofu/svc-mihomo.tf, переезд 2026-09-03 (migration-tofu.md) lxc: cores: 1 memory: 512 swap: 512 disk: local-lvm:8 startup: order=70 - features: "nesting=1,keyctl=1" + # fuse=1 — штатный флаг PVE вместо прежнего обхода сырыми lxc.* строками. + # /dev/net/tun — блок device_passthrough в Tofu (dev0: path=/dev/net/tun), + # тоже вместо сырых lxc.* строк. Эффект тот же, запись в pct config иная. + features: "fuse=1,keyctl=1,nesting=1" unprivileged: true ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst devices: ["/dev/fuse", "/dev/net/tun"] @@ -311,19 +302,21 @@ homelab_services: # -------------------------------------------------------------------------- adguard: - vmid: 144 + vmid: 158 node: mini-pc ip: 192.168.1.28 hostname: adguard role: AdGuard Home — DNS с фильтрацией - provisioner: pct_ssh # playbooks/pve-adguard.yml + provisioner: tofu # tofu/svc-adguard.tf, переезд 2026-09-03 (migration-tofu.md) lxc: cores: 1 memory: 512 swap: 512 disk: local-lvm:8 startup: order=40 - features: "nesting=1,keyctl=1" + # fuse=1 — штатный флаг PVE вместо прежнего обхода сырыми lxc.* строками + # (lineinfile по /etc/pve/lxc/144.conf в pve-adguard.yml). Эффект тот же. + features: "fuse=1,keyctl=1,nesting=1" unprivileged: true ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst devices: ["/dev/fuse"] @@ -348,19 +341,22 @@ homelab_services: # -------------------------------------------------------------------------- docker-test: - vmid: 145 + vmid: 152 node: cloud-pc ip: 192.168.1.29 hostname: docker-test role: Песочница для проверки Docker в непривилегированном LXC - provisioner: pct_ssh # playbooks/pve-docker-test.yml + provisioner: tofu # tofu/svc-docker-test.tf, переезд 2026-09-02 (migration-tofu.md) lxc: cores: 1 memory: 512 swap: 512 disk: data:8 startup: order=50 - features: "nesting=1,keyctl=1" + # fuse=1 появился после переезда на Tofu: раньше /dev/fuse пробрасывался + # сырыми lxc.* строками через lineinfile, теперь это штатный флаг PVE. + # Эффект тот же, запись в pct config другая. + features: "fuse=1,keyctl=1,nesting=1" unprivileged: true ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst devices: ["/dev/fuse"] @@ -378,23 +374,33 @@ homelab_services: # -------------------------------------------------------------------------- monitoring: - vmid: 146 + vmid: 155 node: cloud-pc ip: 192.168.1.30 hostname: monitoring - role: Prometheus + Alertmanager + Grafana + blackbox + pve-exporter + Uptime Kuma - provisioner: pve_lxc # playbooks/pve-monitoring.yml + # До переезда 2026-09-02 контейнер нёс ещё и замороженный стек + # Prometheus/Alertmanager/Grafana/blackbox/pve-exporter. В новый контейнер + # он сознательно не разворачивался (migration-tofu.md, п.5.5), поэтому + # фактически здесь работает только Uptime Kuma. Возврат стека — отдельное + # architecture decision, см. legacy-warning.md. + role: Uptime Kuma (замороженный стек Prometheus больше не развёрнут) + provisioner: tofu # tofu/svc-monitoring.tf, переезд 2026-09-02 (migration-tofu.md) lxc: cores: 2 memory: 4096 swap: 512 disk: data:24 + # Ресурсы оставлены как были, под замороженный стек. Фактическое + # потребление после переезда — 184 МБ RAM и 1.8 ГБ диска. Уменьшение — + # отдельное решение, не часть переезда. startup: order=80 - # создаётся с nesting=1, keyctl=1 добавляется отдельной задачей `pct set` - features: "nesting=1,keyctl=1" + features: "keyctl=1,nesting=1" unprivileged: true ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst - data_dir: /opt/monitoring + # Данные Uptime Kuma лежат в /opt/uptime-kuma, а НЕ в /opt/monitoring: + # последний принадлежал замороженному стеку и на новом контейнере + # отсутствует. + data_dir: /opt/uptime-kuma ports: - {name: grafana, port: 3000, proto: tcp, bind: 192.168.1.30} - {name: pushgateway, port: 9091, proto: tcp, bind: 192.168.1.30} @@ -454,12 +460,12 @@ homelab_services: # -------------------------------------------------------------------------- emergency-bot: - vmid: 148 + vmid: 151 node: mini-pc ip: 192.168.1.32 hostname: emergency-bot role: Telegram-бот аварийного доступа - provisioner: pve_lxc # playbooks/pve-emergency-bot.yml + provisioner: tofu # tofu/services.tf, переезд 2026-09-02 (migration-tofu.md) lxc: cores: 1 memory: 512 @@ -480,20 +486,20 @@ homelab_services: # -------------------------------------------------------------------------- grimmory: - vmid: 149 + vmid: 157 node: cloud-pc ip: 192.168.1.34 hostname: grimmory role: Библиотека книг Grimmory (+ MariaDB), OPDS/KOReader - provisioner: pve_lxc # playbooks/pve-grimmory.yml + provisioner: tofu # tofu/svc-grimmory.tf, переезд 2026-09-02 (migration-tofu.md) lxc: cores: 2 memory: 4096 swap: 1024 disk: data:64 startup: order=100 - # создаётся с nesting=1, keyctl=1 добавляется отдельной задачей `pct set` - features: "nesting=1,keyctl=1" + # fuse=1 — штатный флаг PVE вместо прежнего обхода сырыми lxc.* строками. + features: "fuse=1,keyctl=1,nesting=1" unprivileged: true ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst devices: ["/dev/fuse"] @@ -576,12 +582,12 @@ homelab_services: # -------------------------------------------------------------------------- gyro: - vmid: 150 + vmid: 156 node: mini-pc ip: 192.168.1.35 hostname: gyro role: Изолированный контейнер под задачу gyro (доступ в сеть только через mihomo) - provisioner: pve_lxc # playbooks/pve-gyro.yml + provisioner: tofu # tofu/svc-gyro.tf lxc: cores: 1 memory: 512 @@ -591,7 +597,7 @@ homelab_services: features: "" # pve_lxc_features: [] — nesting отключён намеренно unprivileged: true ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst - firewall: proxmox # /etc/pve/firewall/150.fw, policy_in DROP + firewall: proxmox # /etc/pve/firewall/156.fw, policy_in DROP ports: [] images: [] # роль gyro образы не закрепляет backup: diff --git a/ansible/playbooks/gyro.yml b/ansible/playbooks/gyro.yml index c1d3c83..971e6e0 100644 --- a/ansible/playbooks/gyro.yml +++ b/ansible/playbooks/gyro.yml @@ -1,6 +1,15 @@ --- - name: Configure Gyro investment allocator host - hosts: gyro + # Таргет переопределяем ради blue-green переезда на OpenTofu: во время + # миграции этот play нужно прогнать против нового контейнера на временном + # адресе. Просто `--limit gyro-new` для этого НЕ годится — лимит + # пересекается с паттерном play и даёт ноль хостов, а не перенацеливание + # (проверено 2026-09-02 через --list-hosts на всех плейбуках). + # Использовать вместе с --limit, чтобы play создания контейнера отсеялся: + # ansible-playbook playbooks/gyro.yml \ + # -e pve_config_target=gyro-new --limit gyro-new + # По умолчанию поведение не меняется. + hosts: "{{ pve_config_target | default('gyro') }}" gather_facts: true roles: - role: gyro diff --git a/ansible/playbooks/offsite-restic-yadisk.yml b/ansible/playbooks/offsite-restic-yadisk.yml index 366fc4c..13f7c49 100644 --- a/ansible/playbooks/offsite-restic-yadisk.yml +++ b/ansible/playbooks/offsite-restic-yadisk.yml @@ -1,23 +1,36 @@ +# Профиль выполняется ВНУТРИ контейнера gitea, а не на ноде cloud-pc. +# До переезда на OpenTofu (2026-09-02) данные жили в bind mount каталога ноды +# /opt/data/gitea, и профиль работал на самой ноде. После переезда данные — +# это volume контейнера (mp0 на datastore), снаружи LXC такого пути больше нет, +# поэтому профиль переехал внутрь по образцу vaultwarden. Репозиторий restic +# тот же, цепочка снапшотов продолжается; изменились только пути внутри них. - name: Configure Gitea offsite backup to Yandex Disk - hosts: cloud-pc + hosts: gitea gather_facts: false vars: + ansible_become: false offsite_profile: gitea offsite_repository: rclone:yadisk:System/Backups/HomeLab/restic/gitea - offsite_source_path: /opt/data/gitea - offsite_sqlite_db: /opt/data/gitea/gitea/gitea.db + offsite_source_path: /opt/gitea/data + offsite_sqlite_db: /opt/gitea/data/gitea/gitea.db offsite_backup_tag: gitea,cloud-pc,yadisk offsite_timer_oncalendar: "*-*-* 04:15:00" offsite_rclone_config_local: ~/.config/rclone/rclone.conf offsite_restic_password_local: "{{ playbook_dir }}/../generated/restic-offsite-password" offsite_excludes: - - /opt/data/gitea/gitea/gitea.db - - /opt/data/gitea/gitea/gitea.db-shm - - /opt/data/gitea/gitea/gitea.db-wal - - /opt/data/gitea/gitea/log/** - - /opt/data/gitea/gitea/sessions/** - - /opt/data/gitea/gitea/queues/** - - /opt/data/gitea/gitea/tmp/** + - /opt/gitea/data/gitea/gitea.db + - /opt/gitea/data/gitea/gitea.db-shm + - /opt/gitea/data/gitea/gitea.db-wal + - /opt/gitea/data/gitea/log/** + - /opt/gitea/data/gitea/sessions/** + - /opt/gitea/data/gitea/queues/** + - /opt/gitea/data/gitea/tmp/** + # lost+found появился вместе с переходом на volume: это свежая ext4, и + # каталог принадлежит uid 0 хоста, что внутри unprivileged LXC видно как + # nobody:nogroup и нечитаемо. Без исключения restic отдаёт exit 3 + # ("at least one source file could not be read") и юнит падает каждую + # ночь, хотя снапшот при этом сохраняется. Найдено 2026-09-02. + - /opt/gitea/data/lost+found/** tasks: - name: Configure restic offsite profile ansible.builtin.include_tasks: ../tasks/offsite-restic-profile.yml diff --git a/ansible/playbooks/pve-adguard.yml b/ansible/playbooks/pve-adguard.yml index ec6c21e..aae3f3b 100644 --- a/ansible/playbooks/pve-adguard.yml +++ b/ansible/playbooks/pve-adguard.yml @@ -3,6 +3,13 @@ hosts: mini-pc gather_facts: false vars: + # После переезда на OpenTofu (tofu/svc-adguard.tf, VMID 158) этот play — + # legacy: он таргетит СТАРЫЙ VMID 144 и его handler сделал бы pct start 144, + # подняв остановленный откат на боевом адресе. Пропускаем, пока реестр + # говорит provisioner: tofu. Для намеренного legacy rollback: + # -e pve_adguard_legacy_provisioning_enabled=true + pve_adguard_legacy_provisioning_enabled: >- + {{ homelab_services['adguard'].provisioner != 'tofu' }} adguard_vmid: 144 adguard_hostname: adguard adguard_ip: 192.168.1.28/24 @@ -15,6 +22,10 @@ - name: restart adguard lxc ansible.builtin.shell: "pct stop {{ adguard_vmid }} || true; pct start {{ adguard_vmid }}" changed_when: true + pre_tasks: + - name: Skip legacy AdGuard provisioning after cutover + ansible.builtin.meta: end_play + when: not (pve_adguard_legacy_provisioning_enabled | bool) tasks: - name: Check if AdGuard LXC exists ansible.builtin.command: "pct config {{ adguard_vmid }}" @@ -93,7 +104,16 @@ ansible_become: false - name: Configure AdGuard Home inside LXC - hosts: adguard + # Таргет переопределяем ради blue-green переезда на OpenTofu: во время + # миграции этот play нужно прогнать против нового контейнера на временном + # адресе. Просто `--limit adguard-new` для этого НЕ годится — лимит + # пересекается с паттерном play и даёт ноль хостов, а не перенацеливание + # (проверено 2026-09-02 через --list-hosts на всех плейбуках). + # Использовать вместе с --limit, чтобы play создания контейнера отсеялся: + # ansible-playbook playbooks/pve-adguard.yml \ + # -e pve_config_target=adguard-new --limit adguard-new + # По умолчанию поведение не меняется. + hosts: "{{ pve_config_target | default('adguard') }}" gather_facts: true vars: ansible_become: false diff --git a/ansible/playbooks/pve-docker-test.yml b/ansible/playbooks/pve-docker-test.yml index cff5852..29d586c 100644 --- a/ansible/playbooks/pve-docker-test.yml +++ b/ansible/playbooks/pve-docker-test.yml @@ -79,7 +79,16 @@ ansible_become: false - name: Configure Docker test host - hosts: docker-test + # Таргет переопределяем ради blue-green переезда на OpenTofu: во время + # миграции этот play нужно прогнать против нового контейнера на временном + # адресе. Просто `--limit docker-test-new` для этого НЕ годится — лимит + # пересекается с паттерном play и даёт ноль хостов, а не перенацеливание + # (проверено 2026-09-02 через --list-hosts на всех плейбуках). + # Использовать вместе с --limit, чтобы play создания контейнера отсеялся: + # ansible-playbook playbooks/pve-docker-test.yml \ + # -e pve_config_target=docker-test-new --limit docker-test-new + # По умолчанию поведение не меняется. + hosts: "{{ pve_config_target | default('docker-test') }}" gather_facts: true vars: ansible_become: false diff --git a/ansible/playbooks/pve-gitea.yml b/ansible/playbooks/pve-gitea.yml index 5539246..aea4020 100644 --- a/ansible/playbooks/pve-gitea.yml +++ b/ansible/playbooks/pve-gitea.yml @@ -1,6 +1,10 @@ - name: Create Gitea LXC on cloud-pc hosts: cloud-pc gather_facts: false + pre_tasks: + - name: Skip legacy Gitea provisioning for runtime-only calls + ansible.builtin.meta: end_play + when: not (pve_provisioning_enabled | default(true) | bool) vars: gitea_vmid: 141 gitea_hostname: gitea @@ -111,7 +115,16 @@ ansible_become: false - name: Configure Docker and Gitea inside LXC - hosts: gitea + # Таргет переопределяем ради blue-green переезда на OpenTofu: во время + # миграции этот play нужно прогнать против нового контейнера на временном + # адресе. Просто `--limit gitea-new` для этого НЕ годится — лимит + # пересекается с паттерном play и даёт ноль хостов, а не перенацеливание + # (проверено 2026-09-02 через --list-hosts на всех плейбуках). + # Использовать вместе с --limit, чтобы play создания контейнера отсеялся: + # ansible-playbook playbooks/pve-gitea.yml \ + # -e pve_config_target=gitea-new --limit gitea-new + # По умолчанию поведение не меняется. + hosts: "{{ pve_config_target | default('gitea') }}" gather_facts: true vars: ansible_become: false diff --git a/ansible/playbooks/pve-grimmory.yml b/ansible/playbooks/pve-grimmory.yml index fda55ca..8ed992d 100644 --- a/ansible/playbooks/pve-grimmory.yml +++ b/ansible/playbooks/pve-grimmory.yml @@ -2,6 +2,10 @@ - name: Guard Grimmory VMID before API updates hosts: cloud-pc gather_facts: false + pre_tasks: + - name: Skip legacy Grimmory provisioning for runtime-only calls + ansible.builtin.meta: end_play + when: not (pve_provisioning_enabled | default(true) | bool) tasks: - name: Read existing VMID 149 configuration ansible.builtin.command: pct config 149 @@ -28,6 +32,10 @@ connection: local become: false gather_facts: false + pre_tasks: + - name: Skip legacy Grimmory provisioning for runtime-only calls + ansible.builtin.meta: end_play + when: not (pve_provisioning_enabled | default(true) | bool) vars: ansible_become: false ansible_python_interpreter: "{{ ansible_playbook_python }}" @@ -52,6 +60,10 @@ - name: Configure Grimmory LXC devices hosts: cloud-pc gather_facts: false + pre_tasks: + - name: Skip legacy Grimmory provisioning for runtime-only calls + ansible.builtin.meta: end_play + when: not (pve_provisioning_enabled | default(true) | bool) vars: grimmory_vmid: 149 handlers: @@ -119,13 +131,32 @@ ansible_become: false - name: Configure Grimmory runtime - hosts: grimmory + # Таргет переопределяем ради blue-green переезда на OpenTofu: во время + # миграции этот play нужно прогнать против нового контейнера на временном + # адресе. Просто `--limit grimmory-new` для этого НЕ годится — лимит + # пересекается с паттерном play и даёт ноль хостов, а не перенацеливание + # (проверено 2026-09-02 через --list-hosts на всех плейбуках). + # Использовать вместе с --limit, чтобы play создания контейнера отсеялся: + # ansible-playbook playbooks/pve-grimmory.yml \ + # -e pve_config_target=grimmory-new --limit grimmory-new + # По умолчанию поведение не меняется. + hosts: "{{ pve_config_target | default('grimmory') }}" gather_facts: true vars: ansible_become: false grimmory_root: /opt/grimmory grimmory_image: grimmory/grimmory:v3.2.4@sha256:dfa7afdfcf25d649fd664497a62385dd00cd9678c37546e182c172e41c8e80cb grimmory_mariadb_image: lscr.io/linuxserver/mariadb:11.4.8@sha256:91de7f701bc7fc3a424b81beafca7a7c6c4c5b7c8be6afd2ae148698695c0b0c + # Адрес, на который биндится порт, на который смотрят правила DOCKER-USER и + # по которому проверяется health. Раньше во всех трёх местах был зашит + # боевой 192.168.1.34, из-за чего play нельзя было прогнать против другого + # контейнера: Docker не биндит чужой адрес и роняет grimmory.service, а + # health-check уходил по сети в БОЕВОЙ сервис и давал ложный успех. + # expected_lan_ip — уже принятая в репозитории идиома для "адрес этого + # хоста в LAN" (её же использует roles/uptime_kuma). На боевом grimmory она + # равна 192.168.1.34, поэтому рендер там не меняется. Найдено 2026-09-02 + # при blue-green переезде на OpenTofu. + grimmory_bind_ip: "{{ expected_lan_ip }}" tasks: - name: Install Grimmory runtime packages ansible.builtin.apt: @@ -298,7 +329,7 @@ mariadb: condition: service_healthy ports: - - "192.168.1.34:6060:6060" + - "{{ grimmory_bind_ip }}:6060:6060" volumes: - ./data:/app/data - ./books:/books @@ -338,9 +369,9 @@ iptables -N GRIMMORY-FILTER 2>/dev/null || true iptables -F GRIMMORY-FILTER iptables -A GRIMMORY-FILTER -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT - iptables -A GRIMMORY-FILTER -s {{ homelab_lan_cidr }} -p tcp -m conntrack --ctorigdst 192.168.1.34 --ctorigdstport 6060 -j ACCEPT - iptables -A GRIMMORY-FILTER -s {{ openvpn_network_cidr }} -p tcp -m conntrack --ctorigdst 192.168.1.34 --ctorigdstport 6060 -j ACCEPT - iptables -A GRIMMORY-FILTER -p tcp -m conntrack --ctorigdst 192.168.1.34 --ctorigdstport 6060 -j DROP + iptables -A GRIMMORY-FILTER -s {{ homelab_lan_cidr }} -p tcp -m conntrack --ctorigdst {{ grimmory_bind_ip }} --ctorigdstport 6060 -j ACCEPT + iptables -A GRIMMORY-FILTER -s {{ openvpn_network_cidr }} -p tcp -m conntrack --ctorigdst {{ grimmory_bind_ip }} --ctorigdstport 6060 -j ACCEPT + iptables -A GRIMMORY-FILTER -p tcp -m conntrack --ctorigdst {{ grimmory_bind_ip }} --ctorigdstport 6060 -j DROP iptables -A GRIMMORY-FILTER -j RETURN iptables -C DOCKER-USER -j GRIMMORY-FILTER 2>/dev/null || iptables -I DOCKER-USER 1 -j GRIMMORY-FILTER register: grimmory_firewall_script @@ -417,7 +448,7 @@ - name: Wait for Grimmory health endpoint ansible.builtin.uri: - url: http://192.168.1.34:6060/api/v1/healthcheck + url: http://{{ grimmory_bind_ip }}:6060/api/v1/healthcheck status_code: 200 register: grimmory_health retries: 120 diff --git a/ansible/playbooks/pve-gyro.yml b/ansible/playbooks/pve-gyro.yml index a55d9f1..111780f 100644 --- a/ansible/playbooks/pve-gyro.yml +++ b/ansible/playbooks/pve-gyro.yml @@ -2,6 +2,12 @@ - name: Guard Gyro VMID before API updates hosts: mini-pc gather_facts: false + vars: + pve_gyro_legacy_provisioning_enabled: "{{ homelab_services['gyro'].provisioner != 'tofu' }}" + pre_tasks: + - name: Skip legacy Gyro provisioning after cutover + ansible.builtin.meta: end_play + when: not (pve_gyro_legacy_provisioning_enabled | bool) tasks: - name: Read existing VMID 150 configuration ansible.builtin.command: pct config 150 @@ -29,6 +35,7 @@ become: false gather_facts: false vars: + pve_gyro_legacy_provisioning_enabled: "{{ homelab_services['gyro'].provisioner != 'tofu' }}" ansible_become: false ansible_python_interpreter: "{{ ansible_playbook_python }}" pve_lxc_vmid: 150 @@ -45,6 +52,10 @@ pve_lxc_update: false pve_lxc_features: [] pve_lxc_ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst + pre_tasks: + - name: Skip legacy Gyro provisioning after cutover + ansible.builtin.meta: end_play + when: not (pve_gyro_legacy_provisioning_enabled | bool) roles: - role: pve_lxc @@ -52,7 +63,12 @@ hosts: mini-pc gather_facts: false vars: + pve_gyro_legacy_provisioning_enabled: "{{ homelab_services['gyro'].provisioner != 'tofu' }}" gyro_vmid: 150 + pre_tasks: + - name: Skip legacy Gyro provisioning after cutover + ansible.builtin.meta: end_play + when: not (pve_gyro_legacy_provisioning_enabled | bool) tasks: - name: Read Gyro LXC configuration ansible.builtin.command: "pct config {{ gyro_vmid }}" diff --git a/ansible/playbooks/pve-mihomo.yml b/ansible/playbooks/pve-mihomo.yml index 73421f2..e0b6e79 100644 --- a/ansible/playbooks/pve-mihomo.yml +++ b/ansible/playbooks/pve-mihomo.yml @@ -3,6 +3,13 @@ hosts: mini-pc gather_facts: false vars: + # После переезда на OpenTofu (tofu/svc-mihomo.tf, VMID 159) этот play — + # legacy: он таргетит СТАРЫЙ VMID 143 и его handler сделал бы pct start 143, + # подняв остановленный откат на боевом адресе. Пропускаем, пока реестр + # говорит provisioner: tofu. Для намеренного legacy rollback: + # -e pve_mihomo_legacy_provisioning_enabled=true + pve_mihomo_legacy_provisioning_enabled: >- + {{ homelab_services['mihomo'].provisioner != 'tofu' }} mihomo_vmid: 143 mihomo_hostname: mihomo mihomo_ip: 192.168.1.27/24 @@ -15,6 +22,10 @@ - name: restart mihomo lxc ansible.builtin.shell: "pct stop {{ mihomo_vmid }} || true; pct start {{ mihomo_vmid }}" changed_when: true + pre_tasks: + - name: Skip legacy mihomo provisioning after cutover + ansible.builtin.meta: end_play + when: not (pve_mihomo_legacy_provisioning_enabled | bool) tasks: - name: Check if mihomo LXC exists ansible.builtin.command: "pct config {{ mihomo_vmid }}" @@ -107,7 +118,16 @@ ansible_become: false - name: Prepare mihomo runtime host - hosts: mihomo + # Таргет переопределяем ради blue-green переезда на OpenTofu: во время + # миграции этот play нужно прогнать против нового контейнера на временном + # адресе. Просто `--limit mihomo-new` для этого НЕ годится — лимит + # пересекается с паттерном play и даёт ноль хостов, а не перенацеливание + # (проверено 2026-09-02 через --list-hosts на всех плейбуках). + # Использовать вместе с --limit, чтобы play создания контейнера отсеялся: + # ansible-playbook playbooks/pve-mihomo.yml \ + # -e pve_config_target=mihomo-new --limit mihomo-new + # По умолчанию поведение не меняется. + hosts: "{{ pve_config_target | default('mihomo') }}" gather_facts: true vars: ansible_become: false diff --git a/ansible/playbooks/pve-ovpn-mini.yml b/ansible/playbooks/pve-ovpn-mini.yml index 2641513..433691a 100644 --- a/ansible/playbooks/pve-ovpn-mini.yml +++ b/ansible/playbooks/pve-ovpn-mini.yml @@ -1,7 +1,23 @@ +--- +# После переезда на OpenTofu (tofu/svc-ovpn-mini.tf, VMID 160) обе play здесь — +# legacy: они таргетят СТАРЫЙ VMID 132, а их handlers сделали бы pct start 132, +# подняв остановленный откат на боевом адресе 192.168.1.23. Пропускаем, пока +# реестр говорит provisioner: tofu. Конфигурация OpenVPN-шлюза в этот плейбук +# никогда не входила — она в playbooks/openvpn-vps-mini.yml (роль +# openvpn_gateway, группа vpn_openvpn). Для намеренного legacy rollback: +# -e pve_ovpn_mini_legacy_provisioning_enabled=true + - name: Create ovpn-mini LXC on mini-pc hosts: localhost connection: local gather_facts: false + vars: + pve_ovpn_mini_legacy_provisioning_enabled: >- + {{ homelab_services['ovpn-mini'].provisioner != 'tofu' }} + pre_tasks: + - name: Skip legacy ovpn-mini provisioning after cutover + ansible.builtin.meta: end_play + when: not (pve_ovpn_mini_legacy_provisioning_enabled | bool) roles: - role: pve_lxc vars: @@ -18,10 +34,17 @@ hosts: mini-pc gather_facts: false become: true + vars: + pve_ovpn_mini_legacy_provisioning_enabled: >- + {{ homelab_services['ovpn-mini'].provisioner != 'tofu' }} handlers: - name: restart ovpn-mini lxc ansible.builtin.shell: pct stop 132 || true; pct start 132 changed_when: true + pre_tasks: + - name: Skip legacy ovpn-mini provisioning after cutover + ansible.builtin.meta: end_play + when: not (pve_ovpn_mini_legacy_provisioning_enabled | bool) tasks: - name: Allow /dev/net/tun device ansible.builtin.lineinfile: diff --git a/ansible/playbooks/pve-vaultwarden.yml b/ansible/playbooks/pve-vaultwarden.yml index 2cffe3d..3e1523c 100644 --- a/ansible/playbooks/pve-vaultwarden.yml +++ b/ansible/playbooks/pve-vaultwarden.yml @@ -2,6 +2,10 @@ - name: Guard Vaultwarden VMID before API updates hosts: mini-pc gather_facts: false + pre_tasks: + - name: Skip legacy Vaultwarden provisioning for runtime-only calls + ansible.builtin.meta: end_play + when: not (pve_provisioning_enabled | default(true) | bool) tasks: - name: Read existing VMID 140 configuration ansible.builtin.command: pct config 140 @@ -25,6 +29,10 @@ - name: Create Vaultwarden LXC on mini-pc hosts: mini-pc gather_facts: false + pre_tasks: + - name: Skip legacy Vaultwarden provisioning for runtime-only calls + ansible.builtin.meta: end_play + when: not (pve_provisioning_enabled | default(true) | bool) vars: vaultwarden_vmid: 140 vaultwarden_hostname: vaultwarden @@ -103,7 +111,16 @@ ansible_become: false - name: Configure Docker and Vaultwarden inside LXC - hosts: vaultwarden + # Таргет переопределяем ради blue-green переезда на OpenTofu: во время + # миграции этот play нужно прогнать против нового контейнера на временном + # адресе. Просто `--limit vaultwarden-new` для этого НЕ годится — лимит + # пересекается с паттерном play и даёт ноль хостов, а не перенацеливание + # (проверено 2026-09-02 через --list-hosts на всех плейбуках). + # Использовать вместе с --limit, чтобы play создания контейнера отсеялся: + # ansible-playbook playbooks/pve-vaultwarden.yml \ + # -e pve_config_target=vaultwarden-new --limit vaultwarden-new + # По умолчанию поведение не меняется. + hosts: "{{ pve_config_target | default('vaultwarden') }}" gather_facts: true vars: ansible_become: false diff --git a/ansible/playbooks/uptime-kuma.yml b/ansible/playbooks/uptime-kuma.yml index 3ae72a5..eb8d9e8 100644 --- a/ansible/playbooks/uptime-kuma.yml +++ b/ansible/playbooks/uptime-kuma.yml @@ -1,6 +1,13 @@ --- - name: Freeze Prometheus monitoring and configure Uptime Kuma - hosts: monitoring_server + # Таргет переопределяем ради blue-green переезда на OpenTofu: конфигурацию + # нужно прогнать против нового контейнера на временном адресе. `--limit` + # сам по себе не перенацеливает play, а обнуляет его (проверено 2026-09-02). + # Использовать вместе с --limit: + # ansible-playbook playbooks/uptime-kuma.yml \ + # -e pve_config_target=monitoring-new --limit monitoring-new + # По умолчанию поведение не меняется. + hosts: "{{ pve_config_target | default('monitoring_server') }}" gather_facts: false roles: - role: uptime_kuma diff --git a/ansible/roles/uptime_kuma/tasks/main.yml b/ansible/roles/uptime_kuma/tasks/main.yml index fcf681b..4e29fec 100644 --- a/ansible/roles/uptime_kuma/tasks/main.yml +++ b/ansible/roles/uptime_kuma/tasks/main.yml @@ -137,11 +137,21 @@ delay: 5 until: uptime_kuma_health.status == 200 +# На хосте, где замороженный стек никогда не разворачивался (например, новый +# контейнер blue-green переезда), юнита homelab-monitoring нет, и systemd-модуль +# упал бы с "Could not find the requested service". Смысл шага — "legacy-стек не +# должен работать", а на чистом хосте это уже так. Проверено 2026-09-02. +- name: Detect the legacy monitoring unit + ansible.builtin.stat: + path: /etc/systemd/system/homelab-monitoring.service + register: uptime_kuma_legacy_unit + - name: Freeze the legacy Prometheus monitoring stack after Uptime Kuma is healthy ansible.builtin.systemd: name: homelab-monitoring enabled: false state: stopped + when: uptime_kuma_legacy_unit.stat.exists - name: Remove legacy monitoring firewall rules community.general.ufw: diff --git a/ansible/ssh_config b/ansible/ssh_config index d71f1dc..950bbc3 100644 --- a/ansible/ssh_config +++ b/ansible/ssh_config @@ -39,12 +39,21 @@ Host cloud-pc mini-pc 192.168.1.5 192.168.1.10 IdentityFile ~/.ssh/id_ed25519_homelab_ansible ProxyJump ru-vps -# ── LXC, доступные напрямую по LAN (без ProxyJump) ────────────────────── +# ── LXC pbs и ovpn-mini ───────────────────────────────────────────────── +# pbs идёт через ProxyJump ru-vps (агрегированная строка ниже): вне LAN это +# его единственный путь, а `make check`/`status`/`backup-audit` иначе падали. Host pbs 192.168.1.20 HostName 192.168.1.20 +# ovpn-mini — ProxyJump none НАМЕРЕННО: путь через ru-vps шёл бы по туннелю, +# который сам ovpn-mini и терминирует. Пока туннель жив это работало, но при +# его реконфигурации (или переезде контейнера) транспорт закольцовывается на +# мёртвый туннель. Из LAN хост доступен напрямую; вне LAN управление — +# через консоль ноды (pct exec) или при заранее поднятом туннеле. +# Первая строка блока побеждает по правилу "первое значение опции". Host ovpn-mini 192.168.1.23 HostName 192.168.1.23 + ProxyJump none # ── LXC за jump-хостом ────────────────────────────────────────────────── Host vaultwarden 192.168.1.24 @@ -53,9 +62,6 @@ Host vaultwarden 192.168.1.24 Host gitea 192.168.1.25 HostName 192.168.1.25 -Host memoir-bot 192.168.1.26 - HostName 192.168.1.26 - Host mihomo 192.168.1.27 HostName 192.168.1.27 @@ -80,23 +86,25 @@ Host grimmory 192.168.1.34 Host gyro 192.168.1.35 HostName 192.168.1.35 - # ── Групповые опции. ssh_config не поддерживает перенос строк, поэтому # списки паттернов длинные: имя хоста (для `ssh gitea`) + IP (Ansible # подключается по ansible_host). ───────────────────────────────────── -# ProxyJump только для LXC, до которых нет прямого доступа -Host vaultwarden gitea memoir-bot mihomo adguard docker-test monitoring hermes-ai emergency-bot grimmory gyro 192.168.1.24 192.168.1.25 192.168.1.26 192.168.1.27 192.168.1.28 192.168.1.29 192.168.1.30 192.168.1.31 192.168.1.32 192.168.1.34 192.168.1.35 +# ProxyJump для всех LXC. ОСТОРОЖНО: путь до ovpn-mini идёт через туннель, +# который сам ovpn-mini и терминирует. Для чтения это безопасно, но +# playbooks/openvpn-vps-mini.yml вне LAN может оборвать себе транспорт на +# середине прогона — переконфигурацию OpenVPN выполняй из локальной сети. +Host pbs ovpn-mini vaultwarden gitea mihomo adguard docker-test monitoring hermes-ai emergency-bot grimmory gyro 192.168.1.20 192.168.1.23 192.168.1.24 192.168.1.25 192.168.1.27 192.168.1.28 192.168.1.29 192.168.1.30 192.168.1.31 192.168.1.32 192.168.1.34 192.168.1.35 ProxyJump ru-vps # Все LXC — под root с ключом id_ed25519_homelab -Host pbs ovpn-mini vaultwarden gitea memoir-bot mihomo adguard docker-test monitoring hermes-ai emergency-bot grimmory gyro 192.168.1.20 192.168.1.23 192.168.1.24 192.168.1.25 192.168.1.26 192.168.1.27 192.168.1.28 192.168.1.29 192.168.1.30 192.168.1.31 192.168.1.32 192.168.1.34 192.168.1.35 +Host pbs ovpn-mini vaultwarden gitea mihomo adguard docker-test monitoring hermes-ai emergency-bot grimmory gyro 192.168.1.20 192.168.1.23 192.168.1.24 192.168.1.25 192.168.1.27 192.168.1.28 192.168.1.29 192.168.1.30 192.168.1.31 192.168.1.32 192.168.1.34 192.168.1.35 User root IdentityFile ~/.ssh/id_ed25519_homelab # Общие опции для хостов HomeLab (намеренно не `Host *`, чтобы не влиять # на остальные записи ~/.ssh/config пользователя) -Host ru-vps vps cloud-pc mini-pc pbs ovpn-mini vaultwarden gitea memoir-bot mihomo adguard docker-test monitoring hermes-ai emergency-bot grimmory gyro 157.22.231.198 192.168.1.5 192.168.1.10 192.168.1.20 192.168.1.23 192.168.1.24 192.168.1.25 192.168.1.26 192.168.1.27 192.168.1.28 192.168.1.29 192.168.1.30 192.168.1.31 192.168.1.32 192.168.1.34 192.168.1.35 +Host ru-vps vps cloud-pc mini-pc pbs ovpn-mini vaultwarden gitea mihomo adguard docker-test monitoring hermes-ai emergency-bot grimmory gyro 157.22.231.198 192.168.1.5 192.168.1.10 192.168.1.20 192.168.1.23 192.168.1.24 192.168.1.25 192.168.1.27 192.168.1.28 192.168.1.29 192.168.1.30 192.168.1.31 192.168.1.32 192.168.1.34 192.168.1.35 IdentitiesOnly yes StrictHostKeyChecking accept-new ControlMaster auto diff --git a/tofu/services.tf b/tofu/services.tf new file mode 100644 index 0000000..8f38301 --- /dev/null +++ b/tofu/services.tf @@ -0,0 +1,93 @@ +# ============================================================================ +# Сервисы, переведённые на OpenTofu provisioning по схеме blue-green. +# Процедура и порядок сервисов: docs/ai/migration-tofu.md. +# ============================================================================ +# +# Сервис №1: emergency-bot (OLD vmid 148, mini-pc). +# Эталон снят 2026-09-02: ssh mini-pc sudo pct config 148. +# Данных нет (backup: none), шаг 4.4 (перенос данных) пропускается целиком. +# +# vm_id 151 — новый VMID для blue-green переезда, TMPIP 192.168.1.6 был +# использован только на шаге 4.3 (проверка перед cutover). После шага 4.5.15 +# адрес — боевой 192.168.1.32, OLD (148) остановлен и остаётся откатом +# минимум неделю. + +resource "proxmox_virtual_environment_container" "emergency_bot" { + node_name = "mini-pc" + vm_id = 151 + unprivileged = true + start_on_boot = true + started = true + tags = ["tofu"] + + initialization { + hostname = "emergency-bot" + + ip_config { + ipv4 { + address = "192.168.1.32/24" + gateway = "192.168.1.1" + } + } + + dns { + servers = ["1.1.1.1"] + } + + user_account { + keys = [trimspace(file(pathexpand("~/.ssh/id_ed25519_homelab.pub")))] + } + } + + operating_system { + template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst" + type = "debian" + } + + cpu { + cores = 1 + } + + memory { + dedicated = 512 + swap = 256 + } + + disk { + datastore_id = "local-lvm" + size = 4 + } + + # Эталон: features: nesting=1. Ни keyctl, ни fuse, ни device passthrough + # эталону не нужны, поэтому доступно в обоих режимах аутентификации. + features { + nesting = true + } + + # roles/pve_lxc всем контейнерам ставит cmode=shell (pve_lxc_cmode), провайдер + # это отслеживает через блок console.type — без него на следующем apply + # откатит на дефолт Proxmox tty. Обнаружено 2026-09-02 на этом сервисе, + # см. tofu/README.md. + console { + type = "shell" + } + + network_interface { + name = "eth0" + bridge = "vmbr0" + firewall = true + } + + startup { + order = 70 + } +} + +output "emergency_bot" { + description = "Что проверять на узле после apply" + value = { + vmid = proxmox_virtual_environment_container.emergency_bot.vm_id + node = proxmox_virtual_environment_container.emergency_bot.node_name + verify = "ssh mini-pc sudo pct config ${proxmox_virtual_environment_container.emergency_bot.vm_id}" + } +} diff --git a/tofu/svc-adguard.tf b/tofu/svc-adguard.tf new file mode 100644 index 0000000..72191eb --- /dev/null +++ b/tofu/svc-adguard.tf @@ -0,0 +1,127 @@ +# ============================================================================ +# Сервис №8 по плану (docs/ai/migration-tofu.md, раздел 5): adguard. +# DNS-фильтр всей LAN. Жёсткое окно простоя только между шагами 4.5.14 и +# 4.5.17 (два контейнера на 192.168.1.28 одновременно невозможны). +# +# OLD vmid 144, узел mini-pc, боевой адрес 192.168.1.28. +# Эталон снят 2026-09-02: ssh mini-pc sudo pct config 144 (дословно совпадает +# с /etc/pve/lxc/144.conf): +# +# arch: amd64 +# cmode: shell +# cores: 1 +# features: nesting=1,keyctl=1 +# hostname: adguard +# memory: 512 +# nameserver: 1.1.1.1 +# net0: name=eth0,bridge=vmbr0,firewall=1,gw=192.168.1.1,ip=192.168.1.28/24,type=veth +# onboot: 1 +# ostype: debian +# rootfs: local-lvm:vm-144-disk-0,size=8G +# startup: order=40 +# swap: 512 +# unprivileged: 1 +# lxc.cgroup2.devices.allow: c 10:229 rwm +# lxc.mount.entry: /dev/fuse dev/fuse none bind,create=file +# +# vm_id 158 — новый VMID для blue-green переезда (назначен, не менять). +# TMPIP 192.168.1.17 используется только на шаге 4.3 (проверка перед cutover). +# После шага 4.5.15 адрес меняется на боевой 192.168.1.28, OLD (144) +# останавливается и остаётся откатом минимум неделю. +# +# --- /dev/fuse: features.fuse, а не device_passthrough ---------------------- +# Эталонные lxc.cgroup2.devices.allow + lxc.mount.entry для /dev/fuse — ручной +# обход (lineinfile по /etc/pve/lxc/144.conf в pve-adguard.yml), потому что +# Proxmox API сырые lxc.* ключи не принимает. Заменяется штатным флагом PVE +# features.fuse, проверенным на пилоте VMID 199 и на сервисах 2-7. Следствие: +# pct config нового контейнера покажет features: fuse=1,keyctl=1,nesting=1 — +# текстуально иначе, чем эталон, при том же эффекте. См. tofu/README.md. +# +# --- Данные ---------------------------------------------------------------- +# /opt/adguard/{conf,work} (~313 МБ, почти всё — work/: журнал запросов, +# статистика, кэш фильтров). conf/AdGuardHome.yaml несёт весь конфиг, включая +# хэш пароля админа, DNS rewrites, upstream и клиентов. Переносится целиком +# на шаге 4.4; Ansible этим каталогом не управляет. +# ============================================================================ + +resource "proxmox_virtual_environment_container" "adguard" { + node_name = "mini-pc" + vm_id = 158 + unprivileged = true + start_on_boot = true + started = true + tags = ["tofu"] + + initialization { + hostname = "adguard" + + ip_config { + ipv4 { + address = "192.168.1.28/24" + gateway = "192.168.1.1" + } + } + + dns { + servers = ["1.1.1.1"] + } + + user_account { + keys = [trimspace(file(pathexpand("~/.ssh/id_ed25519_homelab.pub")))] + } + } + + operating_system { + template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst" + type = "debian" + } + + cpu { + cores = 1 + } + + memory { + dedicated = 512 + swap = 512 + } + + disk { + datastore_id = "local-lvm" + size = 8 + } + + # Эталон: features: nesting=1,keyctl=1 плюс проброс /dev/fuse через сырые + # lxc.* строки. fuse=1 — штатная замена этого обхода (см. комментарий выше). + features { + nesting = true + keyctl = true + fuse = true + } + + # roles/pve_lxc всем контейнерам ставит cmode=shell (pve_lxc_cmode), провайдер + # это отслеживает через блок console.type — без него на следующем apply + # откатит на дефолт Proxmox tty. Обнаружено 2026-09-02 на emergency-bot, + # см. tofu/README.md. + console { + type = "shell" + } + + network_interface { + name = "eth0" + bridge = "vmbr0" + firewall = true + } + + startup { + order = 40 + } +} + +output "adguard" { + description = "Что проверять на узле после apply" + value = { + vmid = proxmox_virtual_environment_container.adguard.vm_id + node = proxmox_virtual_environment_container.adguard.node_name + verify = "ssh mini-pc sudo pct config ${proxmox_virtual_environment_container.adguard.vm_id}" + } +} diff --git a/tofu/svc-docker-test.tf b/tofu/svc-docker-test.tf new file mode 100644 index 0000000..3c5ca7c --- /dev/null +++ b/tofu/svc-docker-test.tf @@ -0,0 +1,104 @@ +# ============================================================================ +# Сервис №2: docker-test (OLD vmid 145, cloud-pc). +# Эталон снят 2026-09-02: ssh cloud-pc sudo pct config 145. +# Данных нет (песочница), шаг 4.4 (перенос данных) пропускается целиком. +# +# vm_id 152 — новый VMID blue-green переезда. TMPIP 192.168.1.11 отработал на +# шаге 4.3; cutover выполнен 2026-09-02, адрес боевой — 192.168.1.29. +# OLD (145) остановлен и остаётся откатом минимум неделю, до 2026-09-09. +# +# В отличие от emergency_bot, этот сервис проверяет проброс /dev/fuse на +# реальном сервисе: Docker внутри работает на storage-driver fuse-overlayfs. +# ============================================================================ + +resource "proxmox_virtual_environment_container" "docker_test" { + node_name = "cloud-pc" + vm_id = 152 + unprivileged = true + start_on_boot = true + started = true + tags = ["tofu"] + + initialization { + hostname = "docker-test" + + ip_config { + ipv4 { + address = "192.168.1.29/24" + gateway = "192.168.1.1" + } + } + + dns { + servers = ["1.1.1.1"] + } + + user_account { + keys = [trimspace(file(pathexpand("~/.ssh/id_ed25519_homelab.pub")))] + } + } + + operating_system { + template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst" + type = "debian" + } + + cpu { + cores = 1 + } + + memory { + dedicated = 512 + swap = 512 + } + + disk { + datastore_id = "data" + size = 8 + } + + # Эталон: features: nesting=1,keyctl=1 плюс проброс /dev/fuse, сделанный в + # pve-docker-test.yml правкой /etc/pve/lxc/145.conf (lxc.cgroup2.devices.allow + # + lxc.mount.entry) — Proxmox API сырые lxc.* ключи не принимает. + # + # У провайдера для /dev/fuse есть ШТАТНЫЙ флаг features.fuse, а не + # device_passthrough: последний нужен только для /dev/net/tun (dev0: в PVE + # 8.2+). См. tofu/pilot.tf.example:17-21 и tofu/README.md. + # Поэтому pct config нового контейнера покажет features: fuse=1,keyctl=1, + # nesting=1 — текстуально иначе, чем эталон, но это тот же результат + # штатным механизмом вместо обхода. + # + # Всё три флага требуют root@pam; токену Proxmox разрешает только nesting. + features { + nesting = true + keyctl = true + fuse = true + } + + # roles/pve_lxc всем контейнерам ставит cmode=shell (pve_lxc_cmode), провайдер + # это отслеживает через блок console.type — без него на следующем apply + # откатит на дефолт Proxmox tty. Обнаружено 2026-09-02 на emergency-bot, + # см. tofu/README.md. + console { + type = "shell" + } + + network_interface { + name = "eth0" + bridge = "vmbr0" + firewall = true + } + + startup { + order = 50 + } +} + +output "docker_test" { + description = "Что проверять на узле после apply" + value = { + vmid = proxmox_virtual_environment_container.docker_test.vm_id + node = proxmox_virtual_environment_container.docker_test.node_name + verify = "ssh cloud-pc sudo pct config ${proxmox_virtual_environment_container.docker_test.vm_id}" + } +} diff --git a/tofu/svc-gitea.tf b/tofu/svc-gitea.tf new file mode 100644 index 0000000..52e95e1 --- /dev/null +++ b/tofu/svc-gitea.tf @@ -0,0 +1,146 @@ +# ============================================================================ +# Сервис №3 по плану (docs/ai/migration-tofu.md, раздел 5): gitea. +# Главный приз всей миграции — ради него она и затевалась. +# +# OLD vmid 141, узел cloud-pc, боевой адрес 192.168.1.25. +# Эталон снят 2026-09-02: ssh cloud-pc sudo pct config 141 (и совпадающий +# /etc/pve/lxc/141.conf): +# +# arch: amd64 +# cmode: shell +# cores: 2 +# features: nesting=1,keyctl=1 +# hostname: gitea +# memory: 2048 +# mp0: /opt/data/gitea,mp=/opt/gitea/data +# nameserver: 1.1.1.1 +# net0: name=eth0,bridge=vmbr0,firewall=1,gw=192.168.1.1,ip=192.168.1.25/24,type=veth +# onboot: 1 +# ostype: debian +# rootfs: data:141/vm-141-disk-0.raw,size=32G +# startup: order=50 +# swap: 1024 +# unprivileged: 1 +# lxc.cgroup2.devices.allow: c 10:229 rwm +# lxc.mount.entry: /dev/fuse dev/fuse none bind,create=file +# +# vm_id 153 — новый VMID для blue-green переезда. IP ниже — TMPIP +# 192.168.1.12, используется только до шага 4.5.15 (проверка перед cutover); +# затем меняется на боевой 192.168.1.25, а OLD (141) останавливается и +# остаётся откатом минимум неделю. +# +# --- Bind mount -> volume ---------------------------------------------------- +# Эталонный mp0 — bind mount каталога НОДЫ (/opt/data/gitea на cloud-pc), а не +# volume на datastore. Provider с root@pam мог бы воспроизвести и bind mount +# буквально, но делать это НЕЛЬЗЯ: если по ошибке одновременно поднимутся +# оба контейнера, два процесса Gitea будут писать в один и тот же каталог +# хоста и повредят SQLite и репозитории. Поэтому mount_point ниже — volume на +# том же datastore, что и rootfs ("data"), с независимым хранилищем. Данные +# переносятся отдельно на шаге 4.4 (rsync/pct push), а не общим bind mount. +# Размер volume — 32G по решению migration-tofu.md (раздел 5, пункт 3): +# фактическое использование на 2026-09-02 — 2.3G (`sudo du -sh +# /opt/data/gitea`), так что 32G — запас с большим множителем на рост +# репозиториев, а не минимально достаточное значение. +# +# --- /dev/fuse: features.fuse, а не device_passthrough ---------------------- +# Эталонные строки lxc.cgroup2.devices.allow + lxc.mount.entry для /dev/fuse — +# это ручной обход, потому что Proxmox API не принимает сырые lxc.* ключи +# (pve-gitea.yml добавляет их через lineinfile). У провайдера bpg/proxmox для +# ровно этого эффекта есть штатный флаг PVE features.fuse — это подтверждено +# на пилоте (tofu/README.md, раздел «Решение: root@pam по паролю») и явно +# названо в комментарии tofu/pilot.tf.example: "У провайдера для этого есть +# features.fuse — штатный флаг PVE, а не обход". device_passthrough (dev0:) +# нужен только для сырых character-устройств вида /dev/net/tun; у gitea +# такого устройства нет (в pct config нет строки dev0:), поэтому блока +# device_passthrough в этом ресурсе нет вообще — только features.fuse. +# ============================================================================ + +resource "proxmox_virtual_environment_container" "gitea" { + node_name = "cloud-pc" + vm_id = 153 + unprivileged = true + start_on_boot = true + started = true + tags = ["tofu"] + + initialization { + hostname = "gitea" + + ip_config { + ipv4 { + address = "192.168.1.25/24" + gateway = "192.168.1.1" + } + } + + dns { + servers = ["1.1.1.1"] + } + + user_account { + keys = [trimspace(file(pathexpand("~/.ssh/id_ed25519_homelab.pub")))] + } + } + + operating_system { + template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst" + type = "debian" + } + + cpu { + cores = 2 + } + + memory { + dedicated = 2048 + swap = 1024 + } + + disk { + datastore_id = "data" + size = 32 + } + + # Эталон: features: nesting=1,keyctl=1 плюс ручной обход для /dev/fuse + # (см. комментарий выше). fuse=1 — штатная замена этого обхода. + features { + nesting = true + keyctl = true + fuse = true + } + + # roles/pve_lxc всем контейнерам ставит cmode=shell (pve_lxc_cmode), провайдер + # это отслеживает через блок console.type — без него на следующем apply + # откатит на дефолт Proxmox tty. Обнаружено 2026-09-02 на emergency-bot, + # см. tofu/README.md. + console { + type = "shell" + } + + # Bind mount ноды заменён на независимый volume — см. комментарий выше. + # Тот же datastore, что и rootfs. + mount_point { + volume = "data" + size = "32G" + path = "/opt/gitea/data" + } + + network_interface { + name = "eth0" + bridge = "vmbr0" + firewall = true + } + + startup { + order = 50 + } +} + +output "gitea" { + description = "Что проверять на узле после apply" + value = { + vmid = proxmox_virtual_environment_container.gitea.vm_id + node = proxmox_virtual_environment_container.gitea.node_name + verify = "ssh cloud-pc sudo pct config ${proxmox_virtual_environment_container.gitea.vm_id}" + } +} diff --git a/tofu/svc-grimmory.tf b/tofu/svc-grimmory.tf new file mode 100644 index 0000000..440a8c5 --- /dev/null +++ b/tofu/svc-grimmory.tf @@ -0,0 +1,149 @@ +# ============================================================================ +# Сервис №7 по плану (docs/ai/migration-tofu.md, раздел 5): grimmory. +# Самый болезненный шаг: MariaDB, Flyway-миграции, контракт OPDS/KOReader. +# +# OLD vmid 149, узел cloud-pc, боевой адрес 192.168.1.34. +# Эталон снят 2026-09-02: ssh cloud-pc sudo pct config 149 (и совпадающий +# дословно /etc/pve/lxc/149.conf): +# +# arch: amd64 +# cmode: shell +# cores: 2 +# features: nesting=1,keyctl=1 +# hostname: grimmory +# memory: 4096 +# nameserver: 1.1.1.1 +# net0: name=eth0,bridge=vmbr0,firewall=1,gw=192.168.1.1,ip=192.168.1.34/24,type=veth +# onboot: 1 +# ostype: debian +# rootfs: data:149/vm-149-disk-0.raw,size=64G +# startup: order=100 +# swap: 1024 +# unprivileged: 1 +# lxc.cgroup2.devices.allow: c 10:229 rwm +# lxc.mount.entry: /dev/fuse dev/fuse none bind,create=file +# +# Ни mpN, ни dev0 в эталоне нет — только /dev/fuse через сырые lxc.* строки. +# Значит правило про bind mount -> mount_point (как у gitea) здесь не +# применяется: mount_point в этом ресурсе не добавлен. +# +# vm_id 157 — новый VMID для blue-green переезда (назначен, не менять). +# TMPIP 192.168.1.16 ниже используется только на шаге 4.3 (проверка перед +# cutover; запись grimmory-new уже есть в hosts.yml/ssh_config — заведена +# координатором). После шага 4.5.15 адрес меняется на боевой 192.168.1.34, +# OLD (149) останавливается и остаётся откатом минимум неделю. +# +# --- /dev/fuse: features.fuse, а не device_passthrough ---------------------- +# Эталонные lxc.cgroup2.devices.allow + lxc.mount.entry для /dev/fuse — это +# ручной обход (lineinfile по /etc/pve/lxc/149.conf в pve-grimmory.yml), +# потому что Proxmox API сырые lxc.* ключи не принимает. +# +# Заменяется штатным флагом PVE features.fuse, а НЕ device_passthrough. +# device_passthrough (dev0:) предназначен для сырых character-устройств вида +# /dev/net/tun; для /dev/fuse у PVE есть флаг, и именно он проверен на пилоте +# VMID 199 (`features: fuse=1,keyctl=1,nesting=1`), см. tofu/pilot.tf.example +# строки 17-21 и tofu/README.md. Формулировка «device_passthrough для каждого +# устройства из lxc.mount.entry» в migration-tofu.md п.4.2 была слишком +# широкой и уточнена там же 2026-09-02; так же поправлены svc-docker-test.tf +# и svc-vaultwarden.tf, так что все ресурсы tofu/ теперь единообразны. +# +# Следствие: pct config нового контейнера покажет features: fuse=1,keyctl=1, +# nesting=1 — текстуально иначе, чем эталон, при том же эффекте. +# +# --- MariaDB: только dump/restore, не копирование файлов -------------------- +# /opt/grimmory/mariadb — это datadir MariaDB (grimmory-mariadb, БД grimmory). +# Копировать эти файлы между старым и новым контейнером НЕЛЬЗЯ: нужен +# mariadb-dump на старом контейнере (сервис приложения остановлен, MariaDB +# работает) и restore на новом. Рецепт dump уже есть и проверен в рабочем +# offsite-профиле restic (playbooks/offsite-restic-yadisk.yml, задача +# ../tasks/offsite-restic-profile.yml): docker exec -e MYSQL_PWD=... +# grimmory-mariadb mariadb-dump --single-transaction --routines --events +# --databases grimmory. legacy-warning.md и docs/ai/edge-cases.md запрещают +# code-only downgrade после Flyway-миграции — на новом контейнере обязан +# развернуться РОВНО ТОТ ЖЕ pinned образ v3.2.4 (проверено 2026-09-02: +# запущенный в CT 149 образ grimmory/grimmory:v3.2.4 имеет digest +# sha256:dfa7afdfcf25d649fd664497a62385dd00cd9678c37546e182c172e41c8e80cb — +# совпадает с реестром services.yml байт-в-байт, расхождения нет). +# ============================================================================ + +resource "proxmox_virtual_environment_container" "grimmory" { + node_name = "cloud-pc" + vm_id = 157 + unprivileged = true + start_on_boot = true + started = true + tags = ["tofu"] + + initialization { + hostname = "grimmory" + + ip_config { + ipv4 { + address = "192.168.1.34/24" + gateway = "192.168.1.1" + } + } + + dns { + servers = ["1.1.1.1"] + } + + user_account { + keys = [trimspace(file(pathexpand("~/.ssh/id_ed25519_homelab.pub")))] + } + } + + operating_system { + template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst" + type = "debian" + } + + cpu { + cores = 2 + } + + memory { + dedicated = 4096 + swap = 1024 + } + + disk { + datastore_id = "data" + size = 64 + } + + # Эталон: features: nesting=1,keyctl=1 плюс /dev/fuse через сырые lxc.* + # строки. fuse=1 — штатная замена этого обхода, см. комментарий файла выше. + features { + nesting = true + keyctl = true + fuse = true + } + + # roles/pve_lxc всем контейнерам ставит cmode=shell (pve_lxc_cmode), провайдер + # это отслеживает через блок console.type — без него на следующем apply + # откатит на дефолт Proxmox tty. Обнаружено 2026-09-02 на emergency-bot, + # см. tofu/README.md. + console { + type = "shell" + } + + network_interface { + name = "eth0" + bridge = "vmbr0" + firewall = true + } + + startup { + order = 100 + } +} + +output "grimmory" { + description = "Что проверять на узле после apply" + value = { + vmid = proxmox_virtual_environment_container.grimmory.vm_id + node = proxmox_virtual_environment_container.grimmory.node_name + verify = "ssh cloud-pc sudo pct config ${proxmox_virtual_environment_container.grimmory.vm_id}" + } +} diff --git a/tofu/svc-gyro.tf b/tofu/svc-gyro.tf new file mode 100644 index 0000000..02f38f3 --- /dev/null +++ b/tofu/svc-gyro.tf @@ -0,0 +1,92 @@ +# ============================================================================ +# Сервис №6: gyro (OLD vmid 150, mini-pc). +# Эталон снят 2026-09-02: ssh mini-pc sudo pct config 150. +# +# vm_id 156 — новый VMID для blue-green переезда, TMPIP 192.168.1.15. +# После шага 4.5.15 адрес — боевой 192.168.1.35, OLD (150) остановлен +# и остаётся откатом минимум неделю. +# +# ОСОБЕННОСТИ: +# - Контейнер намеренно без nesting (features: "" в эталоне). +# - Proxmox-фаервол уже перенесён на VMID 156; Datacenter firewall остаётся выключен. +# - Требует Ansible Vault password (make gyro для конфигурации). +# - Старое предупреждение про host key Gitea неверно: gyro пинит github.com, +# поэтому после cutover менять known_hosts не нужно. +# ============================================================================ + +resource "proxmox_virtual_environment_container" "gyro" { + node_name = "mini-pc" + vm_id = 156 + unprivileged = true + start_on_boot = true + started = true + tags = ["tofu"] + + initialization { + hostname = "gyro" + + ip_config { + ipv4 { + address = "192.168.1.35/24" + gateway = "192.168.1.1" + } + } + + dns { + servers = ["1.1.1.1"] + } + + user_account { + keys = [trimspace(file(pathexpand("~/.ssh/id_ed25519_homelab.pub")))] + } + } + + operating_system { + template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst" + type = "debian" + } + + cpu { + cores = 1 + } + + memory { + dedicated = 512 + swap = 256 + } + + disk { + datastore_id = "local-lvm" + size = 2 + } + + # Эталон: features: (пусто) — nesting отключён НАМЕРЕННО, no keyctl, no fuse. + # Блок features не объявляется, чтобы провайдер не создавал его с дефолтами. + # Это консервативное решение: gyro этот функционал не требует. + + # roles/pve_lxc всем контейнерам ставит cmode=shell (pve_lxc_cmode), провайдер + # это отслеживает через блок console.type — без него на следующем apply + # откатит на дефолт Proxmox tty. Обнаружено 2026-09-02 на emergency-bot. + console { + type = "shell" + } + + network_interface { + name = "eth0" + bridge = "vmbr0" + firewall = true + } + + startup { + order = 80 + } +} + +output "gyro" { + description = "Что проверять на узле после apply" + value = { + vmid = proxmox_virtual_environment_container.gyro.vm_id + node = proxmox_virtual_environment_container.gyro.node_name + verify = "ssh mini-pc sudo pct config ${proxmox_virtual_environment_container.gyro.vm_id}" + } +} diff --git a/tofu/svc-mihomo.tf b/tofu/svc-mihomo.tf new file mode 100644 index 0000000..edda1f7 --- /dev/null +++ b/tofu/svc-mihomo.tf @@ -0,0 +1,139 @@ +# ============================================================================ +# Сервис №9 по плану (docs/ai/migration-tofu.md, раздел 5): mihomo. +# Локальный прокси + MetaCubeXD UI. На него завязаны egress hermes-ai +# (bash_config_proxy_* в hosts.yml), внешние пробы Uptime Kuma, emergency-бот +# и уведомления gyro — простой затрагивает всё это разом. +# +# OLD vmid 143, узел mini-pc, боевой адрес 192.168.1.27. +# Эталон снят 2026-09-02: ssh mini-pc sudo pct config 143 (дословно совпадает +# с /etc/pve/lxc/143.conf): +# +# arch: amd64 +# cmode: shell +# cores: 1 +# features: nesting=1,keyctl=1 +# hostname: mihomo +# memory: 512 +# nameserver: 1.1.1.1 +# net0: name=eth0,bridge=vmbr0,firewall=1,gw=192.168.1.1,ip=192.168.1.27/24,type=veth +# onboot: 1 +# ostype: debian +# rootfs: local-lvm:vm-143-disk-0,size=8G +# startup: order=70 +# swap: 512 +# unprivileged: 1 +# lxc.cgroup2.devices.allow: c 10:229 rwm # /dev/fuse +# lxc.mount.entry: /dev/fuse dev/fuse none bind,create=file +# lxc.cgroup2.devices.allow: c 10:200 rwm # /dev/net/tun +# lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file +# +# vm_id 159 — новый VMID для blue-green переезда (назначен, не менять). +# TMPIP 192.168.1.18 используется только на шаге 4.3. После шага 4.5.15 адрес +# меняется на боевой 192.168.1.27, OLD (143) останавливается и остаётся +# откатом минимум неделю. +# +# --- Два устройства, ДВА разных механизма ----------------------------------- +# /dev/fuse -> штатный флаг features.fuse (Docker внутри на fuse-overlayfs). +# /dev/net/tun -> блок device_passthrough (dev0: в PVE 8.2+). mihomo.service +# запускает контейнер с `--device /dev/net/tun` для TUN-режима. +# Это первый боевой сервис, использующий device_passthrough: на пилоте VMID +# 199 механизм проверен (`dev0: deny-write=0,path=/dev/net/tun,...`, повторный +# plan -> No changes), см. tofu/pilot.tf.example и tofu/README.md. Оба требуют +# root@pam; в обычном токенном режиме Proxmox отвечает 403. +# Следствие: pct config покажет `features: fuse=1,...` и `dev0: path=/dev/net/tun` +# вместо сырых lxc.* строк эталона — тот же эффект штатным механизмом. +# +# --- Данные ---------------------------------------------------------------- +# /opt/mihomo (~80 КБ): config/config.yaml (реальный конфиг прокси), +# config/cache.db, config/providers/main.yaml. Переносится целиком на шаге +# 4.4. Задача "Install default mihomo config if missing" в pve-mihomo.yml +# идёт с force: false, поэтому перенесённый конфиг не перетирается. +# ============================================================================ + +resource "proxmox_virtual_environment_container" "mihomo" { + node_name = "mini-pc" + vm_id = 159 + unprivileged = true + start_on_boot = true + started = true + tags = ["tofu"] + + initialization { + hostname = "mihomo" + + ip_config { + ipv4 { + address = "192.168.1.27/24" + gateway = "192.168.1.1" + } + } + + dns { + servers = ["1.1.1.1"] + } + + user_account { + keys = [trimspace(file(pathexpand("~/.ssh/id_ed25519_homelab.pub")))] + } + } + + operating_system { + template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst" + type = "debian" + } + + cpu { + cores = 1 + } + + memory { + dedicated = 512 + swap = 512 + } + + disk { + datastore_id = "local-lvm" + size = 8 + } + + # Эталон: features: nesting=1,keyctl=1 плюс /dev/fuse через сырые lxc.* + # строки. fuse=1 — штатная замена этого обхода (см. комментарий выше). + features { + nesting = true + keyctl = true + fuse = true + } + + # /dev/net/tun: в эталоне проброшен сырыми lxc.cgroup2.devices.allow + + # lxc.mount.entry. Штатная декларация — device_passthrough (dev0:). + device_passthrough { + path = "/dev/net/tun" + } + + # roles/pve_lxc всем контейнерам ставит cmode=shell (pve_lxc_cmode), провайдер + # это отслеживает через блок console.type — без него на следующем apply + # откатит на дефолт Proxmox tty. Обнаружено 2026-09-02 на emergency-bot, + # см. tofu/README.md. + console { + type = "shell" + } + + network_interface { + name = "eth0" + bridge = "vmbr0" + firewall = true + } + + startup { + order = 70 + } +} + +output "mihomo" { + description = "Что проверять на узле после apply" + value = { + vmid = proxmox_virtual_environment_container.mihomo.vm_id + node = proxmox_virtual_environment_container.mihomo.node_name + verify = "ssh mini-pc sudo pct config ${proxmox_virtual_environment_container.mihomo.vm_id}" + } +} diff --git a/tofu/svc-monitoring.tf b/tofu/svc-monitoring.tf new file mode 100644 index 0000000..483b1e5 --- /dev/null +++ b/tofu/svc-monitoring.tf @@ -0,0 +1,107 @@ +# ============================================================================ +# Сервис №5: monitoring (OLD vmid 146, cloud-pc). +# Эталон снят 2026-09-02: ssh cloud-pc sudo pct config 146 (совпадает с +# cat /etc/pve/lxc/146.conf дословно). +# +# vm_id 155 — новый VMID для blue-green переезда, TMPIP 192.168.1.14 +# используется только на шаге 4.3 (проверка перед cutover). После шага 4.5.15 +# адрес — боевой 192.168.1.30 (тот же, что и у OLD: в этой схеме переезжает +# VMID, а не IP), OLD (146) остановлен и остаётся откатом минимум неделю. +# +# Активный сервис — Uptime Kuma: SQLite в /opt/uptime-kuma/data/kuma.db, +# UI-состояние (мониторы, уведомления) в Ansible не описано, каталог +# переносится целиком на шаге 4.4. Стек Prometheus + Alertmanager + Grafana +# в этом контейнере ЗАМОРОЖЕН (docs/ai/legacy-warning.md) — его данные лежат +# отдельно в /opt/monitoring (в основном TSDB Prometheus) и в новый контейнер +# намеренно НЕ переносятся; решение о судьбе замороженного стека отдельное, +# не часть этого переезда. +# +# ОСОБЕННОСТЬ features: реестр (services.yml) отмечает, что роль создаёт +# контейнер с nesting=1, а keyctl=1 добавляет отдельной задачей `pct set 146 +# --features nesting=1,keyctl=1` уже после создания (playbooks/pve-monitoring.yml). +# Здесь воспроизведён ЭТАЛОН как он есть на живом контейнере — оба флага сразу, +# декларативно, без промежуточного шага: в привилегированном режиме root@pam +# (см. tofu/README.md, «Решение: root@pam по паролю») дошаг `pct set` для этого +# и остальных сервисов больше не нужен, features применяются одной фазой. +# ============================================================================ + +resource "proxmox_virtual_environment_container" "monitoring" { + node_name = "cloud-pc" + vm_id = 155 + unprivileged = true + start_on_boot = true + started = true + tags = ["tofu"] + + initialization { + hostname = "monitoring" + + ip_config { + ipv4 { + address = "192.168.1.30/24" + gateway = "192.168.1.1" + } + } + + dns { + servers = ["1.1.1.1"] + } + + user_account { + keys = [trimspace(file(pathexpand("~/.ssh/id_ed25519_homelab.pub")))] + } + } + + operating_system { + template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst" + type = "debian" + } + + cpu { + cores = 2 + } + + memory { + dedicated = 4096 + swap = 512 + } + + disk { + datastore_id = "data" + size = 24 + } + + # Эталон: features: nesting=1,keyctl=1 — см. ОСОБЕННОСТЬ в комментарии выше. + # Ни fuse, ни device passthrough эталону не нужны (dev0 в pct config нет). + features { + nesting = true + keyctl = true + } + + # roles/pve_lxc всем контейнерам ставит cmode=shell (pve_lxc_cmode), провайдер + # это отслеживает через блок console.type — без него на следующем apply + # откатит на дефолт Proxmox tty. Обнаружено 2026-09-02 на emergency-bot, + # см. tofu/README.md. + console { + type = "shell" + } + + network_interface { + name = "eth0" + bridge = "vmbr0" + firewall = true + } + + startup { + order = 80 + } +} + +output "monitoring" { + description = "Что проверять на узле после apply" + value = { + vmid = proxmox_virtual_environment_container.monitoring.vm_id + node = proxmox_virtual_environment_container.monitoring.node_name + verify = "ssh cloud-pc sudo pct config ${proxmox_virtual_environment_container.monitoring.vm_id}" + } +} diff --git a/tofu/svc-ovpn-mini.tf b/tofu/svc-ovpn-mini.tf new file mode 100644 index 0000000..140736a --- /dev/null +++ b/tofu/svc-ovpn-mini.tf @@ -0,0 +1,141 @@ +# ============================================================================ +# Сервис №10 по плану (docs/ai/migration-tofu.md, раздел 5): ovpn-mini. +# Терминирует OpenVPN-туннель ru-vps <-> LAN (gateway-конец, 10.78.0.2). +# Через него идёт ProxyJump ко всем LXC вне LAN и обратный путь reverse-proxy. +# ПЕРЕЕЗД ТОЛЬКО ИЗ ЛОКАЛЬНОЙ СЕТИ: вне LAN прогон оборвёт себе транспорт. +# +# OLD vmid 132, узел mini-pc, боевой адрес 192.168.1.23. +# Эталон снят 2026-09-02: ssh mini-pc sudo pct config 132 (дословно совпадает +# с /etc/pve/lxc/132.conf): +# +# arch: amd64 +# cmode: shell +# cores: 1 +# features: nesting=1 +# hostname: ovpn-mini +# memory: 256 +# nameserver: 1.1.1.1 +# net0: name=eth0,bridge=vmbr0,firewall=1,gw=192.168.1.1,ip=192.168.1.23/24,type=veth +# onboot: 1 +# ostype: debian +# rootfs: local-lvm:vm-132-disk-0,size=8G +# startup: order=30 +# swap: 128 +# unprivileged: 1 +# lxc.cgroup2.devices.allow: c 10:200 rwm # /dev/net/tun +# lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file +# +# vm_id 160 — новый VMID для blue-green переезда (назначен, не менять). +# TMPIP 192.168.1.19 используется только на шаге 4.3. После шага 4.5.15 адрес +# меняется на боевой 192.168.1.23, OLD (132) останавливается и остаётся +# откатом минимум неделю. +# +# --- features: только nesting --------------------------------------------- +# Эталон: features: nesting=1 — ни keyctl, ни fuse. Оставлено как есть: +# OpenVPN-шлюзу этот функционал не нужен. +# +# --- /dev/net/tun: device_passthrough ------------------------------------ +# В эталоне проброшен сырыми lxc.cgroup2.devices.allow + lxc.mount.entry +# (play "Allow TUN device" в pve-ovpn-mini.yml). Штатная декларация — +# device_passthrough (dev0:), проверена на пилоте VMID 199 и на mihomo. +# Требует root@pam. +# +# --- Конфигурация и данные ---------------------------------------------- +# pve-ovpn-mini.yml конфигурационной части НЕ содержит (только создание + +# проброс TUN). Настраивает playbooks/openvpn-vps-mini.yml (роль +# openvpn_gateway, группа vpn_openvpn = ru-vps + ovpn-mini). +# Единственные данные для переноса: /etc/openvpn/homelab/static.key — +# общий статический ключ с ru-vps. Остальное (homelab.conf, up.sh, down.sh, +# systemd-юнит) роль рендерит заново. LAN-адрес ovpn-mini ни в одном шаблоне +# роли не фигурирует (masquerade идёт по `-o eth0`), поэтому туннель не +# зависит от смены адреса — меняется только VMID. +# +# --- roles/pve_lxc ----------------------------------------------------- +# До этого переезда ovpn-mini был единственным потребителем roles/pve_lxc. +# После него роль не подключена нигде — кандидат на удаление, см. +# docs/ai/migration-tofu.md раздел 6. +# ============================================================================ + +resource "proxmox_virtual_environment_container" "ovpn_mini" { + node_name = "mini-pc" + vm_id = 160 + unprivileged = true + start_on_boot = true + started = true + tags = ["tofu"] + + initialization { + hostname = "ovpn-mini" + + ip_config { + ipv4 { + address = "192.168.1.23/24" + gateway = "192.168.1.1" + } + } + + dns { + servers = ["1.1.1.1"] + } + + user_account { + keys = [trimspace(file(pathexpand("~/.ssh/id_ed25519_homelab.pub")))] + } + } + + operating_system { + template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst" + type = "debian" + } + + cpu { + cores = 1 + } + + memory { + dedicated = 256 + swap = 128 + } + + disk { + datastore_id = "local-lvm" + size = 8 + } + + # Эталон: features: nesting=1 (без keyctl/fuse). + features { + nesting = true + } + + # /dev/net/tun: в эталоне — сырые lxc.* строки. Штатная декларация dev0:. + device_passthrough { + path = "/dev/net/tun" + } + + # roles/pve_lxc всем контейнерам ставит cmode=shell (pve_lxc_cmode), провайдер + # это отслеживает через блок console.type — без него на следующем apply + # откатит на дефолт Proxmox tty. Обнаружено 2026-09-02 на emergency-bot, + # см. tofu/README.md. + console { + type = "shell" + } + + network_interface { + name = "eth0" + bridge = "vmbr0" + firewall = true + } + + startup { + order = 30 + } +} + +output "ovpn_mini" { + description = "Что проверять на узле после apply" + value = { + vmid = proxmox_virtual_environment_container.ovpn_mini.vm_id + node = proxmox_virtual_environment_container.ovpn_mini.node_name + verify = "ssh mini-pc sudo pct config ${proxmox_virtual_environment_container.ovpn_mini.vm_id}" + } +} diff --git a/tofu/svc-vaultwarden.tf b/tofu/svc-vaultwarden.tf new file mode 100644 index 0000000..7291708 --- /dev/null +++ b/tofu/svc-vaultwarden.tf @@ -0,0 +1,109 @@ +# ============================================================================ +# Сервис №4: vaultwarden (OLD vmid 140, mini-pc). +# Эталон снят 2026-09-02: ssh mini-pc sudo pct config 140 / cat /etc/pve/lxc/140.conf. +# Данные критичны: SQLite /opt/vaultwarden/data/db.sqlite3 (плюс config.json, +# rsa_key.pem, icon_cache/). Переносятся вручную на шаге 4.4, а не этим +# ресурсом — контейнер создаётся пустым, данные переезжают отдельной +# процедурой (см. docs/ai/migration-tofu.md, п.5.4). +# +# vm_id 154. TMPIP 192.168.1.13 отработал на шагах 4.3 и 4.4; cutover выполнен +# 2026-09-02, адрес боевой — 192.168.1.24. Данные перенесены и сверены: +# integrity_check ok, 1 users / 314 ciphers совпали с боевыми. +# OLD (140) остановлен и остаётся откатом минимум неделю, до 2026-09-09 +# (docs/ai/migration-tofu.md, инвариант №2). +# ============================================================================ + +resource "proxmox_virtual_environment_container" "vaultwarden" { + node_name = "mini-pc" + vm_id = 154 + unprivileged = true + start_on_boot = true + started = true + tags = ["tofu"] + + initialization { + hostname = "vaultwarden" + + ip_config { + ipv4 { + address = "192.168.1.24/24" + gateway = "192.168.1.1" + } + } + + dns { + servers = ["1.1.1.1"] + } + + user_account { + keys = [trimspace(file(pathexpand("~/.ssh/id_ed25519_homelab.pub")))] + } + } + + operating_system { + template_file_id = "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst" + type = "debian" + } + + cpu { + cores = 2 + } + + memory { + dedicated = 1024 + swap = 512 + } + + disk { + datastore_id = "local-lvm" + size = 16 + } + + # Эталон: features: nesting=1,keyctl=1 плюс /dev/fuse, проброшенный вручную + # двумя строками в /etc/pve/lxc/140.conf (lxc.cgroup2.devices.allow c 10:229 + # rwm + lxc.mount.entry) — Proxmox API сырые lxc.* ключи не принимает, + # поэтому pve-vaultwarden.yml добавляет их через lineinfile. + # + # ЗАМЕНА ЭТОГО ОБХОДА — features.fuse, а НЕ device_passthrough. + # device_passthrough (dev0:) нужен только для сырых character-устройств + # вида /dev/net/tun. Для /dev/fuse у PVE есть штатный флаг, и именно он + # проверен на пилоте VMID 199 — результат был + # `features: fuse=1,keyctl=1,nesting=1`, см. tofu/pilot.tf.example:17-21 + # и tofu/README.md. Формулировка «device_passthrough для каждого устройства» + # в migration-tofu.md п.4.2 слишком широкая; она уточнена там же. + # + # Следствие: pct config нового контейнера покажет + # `features: fuse=1,keyctl=1,nesting=1` — текстуально иначе, чем эталон, + # но это тот же эффект штатным механизмом вместо обхода. + features { + nesting = true + keyctl = true + fuse = true + } + + # roles/pve_lxc всем контейнерам ставит cmode=shell (pve_lxc_cmode), провайдер + # это отслеживает через блок console.type — без него на следующем apply + # откатит на дефолт Proxmox tty (найдено на emergency-bot, см. tofu/README.md). + console { + type = "shell" + } + + network_interface { + name = "eth0" + bridge = "vmbr0" + firewall = true + } + + startup { + order = 40 + } +} + +output "vaultwarden" { + description = "Что проверять на узле после apply" + value = { + vmid = proxmox_virtual_environment_container.vaultwarden.vm_id + node = proxmox_virtual_environment_container.vaultwarden.node_name + verify = "ssh mini-pc sudo pct config ${proxmox_virtual_environment_container.vaultwarden.vm_id}" + } +}