diff --git a/ansible/.env.example b/.env.example similarity index 100% rename from ansible/.env.example rename to .env.example diff --git a/ansible/Makefile b/ansible/Makefile index 2225f05..90adb69 100644 --- a/ansible/Makefile +++ b/ansible/Makefile @@ -24,6 +24,7 @@ SHELL := /bin/sh MAKEFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST))) ANSIBLE_DIR := $(patsubst %/,%,$(dir $(MAKEFILE_PATH))) +REPO_ROOT := $(abspath $(ANSIBLE_DIR)/..) VENV_BIN := $(ANSIBLE_DIR)/.venv/bin # Разрешение бинарей: сначала рабочий локальный venv, иначе PATH (nix devshell, @@ -42,7 +43,11 @@ YAMLLINT ?= $(call venv_bin,yamllint) # python3 из PATH, а не из .venv. PYTHON ?= python3 -ENV_FILE ?= .env +# Секреты живут в КОРНЕ репозитория, а не в ansible/: их потребляет не только +# Ansible, но и OpenTofu (tofu/), а держать два файла или ходить в подкаталог +# из соседнего инструмента неудобно. Путь абсолютный, поэтому цели работают +# из любого cwd. +ENV_FILE ?= $(REPO_ROOT)/.env # Дополнительные аргументы ansible-playbook для любой цели. EXTRA ?= @@ -51,19 +56,19 @@ ASK_BECOME ?= -K # Спрашивать пароль Ansible Vault. `make gyro ASK_VAULT=` отключает. ASK_VAULT ?= --ask-vault-pass -# Строгая загрузка ansible/.env: обязательна для Proxmox API и секретов. +# Строгая загрузка корневого .env: обязательна для Proxmox API и секретов. # Каждая строка рецепта make — отдельный шелл, поэтому source и запуск идут одной строкой. REQUIRE_ENV = if [ ! -f '$(ENV_FILE)' ]; then \ - printf 'ОШИБКА: не найден %s/%s\n' '$(ANSIBLE_DIR)' '$(ENV_FILE)' >&2; \ + printf 'ОШИБКА: не найден %s\n' '$(ENV_FILE)' >&2; \ printf 'Создай его и заполни реальными значениями:\n' >&2; \ - printf ' cp .env.example .env\n' >&2; \ + printf ' cp %s/.env.example %s\n' '$(REPO_ROOT)' '$(ENV_FILE)' >&2; \ printf 'Нужны PROXMOX_* (и MONITORING_* / EMERGENCY_* для профильных целей).\n' >&2; \ exit 1; \ fi; \ - set -a; . './$(ENV_FILE)'; set +a + set -a; . '$(ENV_FILE)'; set +a # Мягкая загрузка: .env подхватывается если есть, иначе просто предупреждение. -LOAD_ENV = if [ -f '$(ENV_FILE)' ]; then set -a; . './$(ENV_FILE)'; set +a; \ +LOAD_ENV = if [ -f '$(ENV_FILE)' ]; then set -a; . '$(ENV_FILE)'; set +a; \ else printf 'ВНИМАНИЕ: %s не найден, продолжаю без него.\n' '$(ENV_FILE)' >&2; fi # Защита от случайного запуска опасных плейбуков. @@ -85,7 +90,7 @@ help: ## Показать этот список целей print "HomeLab Ansible — ручное управление инфраструктурой"; \ print ""; \ print " Использование: make <цель> [EXTRA=\"--limit host -vv\"]"; \ - print " Плейбуки Proxmox API сами подхватывают ./.env"; \ + print " Секреты берутся из .env в КОРНЕ репозитория"; \ } \ /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5); next } \ /^[a-zA-Z0-9_%.-]+:.*##/ { printf " \033[36m%-24s\033[0m %s\n", $$1, $$2 } \ @@ -126,10 +131,21 @@ check: ## Проверка связности и ожидаемых IP (playbook status: ## Сводный статус инфраструктуры (playbooks/status.yml) ANSIBLE_CALLBACK_RESULT_FORMAT=yaml $(ANSIBLE_PLAYBOOK) playbooks/status.yml $(EXTRA) +# Оба линтера ищут конфиг (.ansible-lint / .yamllint) в текущем каталоге, а не +# у родителей. Запуск из ansible/ конфиг не находил, из-за чего в проверку +# затягивался вендоренный collections/ и цель выдавала около тысячи чужих +# нарушений. Запускаем из корня — ровно как .gitea/workflows/lint.yml. .PHONY: lint -lint: ## Прогнать ansible-lint и yamllint по репозиторию - $(ANSIBLE_LINT) - $(YAMLLINT) . +lint: ## Прогнать ansible-lint, yamllint и syntax-check — то же, что делает CI + cd '$(REPO_ROOT)' && $(ANSIBLE_LINT) + cd '$(REPO_ROOT)' && $(YAMLLINT) . + @rc=0; for f in playbooks/*.yml; do \ + if $(ANSIBLE_PLAYBOOK) --syntax-check "$$f" >/dev/null 2>&1; then \ + printf 'ok %s\n' "$$f"; \ + else \ + rc=1; printf 'FAIL %s\n' "$$f"; $(ANSIBLE_PLAYBOOK) --syntax-check "$$f" 2>&1 | sed 's/^/ /'; \ + fi; \ + done; exit $$rc .PHONY: docs docs: ## Напечатать markdown-таблицу хостов и групп в stdout @@ -139,6 +155,10 @@ docs: ## Напечатать markdown-таблицу хостов и групп inventory: ## Показать дерево инвентаря (ansible-inventory --graph) @$(INVENTORY_ENV) $(ANSIBLE_INVENTORY_BIN) -i inventory/hosts.yml --graph +.PHONY: validate +validate: ## Сверить реестр homelab_services с фактическим состоянием Proxmox (gate) + $(ANSIBLE_PLAYBOOK) playbooks/validate.yml $(EXTRA) + .PHONY: openvpn-check openvpn-check: ## Проверить OpenVPN-транспорт ru-vps <-> ovpn-mini $(ANSIBLE_PLAYBOOK) playbooks/openvpn-check.yml $(EXTRA) @@ -159,6 +179,10 @@ deploy-%: ## Применить playbooks/pve-<имя>.yml (пример: make d backup-jobs: ## Настроить PBS backup jobs (playbooks/pve-backup-jobs.yml) @$(REQUIRE_ENV); $(ANSIBLE_PLAYBOOK) playbooks/pve-backup-jobs.yml $(EXTRA) +.PHONY: pbs-storage +pbs-storage: ## Убрать storage-level prune policy с PBS storage (playbooks/pve-storage-pbs.yml) + $(ANSIBLE_PLAYBOOK) playbooks/pve-storage-pbs.yml $(EXTRA) + .PHONY: bootstrap-pve-token bootstrap-pve-token: ## Выпустить Proxmox API-токен прямо с ноды (нужен sudo) $(ANSIBLE_PLAYBOOK) playbooks/bootstrap-pve-api-token.yml $(ASK_BECOME) $(EXTRA) @@ -173,6 +197,14 @@ bootstrap-ansible-user: ## Завести сервисный аккаунт ansi ##@ Настройка сервисов +.PHONY: ru-vps-base +ru-vps-base: ## Базовое состояние ru-vps и стек Caddy (закрепляет образ, ставит homelab-caddy.service) + $(ANSIBLE_PLAYBOOK) playbooks/ru-vps-base.yml $(EXTRA) + +.PHONY: dry-ru-vps-base +dry-ru-vps-base: ## Предпросмотр ru-vps-base.yml (--check --diff) + $(ANSIBLE_PLAYBOOK) playbooks/ru-vps-base.yml --check --diff $(EXTRA) + .PHONY: reverse-proxy reverse-proxy: ## Единый reverse-proxy для gitea/vaultwarden/grimmory @$(LOAD_ENV); $(ANSIBLE_PLAYBOOK) playbooks/reverse-proxy.yml $(EXTRA) @@ -198,7 +230,7 @@ emergency-access: ## Настроить emergency reverse-SSH и Telegram-бот @$(REQUIRE_ENV); $(ANSIBLE_PLAYBOOK) playbooks/emergency-access.yml $(EXTRA) .PHONY: gyro -gyro: ## Настроить gyro-аллокатор в CT 150 (спрашивает пароль Vault) +gyro: ## Настроить gyro-аллокатор в production CT 156 (Tofu-managed); CT 150 — остановленный rollback (спрашивает пароль Vault) @$(REQUIRE_ENV); $(ANSIBLE_PLAYBOOK) playbooks/gyro.yml $(ASK_VAULT) $(EXTRA) .PHONY: uptime-kuma @@ -226,6 +258,65 @@ update-all: ## Последовательно обновить gitea, vaultwarde $(MAKE) update-mihomo $(MAKE) update-grimmory +##@ OpenTofu (пилот provisioning LXC) + +TOFU_DIR := $(REPO_ROOT)/tofu +TOFU ?= tofu + +# Tofu, в отличие от Ansible, ходит в Proxmox API напрямую по HTTPS и НЕ умеет +# ProxyJump из ssh_config. Вне локальной сети узлы недоступны (проверено: +# https://192.168.1.10:8006 отдаёт connection failure), поэтому на время команды +# поднимаем SSH-туннель через ru-vps и направляем провайдер в localhost. +# Изнутри LAN это тоже работает — просто лишний хоп, зато поведение одинаково. +TOFU_TUNNEL_PORT ?= 18006 +TOFU_SSH_SOCKET := $(ANSIBLE_DIR)/.ansible/tofu-tunnel.sock + +# insecure=true здесь не настройка вкуса: сертификат Proxmox выписан на узел, +# а обращаемся мы к 127.0.0.1, так что проверка имени не пройдёт в любом случае. +# +# Способ аутентификации выбирается автоматически: если в корневом .env задан +# PROXMOX_ROOT_PASSWORD — идём как root@pam (только так Proxmox разрешает +# features кроме nesting, device passthrough и bind mount каталога хоста), +# иначе токеном ansible@pve. Выбранный режим печатается в stderr, чтобы из +# вывода было видно, какими правами шёл apply. +TOFU_ENV = $(REQUIRE_ENV); \ + mkdir -p '$(dir $(TOFU_SSH_SOCKET))'; \ + ssh -F '$(ANSIBLE_DIR)/ssh_config' -M -S '$(TOFU_SSH_SOCKET)' -fN \ + -L $(TOFU_TUNNEL_PORT):$$PROXMOX_HOST:$${PROXMOX_PORT:-8006} ru-vps; \ + trap "ssh -S '$(TOFU_SSH_SOCKET)' -O exit ru-vps 2>/dev/null || true" EXIT; \ + export TF_VAR_pve_endpoint="https://127.0.0.1:$(TOFU_TUNNEL_PORT)"; \ + export TF_VAR_pve_insecure=true; \ + if [ -n "$${PROXMOX_ROOT_PASSWORD:-}" ] && [ "$${PROXMOX_ROOT_PASSWORD}" != 'replace-me' ]; then \ + export TF_VAR_pve_username="$${PROXMOX_ROOT_USER:-root@pam}"; \ + export TF_VAR_pve_password="$$PROXMOX_ROOT_PASSWORD"; \ + export TF_VAR_pve_api_token=""; \ + printf 'Proxmox: аутентификация %s (привилегированный режим)\n' "$${PROXMOX_ROOT_USER:-root@pam}" >&2; \ + else \ + export TF_VAR_pve_api_token="$$PROXMOX_USER!$$PROXMOX_TOKEN_ID=$$PROXMOX_TOKEN_SECRET"; \ + export TF_VAR_pve_username=""; \ + export TF_VAR_pve_password=""; \ + printf 'Proxmox: аутентификация токеном %s (features кроме nesting и dev-passthrough недоступны)\n' "$$PROXMOX_USER" >&2; \ + fi + +.PHONY: tofu-init +tofu-init: ## Скачать провайдер и инициализировать рабочий каталог tofu/ + @cd '$(TOFU_DIR)' && $(TOFU) init -input=false + +.PHONY: tofu-plan +tofu-plan: ## Показать план (read-only, ничего не меняет) + @$(TOFU_ENV); cd '$(TOFU_DIR)' && $(TOFU) plan -input=false + +.PHONY: tofu-apply +tofu-apply: ## Применить план — СОЗДАЁТ гостей в Proxmox (требует CONFIRM=1) + @$(REQUIRE_CONFIRM) + @$(TOFU_ENV); cd '$(TOFU_DIR)' && $(TOFU) apply -input=false -auto-approve + +.PHONY: tofu-destroy +tofu-destroy: ## УНИЧТОЖИТЬ всё, что создано в tofu/ (требует CONFIRM=1) + @$(REQUIRE_CONFIRM) + @printf 'Удаляет гостей Proxmox из состояния tofu вместе с их дисками.\n' >&2 + @$(TOFU_ENV); cd '$(TOFU_DIR)' && $(TOFU) destroy -input=false -auto-approve + ##@ Опасное (только осознанно, требует CONFIRM=1) .PHONY: mihomo-harden @@ -234,6 +325,16 @@ mihomo-harden: ## РОТИРУЕТ живые SOCKS-креды Mihomo на ru-vp @printf 'Ротация кредов необратима для клиентов без отката бэкапа конфига.\n' >&2 $(ANSIBLE_PLAYBOOK) playbooks/ru-vps-mihomo-harden.yml -e ru_vps_mihomo_harden_confirm=true $(EXTRA) +.PHONY: zerotier-decommission +zerotier-decommission: ## Вывести ZeroTier с ru-vps и почистить его правила UFW (требует CONFIRM=1) + @$(REQUIRE_CONFIRM) + @printf 'Останавливает контейнер zerotier и удаляет правила UFW. Данные и файлы остаются.\n' >&2 + $(ANSIBLE_PLAYBOOK) playbooks/ru-vps-zerotier-decommission.yml $(EXTRA) + +.PHONY: dry-zerotier-decommission +dry-zerotier-decommission: ## Предпросмотр вывода ZeroTier (--check --diff) + $(ANSIBLE_PLAYBOOK) playbooks/ru-vps-zerotier-decommission.yml --check --diff $(EXTRA) + .PHONY: monitoring monitoring: ## ЗАМОРОЖЕН: стек Prometheus. Запускать только при восстановлении мониторинга @$(REQUIRE_CONFIRM) diff --git a/ansible/playbooks/bootstrap-monitoring-pve-token.yml b/ansible/playbooks/bootstrap-monitoring-pve-token.yml index ed0d4db..1460aae 100644 --- a/ansible/playbooks/bootstrap-monitoring-pve-token.yml +++ b/ansible/playbooks/bootstrap-monitoring-pve-token.yml @@ -5,7 +5,8 @@ vars: monitoring_pve_user: monitoring@pve monitoring_pve_token_id: prometheus - monitoring_pve_env_file: "{{ playbook_dir }}/../.env" + # .env лежит в корне репозитория, playbook_dir — это ansible/playbooks. + monitoring_pve_env_file: "{{ playbook_dir }}/../../.env" monitoring_pve_rotate_existing_token: false tasks: - name: Read existing Proxmox users diff --git a/ansible/playbooks/bootstrap-pve-api-token.yml b/ansible/playbooks/bootstrap-pve-api-token.yml index c5b0f99..98ea839 100644 --- a/ansible/playbooks/bootstrap-pve-api-token.yml +++ b/ansible/playbooks/bootstrap-pve-api-token.yml @@ -37,10 +37,17 @@ pve_token_data: "{{ pve_token_add.stdout | from_json }}" no_log: true - - name: Write local ansible/.env + # ВНИМАНИЕ: задача переписывает файл ЦЕЛИКОМ, а не правит отдельные строки. + # Всё, что добавлено вручную или другими целями — MONITORING_*, EMERGENCY_*, + # PROXMOX_ROOT_PASSWORD — будет потеряно. Поэтому backup: true. + # Строки пишутся с префиксом `export`: по нему bootstrap-monitoring-pve-token.yml + # находит их своим lineinfile (regexp: "^export NAME="), и он же позволяет + # просто сделать `source .env` в обычном шелле. + - name: Write the local .env in the repository root ansible.builtin.copy: - dest: "{{ playbook_dir }}/../.env" + dest: "{{ playbook_dir }}/../../.env" mode: "0600" + backup: true content: | export PROXMOX_HOST={{ ansible_host }} export PROXMOX_USER='{{ pve_api_user }}' diff --git a/ansible/roles/monitoring_server/tasks/main.yml b/ansible/roles/monitoring_server/tasks/main.yml index 617b113..8ad37c8 100644 --- a/ansible/roles/monitoring_server/tasks/main.yml +++ b/ansible/roles/monitoring_server/tasks/main.yml @@ -12,7 +12,7 @@ - monitoring_pve_api_token_secret != 'replace-me' - monitoring_grafana_admin_password | length > 0 - monitoring_grafana_admin_password != 'change-me' - fail_msg: Load monitoring secrets from ignored ansible/.env or Ansible Vault before running this playbook. + fail_msg: Load monitoring secrets from the ignored repository-root .env or Ansible Vault before running this playbook. no_log: true - name: Install monitoring runtime packages diff --git a/ansible/roles/pve_lxc/tasks/main.yml b/ansible/roles/pve_lxc/tasks/main.yml index c224f55..556784d 100644 --- a/ansible/roles/pve_lxc/tasks/main.yml +++ b/ansible/roles/pve_lxc/tasks/main.yml @@ -6,7 +6,7 @@ - lookup('env', 'PROXMOX_USER') | length > 0 - lookup('env', 'PROXMOX_TOKEN_ID') | length > 0 - lookup('env', 'PROXMOX_TOKEN_SECRET') | length > 0 - fail_msg: Load ansible/.env with PROXMOX_HOST, PROXMOX_USER, PROXMOX_TOKEN_ID and PROXMOX_TOKEN_SECRET. + fail_msg: Load the repository-root .env with PROXMOX_HOST, PROXMOX_USER, PROXMOX_TOKEN_ID and PROXMOX_TOKEN_SECRET. - name: Create or update LXC container community.proxmox.proxmox: