Add Makefile as the entry point for manual operation
Knowing how to run something required reading ansible/README.md and remembering to source .env first. Provide `make help` instead, with targets grouped by purpose and pattern rules for the repetitive families: deploy-%, dry-%, update-% and play-%. .env is sourced automatically; targets that need Proxmox credentials fail with an actionable message when it is missing. Destructive targets -- mihomo-harden, which rotates live credentials, the frozen monitoring stack, and update-all -- require CONFIRM=1. The interpreter is resolved at runtime rather than hardcoded to .venv: the repository's venv is currently broken, so the Makefile falls back to whatever is on PATH, which is what the Nix devshell provides. gen-inventory-docs.py prints the host and group tables from ansible-inventory, so documentation can be regenerated instead of being maintained by hand and drifting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTocXkGUUazHdKKd3r9k71
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
# HomeLab Ansible — единая точка входа для РУЧНОГО управления инфраструктурой.
|
||||
#
|
||||
# Запускать из каталога ansible/ (или через `make -C ansible <цель>`):
|
||||
#
|
||||
# make список целей
|
||||
# make check базовая проверка связности
|
||||
# make deploy-gitea playbooks/pve-gitea.yml с загруженным .env
|
||||
# make dry-gitea то же самое в режиме --check --diff
|
||||
# make update-gitea playbooks/gitea-update.yml
|
||||
# make docs markdown-таблица хостов в stdout
|
||||
#
|
||||
# Дополнительные флаги ansible-playbook передаются через EXTRA:
|
||||
#
|
||||
# make check EXTRA="--limit pve_nodes -vv"
|
||||
#
|
||||
# Интерпретатор определяется автоматически: если рядом есть ./.venv — берётся он,
|
||||
# иначе бинарь ищется в PATH (nix devshell, системный ansible). Любой путь можно
|
||||
# переопределить переменной окружения, например:
|
||||
#
|
||||
# ANSIBLE_PLAYBOOK=$(which ansible-playbook) make check
|
||||
|
||||
SHELL := /bin/sh
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
MAKEFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST)))
|
||||
ANSIBLE_DIR := $(patsubst %/,%,$(dir $(MAKEFILE_PATH)))
|
||||
VENV_BIN := $(ANSIBLE_DIR)/.venv/bin
|
||||
|
||||
# Разрешение бинарей: сначала рабочий локальный venv, иначе PATH (nix devshell,
|
||||
# системный ansible). VENV_OK проверяет, что интерпретатор venv реально запускается,
|
||||
# — иначе сломанный .venv (например, после смены системного python) молча ломал бы
|
||||
# все цели. Переменные окружения имеют приоритет благодаря `?=`.
|
||||
VENV_OK := $(shell [ -x '$(VENV_BIN)/python3' ] && '$(VENV_BIN)/python3' -c '' >/dev/null 2>&1 && echo yes)
|
||||
venv_bin = $(if $(VENV_OK),$(if $(wildcard $(VENV_BIN)/$(1)),$(VENV_BIN)/$(1),$(1)),$(1))
|
||||
|
||||
ANSIBLE_PLAYBOOK ?= $(call venv_bin,ansible-playbook)
|
||||
ANSIBLE_INVENTORY_BIN ?= $(call venv_bin,ansible-inventory)
|
||||
ANSIBLE_GALAXY ?= $(call venv_bin,ansible-galaxy)
|
||||
ANSIBLE_LINT ?= $(call venv_bin,ansible-lint)
|
||||
YAMLLINT ?= $(call venv_bin,yamllint)
|
||||
# scripts/gen-inventory-docs.py обходится стандартной библиотекой, поэтому берём
|
||||
# python3 из PATH, а не из .venv.
|
||||
PYTHON ?= python3
|
||||
|
||||
ENV_FILE ?= .env
|
||||
|
||||
# Дополнительные аргументы ansible-playbook для любой цели.
|
||||
EXTRA ?=
|
||||
# Спрашивать sudo-пароль там, где нужен become. `make openvpn ASK_BECOME=` отключает.
|
||||
ASK_BECOME ?= -K
|
||||
# Спрашивать пароль Ansible Vault. `make gyro ASK_VAULT=` отключает.
|
||||
ASK_VAULT ?= --ask-vault-pass
|
||||
|
||||
# Строгая загрузка ansible/.env: обязательна для Proxmox API и секретов.
|
||||
# Каждая строка рецепта make — отдельный шелл, поэтому source и запуск идут одной строкой.
|
||||
REQUIRE_ENV = if [ ! -f '$(ENV_FILE)' ]; then \
|
||||
printf 'ОШИБКА: не найден %s/%s\n' '$(ANSIBLE_DIR)' '$(ENV_FILE)' >&2; \
|
||||
printf 'Создай его и заполни реальными значениями:\n' >&2; \
|
||||
printf ' cp .env.example .env\n' >&2; \
|
||||
printf 'Нужны PROXMOX_* (и MONITORING_* / EMERGENCY_* для профильных целей).\n' >&2; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
set -a; . './$(ENV_FILE)'; set +a
|
||||
|
||||
# Мягкая загрузка: .env подхватывается если есть, иначе просто предупреждение.
|
||||
LOAD_ENV = if [ -f '$(ENV_FILE)' ]; then set -a; . './$(ENV_FILE)'; set +a; \
|
||||
else printf 'ВНИМАНИЕ: %s не найден, продолжаю без него.\n' '$(ENV_FILE)' >&2; fi
|
||||
|
||||
# Защита от случайного запуска опасных плейбуков.
|
||||
REQUIRE_CONFIRM = if [ "$(CONFIRM)" != "1" ]; then \
|
||||
printf 'ОПАСНАЯ ЦЕЛЬ. Повтори явно: make $@ CONFIRM=1\n' >&2; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
# host_vars/gyro/vault.yml зашифрован Vault. Для чтения инвентаря он не нужен,
|
||||
# поэтому vars-плагины отключаются, чтобы make не спрашивал пароль Vault.
|
||||
INVENTORY_ENV = ANSIBLE_VARS_ENABLED= ANSIBLE_NOCOLOR=1
|
||||
|
||||
##@ Справка
|
||||
|
||||
.PHONY: help
|
||||
help: ## Показать этот список целей
|
||||
@awk 'BEGIN { FS = ":.*##"; \
|
||||
print ""; \
|
||||
print "HomeLab Ansible — ручное управление инфраструктурой"; \
|
||||
print ""; \
|
||||
print " Использование: make <цель> [EXTRA=\"--limit host -vv\"]"; \
|
||||
print " Плейбуки Proxmox API сами подхватывают ./.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 } \
|
||||
END { print "" }' $(MAKEFILE_PATH)
|
||||
|
||||
##@ Setup
|
||||
|
||||
.PHONY: setup
|
||||
setup: ## Создать .venv, поставить requirements.txt и galaxy-коллекции (не нужно в nix)
|
||||
$(PYTHON) -m venv $(ANSIBLE_DIR)/.venv
|
||||
$(VENV_BIN)/pip install --upgrade pip
|
||||
$(VENV_BIN)/pip install -r requirements.txt
|
||||
$(VENV_BIN)/ansible-galaxy collection install -r requirements.yml -p collections
|
||||
|
||||
.PHONY: collections
|
||||
collections: ## Доустановить только galaxy-коллекции из requirements.yml
|
||||
$(ANSIBLE_GALAXY) collection install -r requirements.yml -p collections
|
||||
|
||||
.PHONY: env-check
|
||||
env-check: ## Проверить наличие .env и заполненность ключевых переменных
|
||||
@$(REQUIRE_ENV); \
|
||||
rc=0; \
|
||||
for v in PROXMOX_HOST PROXMOX_USER PROXMOX_TOKEN_ID PROXMOX_TOKEN_SECRET; do \
|
||||
eval "val=\$$$$v"; \
|
||||
if [ -z "$$val" ] || [ "$$val" = 'replace-me' ]; then \
|
||||
printf 'не задано: %s\n' "$$v"; rc=1; \
|
||||
else printf 'ok: %s\n' "$$v"; fi; \
|
||||
done; \
|
||||
exit $$rc
|
||||
|
||||
##@ Проверки и диагностика
|
||||
|
||||
.PHONY: check
|
||||
check: ## Проверка связности и ожидаемых IP (playbooks/check.yml)
|
||||
$(ANSIBLE_PLAYBOOK) playbooks/check.yml $(EXTRA)
|
||||
|
||||
.PHONY: status
|
||||
status: ## Сводный статус инфраструктуры (playbooks/status.yml)
|
||||
ANSIBLE_CALLBACK_RESULT_FORMAT=yaml $(ANSIBLE_PLAYBOOK) playbooks/status.yml $(EXTRA)
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## Прогнать ansible-lint и yamllint по репозиторию
|
||||
$(ANSIBLE_LINT)
|
||||
$(YAMLLINT) .
|
||||
|
||||
.PHONY: docs
|
||||
docs: ## Напечатать markdown-таблицу хостов и групп в stdout
|
||||
@ANSIBLE_INVENTORY_BIN='$(ANSIBLE_INVENTORY_BIN)' $(PYTHON) scripts/gen-inventory-docs.py
|
||||
|
||||
.PHONY: inventory
|
||||
inventory: ## Показать дерево инвентаря (ansible-inventory --graph)
|
||||
@$(INVENTORY_ENV) $(ANSIBLE_INVENTORY_BIN) -i inventory/hosts.yml --graph
|
||||
|
||||
.PHONY: openvpn-check
|
||||
openvpn-check: ## Проверить OpenVPN-транспорт ru-vps <-> ovpn-mini
|
||||
$(ANSIBLE_PLAYBOOK) playbooks/openvpn-check.yml $(EXTRA)
|
||||
|
||||
.PHONY: backup-audit
|
||||
backup-audit: ## Аудит свежести бэкапов PBS и offsite restic
|
||||
$(ANSIBLE_PLAYBOOK) playbooks/backup-audit.yml $(EXTRA)
|
||||
|
||||
dry-%: ## Прогон playbooks/pve-<имя>.yml в режиме --check --diff (пример: make dry-gitea)
|
||||
@$(REQUIRE_ENV); $(ANSIBLE_PLAYBOOK) playbooks/pve-$*.yml --check --diff $(EXTRA)
|
||||
|
||||
##@ Деплой LXC/VM через Proxmox API (требует .env)
|
||||
|
||||
deploy-%: ## Применить playbooks/pve-<имя>.yml (пример: make deploy-gitea, deploy-adguard, deploy-monitoring)
|
||||
@$(REQUIRE_ENV); $(ANSIBLE_PLAYBOOK) playbooks/pve-$*.yml $(EXTRA)
|
||||
|
||||
.PHONY: backup-jobs
|
||||
backup-jobs: ## Настроить PBS backup jobs (playbooks/pve-backup-jobs.yml)
|
||||
@$(REQUIRE_ENV); $(ANSIBLE_PLAYBOOK) playbooks/pve-backup-jobs.yml $(EXTRA)
|
||||
|
||||
.PHONY: bootstrap-pve-token
|
||||
bootstrap-pve-token: ## Выпустить Proxmox API-токен прямо с ноды (нужен sudo)
|
||||
$(ANSIBLE_PLAYBOOK) playbooks/bootstrap-pve-api-token.yml $(ASK_BECOME) $(EXTRA)
|
||||
|
||||
.PHONY: bootstrap-monitoring-token
|
||||
bootstrap-monitoring-token: ## Выпустить read-only PVE-токен для мониторинга
|
||||
$(ANSIBLE_PLAYBOOK) playbooks/bootstrap-monitoring-pve-token.yml $(EXTRA)
|
||||
|
||||
.PHONY: bootstrap-ansible-user
|
||||
bootstrap-ansible-user: ## Завести сервисный аккаунт ansible на shell-хостах (нужен sudo)
|
||||
$(ANSIBLE_PLAYBOOK) playbooks/bootstrap-ansible-user.yml $(ASK_BECOME) $(EXTRA)
|
||||
|
||||
##@ Настройка сервисов
|
||||
|
||||
.PHONY: reverse-proxy
|
||||
reverse-proxy: ## Единый reverse-proxy для gitea/vaultwarden/grimmory
|
||||
@$(LOAD_ENV); $(ANSIBLE_PLAYBOOK) playbooks/reverse-proxy.yml $(EXTRA)
|
||||
|
||||
.PHONY: openvpn
|
||||
openvpn: ## Поднять OpenVPN-транспорт ru-vps <-> ovpn-mini (нужен sudo)
|
||||
$(ANSIBLE_PLAYBOOK) playbooks/openvpn-vps-mini.yml $(ASK_BECOME) $(EXTRA)
|
||||
|
||||
.PHONY: openvpn-laptop
|
||||
openvpn-laptop: ## Настроить OpenVPN-профиль ноутбука
|
||||
$(ANSIBLE_PLAYBOOK) playbooks/openvpn-laptop.yml $(EXTRA)
|
||||
|
||||
.PHONY: bash-config
|
||||
bash-config: ## Раскатать единый bash-конфиг на shell_hosts
|
||||
$(ANSIBLE_PLAYBOOK) playbooks/bash-config.yml $(EXTRA)
|
||||
|
||||
.PHONY: user-ssh-key
|
||||
user-ssh-key: ## Разложить публичный SSH-ключ пользователя на все хосты
|
||||
$(ANSIBLE_PLAYBOOK) playbooks/user-ssh-key.yml $(EXTRA)
|
||||
|
||||
.PHONY: emergency-access
|
||||
emergency-access: ## Настроить emergency reverse-SSH и Telegram-бота (нужны EMERGENCY_* в .env)
|
||||
@$(REQUIRE_ENV); $(ANSIBLE_PLAYBOOK) playbooks/emergency-access.yml $(EXTRA)
|
||||
|
||||
.PHONY: gyro
|
||||
gyro: ## Настроить gyro-аллокатор в CT 150 (спрашивает пароль Vault)
|
||||
@$(REQUIRE_ENV); $(ANSIBLE_PLAYBOOK) playbooks/gyro.yml $(ASK_VAULT) $(EXTRA)
|
||||
|
||||
.PHONY: uptime-kuma
|
||||
uptime-kuma: ## Развернуть/обновить Uptime Kuma на monitoring LXC
|
||||
$(ANSIBLE_PLAYBOOK) playbooks/uptime-kuma.yml $(EXTRA)
|
||||
|
||||
.PHONY: offsite-restic
|
||||
offsite-restic: ## Настроить offsite restic-бэкапы на Yandex.Disk
|
||||
@$(LOAD_ENV); $(ANSIBLE_PLAYBOOK) playbooks/offsite-restic-yadisk.yml $(EXTRA)
|
||||
|
||||
play-%: ## Запустить произвольный playbooks/<имя>.yml с загруженным .env (пример: make play-check)
|
||||
@$(LOAD_ENV); $(ANSIBLE_PLAYBOOK) playbooks/$*.yml $(EXTRA)
|
||||
|
||||
##@ Обновления сервисов (сначала свежий бэкап, потом апдейт, потом проверка)
|
||||
|
||||
update-%: ## Обновить сервис через playbooks/<имя>-update.yml (gitea, vaultwarden, adguard, mihomo, grimmory)
|
||||
@$(LOAD_ENV); $(ANSIBLE_PLAYBOOK) playbooks/$*-update.yml $(EXTRA)
|
||||
|
||||
.PHONY: update-all
|
||||
update-all: ## Последовательно обновить gitea, vaultwarden, adguard, mihomo, grimmory (требует CONFIRM=1)
|
||||
@$(REQUIRE_CONFIRM)
|
||||
$(MAKE) update-vaultwarden
|
||||
$(MAKE) update-gitea
|
||||
$(MAKE) update-adguard
|
||||
$(MAKE) update-mihomo
|
||||
$(MAKE) update-grimmory
|
||||
|
||||
##@ Опасное (только осознанно, требует CONFIRM=1)
|
||||
|
||||
.PHONY: mihomo-harden
|
||||
mihomo-harden: ## РОТИРУЕТ живые SOCKS-креды Mihomo на ru-vps и убирает публичный доступ
|
||||
@$(REQUIRE_CONFIRM)
|
||||
@printf 'Ротация кредов необратима для клиентов без отката бэкапа конфига.\n' >&2
|
||||
$(ANSIBLE_PLAYBOOK) playbooks/ru-vps-mihomo-harden.yml -e ru_vps_mihomo_harden_confirm=true $(EXTRA)
|
||||
|
||||
.PHONY: monitoring
|
||||
monitoring: ## ЗАМОРОЖЕН: стек Prometheus. Запускать только при восстановлении мониторинга
|
||||
@$(REQUIRE_CONFIRM)
|
||||
@printf 'monitoring.yml заморожен, пока используется Uptime Kuma.\n' >&2
|
||||
@$(REQUIRE_ENV); $(ANSIBLE_PLAYBOOK) playbooks/monitoring.yml $(EXTRA)
|
||||
Executable
+196
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Печатает markdown-документацию по Ansible-инвентарю HomeLab в stdout.
|
||||
|
||||
Источник правды — `ansible-inventory -i inventory/hosts.yml --list`, поэтому вывод
|
||||
всегда совпадает с тем, что реально видит Ansible. Файлы не пишутся: результат
|
||||
вставляется в README/Obsidian вручную.
|
||||
|
||||
Запуск:
|
||||
make docs
|
||||
python3 scripts/gen-inventory-docs.py
|
||||
|
||||
Бинарь ansible-inventory ищется в порядке:
|
||||
1. $ANSIBLE_INVENTORY_BIN
|
||||
2. ./.venv/bin/ansible-inventory
|
||||
3. ansible-inventory из PATH
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ANSIBLE_DIR = Path(__file__).resolve().parent.parent
|
||||
INVENTORY = ANSIBLE_DIR / "inventory" / "hosts.yml"
|
||||
|
||||
# Служебные группы, которые не несут смысла в документации.
|
||||
HIDDEN_GROUPS = {"all", "ungrouped"}
|
||||
EMPTY = "—"
|
||||
|
||||
|
||||
def inventory_binaries() -> list[str]:
|
||||
candidates: list[str] = []
|
||||
from_env = os.environ.get("ANSIBLE_INVENTORY_BIN", "").strip()
|
||||
if from_env:
|
||||
candidates.append(from_env)
|
||||
candidates.append(str(ANSIBLE_DIR / ".venv" / "bin" / "ansible-inventory"))
|
||||
on_path = shutil.which("ansible-inventory")
|
||||
if on_path:
|
||||
candidates.append(on_path)
|
||||
|
||||
unique: list[str] = []
|
||||
for candidate in candidates:
|
||||
if candidate not in unique:
|
||||
unique.append(candidate)
|
||||
return unique
|
||||
|
||||
|
||||
def run_inventory(binary: str, env: dict) -> tuple[dict | None, str]:
|
||||
"""Один запуск ansible-inventory. Возвращает (данные, описание ошибки)."""
|
||||
try:
|
||||
done = subprocess.run(
|
||||
[binary, "-i", str(INVENTORY), "--list"],
|
||||
cwd=str(ANSIBLE_DIR),
|
||||
env=env,
|
||||
# stdin закрыт, чтобы ansible не ушёл в интерактивный запрос пароля Vault.
|
||||
stdin=subprocess.DEVNULL,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
except OSError as exc:
|
||||
return None, str(exc)
|
||||
|
||||
if done.returncode != 0:
|
||||
tail = (done.stderr or done.stdout or "").strip().splitlines()
|
||||
return None, tail[-1] if tail else f"код возврата {done.returncode}"
|
||||
|
||||
try:
|
||||
return json.loads(done.stdout), ""
|
||||
except json.JSONDecodeError as exc:
|
||||
return None, f"не удалось разобрать JSON ({exc})"
|
||||
|
||||
|
||||
def load_inventory() -> dict:
|
||||
base_env = dict(os.environ)
|
||||
base_env["ANSIBLE_NOCOLOR"] = "1"
|
||||
base_env.pop("ANSIBLE_INVENTORY_BIN", None)
|
||||
|
||||
# Запасной режим: host_vars/group_vars могут быть зашифрованы Ansible Vault
|
||||
# (inventory/host_vars/gyro/vault.yml). Для таблицы хостов они не нужны, поэтому
|
||||
# при отказе основного режима vars-плагины отключаются и инвентарь читается
|
||||
# только из hosts.yml.
|
||||
no_vars_env = dict(base_env)
|
||||
no_vars_env["ANSIBLE_VARS_ENABLED"] = ""
|
||||
|
||||
problems: list[str] = []
|
||||
for binary in inventory_binaries():
|
||||
data, error = run_inventory(binary, base_env)
|
||||
if data is not None:
|
||||
return data
|
||||
problems.append(f"{binary}: {error}")
|
||||
|
||||
data, fallback_error = run_inventory(binary, no_vars_env)
|
||||
if data is not None:
|
||||
sys.stderr.write(
|
||||
"ВНИМАНИЕ: host_vars/group_vars не прочитаны "
|
||||
f"({error}); таблица построена только по {INVENTORY.name}.\n"
|
||||
)
|
||||
return data
|
||||
problems.append(f"{binary} (без vars-плагинов): {fallback_error}")
|
||||
|
||||
sys.stderr.write("ОШИБКА: не удалось прочитать инвентарь.\n")
|
||||
for problem in problems or ["ansible-inventory не найден"]:
|
||||
sys.stderr.write(f" - {problem}\n")
|
||||
sys.stderr.write(
|
||||
"Установи зависимости (`make setup`), войди в nix-окружение "
|
||||
"или задай ANSIBLE_INVENTORY_BIN явно.\n"
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
def parse_groups(data: dict) -> dict[str, dict[str, list[str]]]:
|
||||
groups: dict[str, dict[str, list[str]]] = {}
|
||||
for name, body in data.items():
|
||||
if name == "_meta" or not isinstance(body, dict):
|
||||
continue
|
||||
groups[name] = {
|
||||
"hosts": list(body.get("hosts", [])),
|
||||
"children": list(body.get("children", [])),
|
||||
}
|
||||
return groups
|
||||
|
||||
|
||||
def resolve_hosts(group: str, groups: dict, seen: set[str] | None = None) -> set[str]:
|
||||
"""Все хосты группы с учётом вложенных children."""
|
||||
seen = seen or set()
|
||||
if group in seen or group not in groups:
|
||||
return set()
|
||||
seen.add(group)
|
||||
hosts = set(groups[group]["hosts"])
|
||||
for child in groups[group]["children"]:
|
||||
hosts |= resolve_hosts(child, groups, seen)
|
||||
return hosts
|
||||
|
||||
|
||||
def escape(value: object) -> str:
|
||||
text = str(value).strip()
|
||||
return text.replace("|", "\\|") if text else EMPTY
|
||||
|
||||
|
||||
def main() -> int:
|
||||
data = load_inventory()
|
||||
hostvars = data.get("_meta", {}).get("hostvars", {})
|
||||
groups = parse_groups(data)
|
||||
|
||||
resolved = {name: resolve_hosts(name, groups) for name in groups}
|
||||
|
||||
all_hosts = set(hostvars)
|
||||
for members in resolved.values():
|
||||
all_hosts |= members
|
||||
|
||||
host_groups: dict[str, list[str]] = {}
|
||||
for host in all_hosts:
|
||||
host_groups[host] = sorted(
|
||||
name
|
||||
for name, members in resolved.items()
|
||||
if host in members and name not in HIDDEN_GROUPS
|
||||
)
|
||||
|
||||
out = sys.stdout.write
|
||||
out("## Хосты\n\n")
|
||||
out("| Хост | ansible_host | expected_lan_ip | Группы |\n")
|
||||
out("|---|---|---|---|\n")
|
||||
for host in sorted(all_hosts):
|
||||
facts = hostvars.get(host, {})
|
||||
out(
|
||||
"| `{host}` | {ansible_host} | {lan_ip} | {groups} |\n".format(
|
||||
host=host,
|
||||
ansible_host=escape(facts.get("ansible_host", "")),
|
||||
lan_ip=escape(facts.get("expected_lan_ip", "")),
|
||||
groups=", ".join(f"`{g}`" for g in host_groups[host]) or EMPTY,
|
||||
)
|
||||
)
|
||||
|
||||
out("\n## Группы\n\n")
|
||||
for name in sorted(groups):
|
||||
if name in HIDDEN_GROUPS:
|
||||
continue
|
||||
members = sorted(resolved[name])
|
||||
children = sorted(groups[name]["children"])
|
||||
out(f"### `{name}` ({len(members)})\n\n")
|
||||
if children:
|
||||
out("- Вложенные группы: " + ", ".join(f"`{c}`" for c in children) + "\n")
|
||||
out("- Хосты: " + (", ".join(f"`{m}`" for m in members) or EMPTY) + "\n\n")
|
||||
|
||||
out(f"_Сгенерировано из `{INVENTORY.relative_to(ANSIBLE_DIR)}` "
|
||||
"через `make docs`._\n")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user