- Роли теперь создаются универсальными для RHEL и Debian семейств - Автоматическое создание OS-специфичных задач (debian.yml, redhat.yml) - Универсальные playbooks с поддержкой разных ОС - Обновлена документация с примерами и лучшими практиками Новые возможности: - Автоматическое определение ОС через ansible_os_family - OS-специфичные задачи в отдельных файлах - Универсальные playbooks с pre_tasks и post_tasks - Поддержка apt для Debian/Ubuntu и yum для RHEL/CentOS Структура универсальной роли: - tasks/main.yml - общая логика и включение OS-специфичных задач - tasks/debian.yml - задачи для Debian/Ubuntu (apt, systemd) - tasks/redhat.yml - задачи для RHEL/CentOS (yum, systemd) - playbooks/ - универсальные playbooks с поддержкой разных ОС Универсальные playbooks: - gather_facts: true - сбор информации об ОС - pre_tasks - отображение информации об ОС - post_tasks - проверка успешного развертывания - Переменные роли автоматически добавляются Документация: - Добавлен раздел 'Универсальные роли' в docs/roles.md - Примеры задач для Debian и RHEL семейств - Лучшие практики для универсальных ролей - Рекомендации по тестированию на разных ОС Преимущества: - Автоматическое создание универсальных ролей - Поддержка RHEL и Debian семейств из коробки - Лучшие практики встроены в шаблоны - Подробная документация с примерами - Приучение к написанию универсальных ролей Автор: Сергей Антропов Сайт: https://devops.org.ru
		
			
				
	
	
		
			833 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			Makefile
		
	
	
	
	
	
			
		
		
	
	
			833 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			Makefile
		
	
	
	
	
	
| # =============================================================================
 | ||
| # Ansible Template - Универсальная лаборатория для тестирования Ansible ролей
 | ||
| # Автор: Сергей Антропов
 | ||
| # Сайт: https://devops.org.ru
 | ||
| # =============================================================================
 | ||
| 
 | ||
| # =============================================================================
 | ||
| # ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ
 | ||
| # =============================================================================
 | ||
| 
 | ||
| # Основные переменные
 | ||
| PROJECT_NAME ?= ansible-template
 | ||
| VERSION ?= 0.1.0
 | ||
| AUTHOR ?= "Сергей Антропов"
 | ||
| SITE ?= "https://devops.org.ru"
 | ||
| 
 | ||
| # Docker переменные
 | ||
| DOCKER_IMAGE ?= quay.io/ansible/creator-ee:latest
 | ||
| DOCKER_COMPOSE ?= docker compose
 | ||
| DOCKER_NETWORK ?= labnet
 | ||
| 
 | ||
| # Molecule переменные
 | ||
| SCENARIO ?= universal
 | ||
| LAB_SPEC ?= molecule/presets/minimal.yml
 | ||
| MOLECULE_EPHEMERAL_DIRECTORY ?= /tmp/molecule
 | ||
| 
 | ||
| # Kubernetes переменные
 | ||
| KUBE_CONTEXT ?= kind-lab
 | ||
| ISTIO_VERSION ?= 1.22.1
 | ||
| KIND_VERSION ?= v0.23.0
 | ||
| 
 | ||
| # Переменные окружения
 | ||
| ENV_FILE ?= .env
 | ||
| ROLES_DIR ?= ./roles
 | ||
| VAULT_PASSWORD_FILE ?= vault/.vault
 | ||
| 
 | ||
| # Цвета для вывода
 | ||
| RED := \033[0;31m
 | ||
| GREEN := \033[0;32m
 | ||
| YELLOW := \033[0;33m
 | ||
| BLUE := \033[0;34m
 | ||
| PURPLE := \033[0;35m
 | ||
| CYAN := \033[0;36m
 | ||
| WHITE := \033[0;37m
 | ||
| RESET := \033[0m
 | ||
| 
 | ||
| # =============================================================================
 | ||
| # ОСНОВНЫЕ КОМАНДЫ
 | ||
| # =============================================================================
 | ||
| 
 | ||
| .PHONY: help
 | ||
| help: ## Показать справку по всем командам
 | ||
| 	@echo "$(CYAN)Ansible Template - Универсальная лаборатория$(RESET)"
 | ||
| 	@echo "$(YELLOW)Автор: $(AUTHOR)$(RESET)"
 | ||
| 	@echo "$(YELLOW)Сайт: $(SITE)$(RESET)"
 | ||
| 	@echo ""
 | ||
| 	@echo "$(GREEN)Доступные команды:$(RESET)"
 | ||
| 	@echo "  $(CYAN)make lab <команда>$(RESET)       - Управление лабораторией (up|down|test|destroy)"
 | ||
| 	@echo "  $(CYAN)make kube <команда>$(RESET)      - Управление Kubernetes (logs|exec|port-forward)"
 | ||
| 	@echo "  $(CYAN)make preset <команда>$(RESET)    - Управление пресетами (list|create|test)"
 | ||
| 	@echo "  $(CYAN)make role <команда>$(RESET)      - Управление ролями (list|create|test|lint|playbook)"
 | ||
| 	@echo "  $(CYAN)make vault <команда>$(RESET)     - Управление vault (view|create|edit|encrypt)"
 | ||
| 	@echo "  $(CYAN)make report$(RESET)              - Генерация HTML отчета"
 | ||
| 	@echo "  $(CYAN)make lint$(RESET)                - Проверка синтаксиса"
 | ||
| 	@echo "  $(CYAN)make snapshot$(RESET)            - Создание снимка лаборатории"
 | ||
| 	@echo "  $(CYAN)make restore$(RESET)             - Восстановление из снимка"
 | ||
| 	@echo "  $(CYAN)make cleanup$(RESET)             - Очистка всех данных"
 | ||
| 	@echo ""
 | ||
| 	@echo "$(YELLOW)Подробная документация: docs/$(RESET)"
 | ||
| 
 | ||
| # =============================================================================
 | ||
| # ИНИЦИАЛИЗАЦИЯ И НАСТРОЙКА
 | ||
| # =============================================================================
 | ||
| 
 | ||
| .PHONY: init
 | ||
| init: setup-env setup-vault setup-roles setup-precommit ## Полная инициализация проекта
 | ||
| 	@echo "$(GREEN)✅ Проект успешно инициализирован!$(RESET)"
 | ||
| 	@echo "$(YELLOW)📖 Документация: $(SITE)$(RESET)"
 | ||
| 	@echo "$(YELLOW)🚀 Быстрый старт: make lab up$(RESET)"
 | ||
| 
 | ||
| .PHONY: setup-env
 | ||
| setup-env: ## Создать .env файл с настройками
 | ||
| 	@if [ ! -f $(ENV_FILE) ]; then \
 | ||
| 		echo "$(YELLOW)Создание .env файла...$(RESET)"; \
 | ||
| 		echo "# Ansible Template Environment" > $(ENV_FILE); \
 | ||
| 		echo "PROJECT_NAME=$(PROJECT_NAME)" >> $(ENV_FILE); \
 | ||
| 		echo "VERSION=$(VERSION)" >> $(ENV_FILE); \
 | ||
| 		echo "AUTHOR=$(AUTHOR)" >> $(ENV_FILE); \
 | ||
| 		echo "SITE=$(SITE)" >> $(ENV_FILE); \
 | ||
| 		echo "" >> $(ENV_FILE); \
 | ||
| 		echo "# Docker settings" >> $(ENV_FILE); \
 | ||
| 		echo "DOCKER_IMAGE=$(DOCKER_IMAGE)" >> $(ENV_FILE); \
 | ||
| 		echo "DOCKER_NETWORK=$(DOCKER_NETWORK)" >> $(ENV_FILE); \
 | ||
| 		echo "" >> $(ENV_FILE); \
 | ||
| 		echo "# Molecule settings" >> $(ENV_FILE); \
 | ||
| 		echo "SCENARIO=$(SCENARIO)" >> $(ENV_FILE); \
 | ||
| 		echo "LAB_SPEC=$(LAB_SPEC)" >> $(ENV_FILE); \
 | ||
| 		echo "" >> $(ENV_FILE); \
 | ||
| 		echo "# Kubernetes settings" >> $(ENV_FILE); \
 | ||
| 		echo "KUBE_CONTEXT=$(KUBE_CONTEXT)" >> $(ENV_FILE); \
 | ||
| 		echo "ISTIO_VERSION=$(ISTIO_VERSION)" >> $(ENV_FILE); \
 | ||
| 		echo "KIND_VERSION=$(KIND_VERSION)" >> $(ENV_FILE); \
 | ||
| 		echo "" >> $(ENV_FILE); \
 | ||
| 		echo "# Paths" >> $(ENV_FILE); \
 | ||
| 		echo "ROLES_DIR=$(ROLES_DIR)" >> $(ENV_FILE); \
 | ||
| 		echo "VAULT_PASSWORD_FILE=$(VAULT_PASSWORD_FILE)" >> $(ENV_FILE); \
 | ||
| 		echo "$(GREEN)✅ .env файл создан$(RESET)"; \
 | ||
| 	else \
 | ||
| 		echo "$(YELLOW)⚠️  .env файл уже существует$(RESET)"; \
 | ||
| 	fi
 | ||
| 
 | ||
| .PHONY: setup-vault
 | ||
| setup-vault: ## Создать vault-password.txt
 | ||
| 	@if [ ! -f $(VAULT_PASSWORD_FILE) ]; then \
 | ||
| 		echo "$(YELLOW)Создание vault-password.txt...$(RESET)"; \
 | ||
| 		echo "ansible-vault-password" > $(VAULT_PASSWORD_FILE); \
 | ||
| 		echo "$(GREEN)✅ vault-password.txt создан$(RESET)"; \
 | ||
| 	else \
 | ||
| 		echo "$(YELLOW)⚠️  vault-password.txt уже существует$(RESET)"; \
 | ||
| 	fi
 | ||
| 
 | ||
| .PHONY: setup-roles
 | ||
| setup-roles: ## Создать директорию для ролей
 | ||
| 	@mkdir -p $(ROLES_DIR)
 | ||
| 	@echo "$(GREEN)✅ Директория ролей создана: $(ROLES_DIR)$(RESET)"
 | ||
| 
 | ||
| .PHONY: setup-precommit
 | ||
| setup-precommit: ## Установить pre-commit хуки
 | ||
| 	@if command -v pre-commit >/dev/null 2>&1; then \
 | ||
| 		pre-commit install; \
 | ||
| 		echo "$(GREEN)✅ Pre-commit хуки установлены$(RESET)"; \
 | ||
| 	else \
 | ||
| 		echo "$(YELLOW)⚠️  pre-commit не установлен. Установите: pip install pre-commit$(RESET)"; \
 | ||
| 	fi
 | ||
| 
 | ||
| # =============================================================================
 | ||
| # УПРАВЛЕНИЕ ЛАБОРАТОРИЕЙ
 | ||
| # =============================================================================
 | ||
| 
 | ||
| .PHONY: lab
 | ||
| lab: ## Управление лабораторией (up|down|sh|test|create|converge|verify|destroy|reset)
 | ||
| 	@case "$(word 2, $(MAKECMDGOALS))" in \
 | ||
| 		up) \
 | ||
| 			echo "$(GREEN)🚀 Поднимаем контроллер...$(RESET)"; \
 | ||
| 			$(DOCKER_COMPOSE) up -d; \
 | ||
| 			echo "$(GREEN)✅ Контроллер запущен$(RESET)";; \
 | ||
| 		down) \
 | ||
| 			echo "$(YELLOW)🛑 Останавливаем контроллер...$(RESET)"; \
 | ||
| 			$(DOCKER_COMPOSE) down -v; \
 | ||
| 			echo "$(GREEN)✅ Контроллер остановлен$(RESET)";; \
 | ||
| 		sh) \
 | ||
| 			echo "$(BLUE)🐚 Входим в контроллер...$(RESET)"; \
 | ||
| 			docker exec -it ansible-controller bash;; \
 | ||
| 		test) \
 | ||
| 			echo "$(PURPLE)🧪 Запускаем полный цикл тестирования...$(RESET)"; \
 | ||
| 			$(MAKE) lab up; \
 | ||
| 			docker exec -e MOLECULE_EPHEMERAL_DIRECTORY=$(MOLECULE_EPHEMERAL_DIRECTORY) ansible-controller \
 | ||
| 				bash -lc 'cd /ansible && molecule test -s $(SCENARIO)'; \
 | ||
| 			echo "$(GREEN)✅ Тестирование завершено$(RESET)";; \
 | ||
| 		create) \
 | ||
| 			echo "$(BLUE)🏗️  Создаем инфраструктуру...$(RESET)"; \
 | ||
| 			$(MAKE) lab up; \
 | ||
| 			docker exec -e MOLECULE_EPHEMERAL_DIRECTORY=$(MOLECULE_EPHEMERAL_DIRECTORY) ansible-controller \
 | ||
| 				bash -lc 'cd /ansible && molecule create -s $(SCENARIO)'; \
 | ||
| 			echo "$(GREEN)✅ Инфраструктура создана$(RESET)";; \
 | ||
| 		converge) \
 | ||
| 			echo "$(YELLOW)⚙️  Запускаем роли...$(RESET)"; \
 | ||
| 			docker exec -e MOLECULE_EPHEMERAL_DIRECTORY=$(MOLECULE_EPHEMERAL_DIRECTORY) ansible-controller \
 | ||
| 				bash -lc 'cd /ansible && molecule converge -s $(SCENARIO)'; \
 | ||
| 			echo "$(GREEN)✅ Роли выполнены$(RESET)";; \
 | ||
| 		verify) \
 | ||
| 			echo "$(CYAN)🔍 Проверяем работу лаборатории...$(RESET)"; \
 | ||
| 			docker exec -e MOLECULE_EPHEMERAL_DIRECTORY=$(MOLECULE_EPHEMERAL_DIRECTORY) ansible-controller \
 | ||
| 				bash -lc 'cd /ansible && molecule verify -s $(SCENARIO)'; \
 | ||
| 			echo "$(GREEN)✅ Проверка завершена$(RESET)";; \
 | ||
| 		destroy) \
 | ||
| 			echo "$(RED)💥 Уничтожаем инфраструктуру...$(RESET)"; \
 | ||
| 			docker exec -e MOLECULE_EPHEMERAL_DIRECTORY=$(MOLECULE_EPHEMERAL_DIRECTORY) ansible-controller \
 | ||
| 				bash -lc 'cd /ansible && molecule destroy -s $(SCENARIO)'; \
 | ||
| 			echo "$(GREEN)✅ Инфраструктура уничтожена$(RESET)";; \
 | ||
| 		reset) \
 | ||
| 			echo "$(PURPLE)🔄 Полный сброс лаборатории...$(RESET)"; \
 | ||
| 			$(MAKE) lab destroy; \
 | ||
| 			$(MAKE) lab down; \
 | ||
| 			$(MAKE) lab up; \
 | ||
| 			echo "$(GREEN)✅ Лаборатория сброшена$(RESET)";; \
 | ||
| 		*) \
 | ||
| 			echo "$(RED)❌ Неизвестная команда. Доступные: up, down, sh, test, create, converge, verify, destroy, reset$(RESET)";; \
 | ||
| 	esac
 | ||
| 
 | ||
| # =============================================================================
 | ||
| # УПРАВЛЕНИЕ KUBERNETES
 | ||
| # =============================================================================
 | ||
| 
 | ||
| .PHONY: kube
 | ||
| kube: ## Управление Kubernetes (sh|cmd|enter|kiali|istio|grafana|prom|pf-stop)
 | ||
| 	@case "$(word 2, $(MAKECMDGOALS))" in \
 | ||
| 		sh) \
 | ||
| 			echo "$(BLUE)🐚 Входим в контейнер с kubectl...$(RESET)"; \
 | ||
| 			docker exec -it ansible-controller bash;; \
 | ||
| 		cmd) \
 | ||
| 			if [ -z "$(CLUSTER)" ]; then \
 | ||
| 				echo "$(RED)❌ Использование: make kube cmd CLUSTER=lab CMD=\"get pods -A\"$(RESET)"; \
 | ||
| 				exit 1; \
 | ||
| 			fi; \
 | ||
| 			echo "$(CYAN)🔧 Выполняем kubectl команду...$(RESET)"; \
 | ||
| 			docker exec -it ansible-controller bash -lc 'kubectl --context kind-$(CLUSTER) $(CMD)';; \
 | ||
| 		enter) \
 | ||
| 			if [ -z "$(CLUSTER)" ]; then \
 | ||
| 				echo "$(RED)❌ Использование: make kube enter CLUSTER=lab$(RESET)"; \
 | ||
| 				exit 1; \
 | ||
| 			fi; \
 | ||
| 			echo "$(BLUE)🚪 Входим в кластер...$(RESET)"; \
 | ||
| 			docker exec -it ansible-controller bash -lc '\
 | ||
| 				POD=$$(kubectl --context kind-$(CLUSTER) -n lab-demo get pod -l app=toolbox -o jsonpath="{.items[0].metadata.name}"); \
 | ||
| 				[ -n "$$POD" ] || { echo "toolbox pod not found"; exit 1; }; \
 | ||
| 				kubectl --context kind-$(CLUSTER) -n lab-demo exec -it $$POD -- /bin/sh';; \
 | ||
| 		kiali) \
 | ||
| 			if [ -z "$(CLUSTER)" ]; then \
 | ||
| 				echo "$(RED)❌ Использование: make kube kiali CLUSTER=lab$(RESET)"; \
 | ||
| 				exit 1; \
 | ||
| 			fi; \
 | ||
| 			echo "$(PURPLE)🔮 Port-forward Kiali...$(RESET)"; \
 | ||
| 			docker exec -d ansible-controller bash -lc 'kubectl --context kind-$(CLUSTER) -n istio-system port-forward svc/kiali 20001:20001'; \
 | ||
| 			echo "$(GREEN)✅ Kiali: http://localhost:20001$(RESET)";; \
 | ||
| 		istio) \
 | ||
| 			if [ -z "$(CLUSTER)" ]; then \
 | ||
| 				echo "$(RED)❌ Использование: make kube istio CLUSTER=lab$(RESET)"; \
 | ||
| 				exit 1; \
 | ||
| 			fi; \
 | ||
| 			echo "$(PURPLE)🌐 Port-forward Istio Gateway...$(RESET)"; \
 | ||
| 			docker exec -d ansible-controller bash -lc 'kubectl --context kind-$(CLUSTER) -n istio-system port-forward svc/istio-ingressgateway 8082:80 8444:443'; \
 | ||
| 			echo "$(GREEN)✅ Istio GW: http://localhost:8082  https://localhost:8444$(RESET)";; \
 | ||
| 		grafana) \
 | ||
| 			if [ -z "$(CLUSTER)" ]; then \
 | ||
| 				echo "$(RED)❌ Использование: make kube grafana CLUSTER=lab$(RESET)"; \
 | ||
| 				exit 1; \
 | ||
| 			fi; \
 | ||
| 			echo "$(BLUE)📊 Port-forward Grafana...$(RESET)"; \
 | ||
| 			docker exec -d ansible-controller bash -lc 'kubectl --context kind-$(CLUSTER) -n monitoring port-forward svc/monitoring-grafana 3000:80'; \
 | ||
| 			echo "$(GREEN)✅ Grafana: http://localhost:3000 (admin/admin)$(RESET)";; \
 | ||
| 		prom) \
 | ||
| 			if [ -z "$(CLUSTER)" ]; then \
 | ||
| 				echo "$(RED)❌ Использование: make kube prom CLUSTER=lab$(RESET)"; \
 | ||
| 				exit 1; \
 | ||
| 			fi; \
 | ||
| 			echo "$(YELLOW)📈 Port-forward Prometheus...$(RESET)"; \
 | ||
| 			docker exec -d ansible-controller bash -lc 'kubectl --context kind-$(CLUSTER) -n monitoring port-forward svc/monitoring-kube-prometheus-prometheus 9090:9090'; \
 | ||
| 			echo "$(GREEN)✅ Prometheus: http://localhost:9090$(RESET)";; \
 | ||
| 		pf-stop) \
 | ||
| 			echo "$(RED)🛑 Останавливаем все port-forward...$(RESET)"; \
 | ||
| 			docker exec -it ansible-controller bash -lc 'pkill -f "kubectl .* port-forward" || true'; \
 | ||
| 			echo "$(GREEN)✅ Port-forward остановлены$(RESET)";; \
 | ||
| 		kubeconfig) \
 | ||
| 			if [ -z "$(CLUSTER)" ]; then \
 | ||
| 				echo "$(RED)❌ Использование: make kube kubeconfig CLUSTER=lab$(RESET)"; \
 | ||
| 				exit 1; \
 | ||
| 			fi; \
 | ||
| 			echo "$(BLUE)📋 Получаем kubeconfig для кластера $(CLUSTER)...$(RESET)"; \
 | ||
| 			mkdir -p reports/kubeconfigs; \
 | ||
| 			docker exec ansible-controller bash -lc 'kubectl --context kind-$(CLUSTER) config view --raw' > reports/kubeconfigs/kubeconfig-$(CLUSTER).yaml; \
 | ||
| 			echo "$(GREEN)✅ Kubeconfig сохранен: reports/kubeconfigs/kubeconfig-$(CLUSTER).yaml$(RESET)";; \
 | ||
| 		*) \
 | ||
| 			echo "$(RED)❌ Неизвестная команда. Доступные: sh, cmd, enter, kiali, istio, grafana, prom, pf-stop, kubeconfig$(RESET)";; \
 | ||
| 	esac
 | ||
| 
 | ||
| # =============================================================================
 | ||
| # УПРАВЛЕНИЕ ПРЕСЕТАМИ
 | ||
| # =============================================================================
 | ||
| 
 | ||
| .PHONY: preset
 | ||
| preset: ## Управление пресетами (list|create|edit|test|copy)
 | ||
| 	@case "$(word 2, $(MAKECMDGOALS))" in \
 | ||
| 		list) \
 | ||
| 			echo "$(CYAN)📋 Доступные пресеты:$(RESET)"; \
 | ||
| 			echo ""; \
 | ||
| 			echo "$(GREEN)Классические пресеты:$(RESET)"; \
 | ||
| 			echo "  minimal.yml       - Минимальная лаборатория (1-3 машины)"; \
 | ||
| 			echo "  webapp.yml        - Веб-приложение (3-5 машин)"; \
 | ||
| 			echo "  microservices.yml - Микросервисы (5-8 машин)"; \
 | ||
| 			echo "  ha.yml            - Высокая доступность (6-10 машин)"; \
 | ||
| 			echo "  k8s-cluster.yml   - Kubernetes кластер (8-12 машин)"; \
 | ||
| 			echo "  cicd.yml          - CI/CD пайплайн (10-15 машин)"; \
 | ||
| 			echo "  bigdata.yml       - Big Data кластер (12-18 машин)"; \
 | ||
| 			echo "  servicemesh.yml   - Service Mesh (15-20 машин)"; \
 | ||
| 			echo "  enterprise.yml    - Enterprise (18-20 машин)"; \
 | ||
| 			echo "  maximum.yml       - Максимальный (20 машин)"; \
 | ||
| 			echo ""; \
 | ||
| 			echo "$(GREEN)Kubernetes пресеты:$(RESET)"; \
 | ||
| 			echo "  k8s-single.yml    - Kubernetes Single Node"; \
 | ||
| 			echo "  k8s-multi.yml     - Kubernetes Multi-Cluster"; \
 | ||
| 			echo "  k8s-istio-full.yml - Kubernetes + Istio Full Stack"; \
 | ||
| 			echo ""; \
 | ||
| 			echo "$(GREEN)DinD пресеты:$(RESET)"; \
 | ||
| 			echo "  dind-simple.yml   - DinD Simple"; \
 | ||
| 			echo "  dind-swarm.yml    - DinD Swarm"; \
 | ||
| 			echo "  dind-compose.yml  - DinD Compose"; \
 | ||
| 			echo ""; \
 | ||
| 			echo "$(GREEN)DOoD пресеты:$(RESET)"; \
 | ||
| 			echo "  dood-simple.yml   - DOoD Simple"; \
 | ||
| 			echo "  dood-mixed.yml    - DOoD Mixed"; \
 | ||
| 			echo ""; \
 | ||
| 			echo "$(GREEN)Смешанные пресеты:$(RESET)"; \
 | ||
| 			echo "  mixed-k8s-dind.yml - Mixed Kubernetes + DinD"; \
 | ||
| 			echo "  mixed-k8s-dood.yml - Mixed Kubernetes + DOoD"; \
 | ||
| 			echo "  mixed-full.yml     - Mixed Full Stack";; \
 | ||
| 		create) \
 | ||
| 			if [ -z "$(NAME)" ]; then \
 | ||
| 				echo "$(RED)❌ Использование: make preset create NAME=my-preset$(RESET)"; \
 | ||
| 				exit 1; \
 | ||
| 			fi; \
 | ||
| 			echo "$(YELLOW)📝 Создаем пресет: $(NAME).yml$(RESET)"; \
 | ||
| 			$(MAKE) preset copy SOURCE=minimal.yml TARGET=$(NAME).yml; \
 | ||
| 			echo "$(GREEN)✅ Пресет создан: molecule/presets/$(NAME).yml$(RESET)";; \
 | ||
| 		edit) \
 | ||
| 			if [ -z "$(NAME)" ]; then \
 | ||
| 				echo "$(RED)❌ Использование: make preset edit NAME=my-preset$(RESET)"; \
 | ||
| 				exit 1; \
 | ||
| 			fi; \
 | ||
| 			echo "$(BLUE)✏️  Редактируем пресет: $(NAME).yml$(RESET)"; \
 | ||
| 			$${EDITOR:-vim} molecule/presets/$(NAME).yml;; \
 | ||
| 		test) \
 | ||
| 			if [ -z "$(NAME)" ]; then \
 | ||
| 				echo "$(RED)❌ Использование: make preset test NAME=my-preset$(RESET)"; \
 | ||
| 				exit 1; \
 | ||
| 			fi; \
 | ||
| 			echo "$(PURPLE)🧪 Тестируем пресет: $(NAME).yml$(RESET)"; \
 | ||
| 			$(MAKE) lab test LAB_SPEC=molecule/presets/$(NAME).yml;; \
 | ||
| 		copy) \
 | ||
| 			if [ -z "$(SOURCE)" ] || [ -z "$(TARGET)" ]; then \
 | ||
| 				echo "$(RED)❌ Использование: make preset copy SOURCE=minimal.yml TARGET=my-preset.yml$(RESET)"; \
 | ||
| 				exit 1; \
 | ||
| 			fi; \
 | ||
| 			echo "$(YELLOW)📋 Копируем пресет: $(SOURCE) -> $(TARGET)$(RESET)"; \
 | ||
| 			cp molecule/presets/$(SOURCE) molecule/presets/$(TARGET); \
 | ||
| 			echo "$(GREEN)✅ Пресет скопирован$(RESET)";; \
 | ||
| 		*) \
 | ||
| 			echo "$(RED)❌ Неизвестная команда. Доступные: list, create, edit, test, copy$(RESET)";; \
 | ||
| 	esac
 | ||
| 
 | ||
| # =============================================================================
 | ||
| # УПРАВЛЕНИЕ РОЛЯМИ
 | ||
| # =============================================================================
 | ||
| 
 | ||
| .PHONY: role
 | ||
| role: ## Управление ролями (list|create|edit|test|lint|deploy)
 | ||
| 	@case "$(word 2, $(MAKECMDGOALS))" in \
 | ||
| 		list) \
 | ||
| 			echo "$(CYAN)📋 Доступные роли:$(RESET)"; \
 | ||
| 			if [ -d "$(ROLES_DIR)" ]; then \
 | ||
| 				ls -la $(ROLES_DIR)/ | grep "^d" | awk '{print "  " $$9}' | grep -v "^\.$\|^\.\.$"; \
 | ||
| 			else \
 | ||
| 				echo "  $(YELLOW)Директория ролей не найдена$(RESET)"; \
 | ||
| 			fi;; \
 | ||
| 		create) \
 | ||
| 			if [ -z "$(NAME)" ]; then \
 | ||
| 				echo "$(RED)❌ Использование: make role create NAME=my-role$(RESET)"; \
 | ||
| 				exit 1; \
 | ||
| 			fi; \
 | ||
| 			echo "$(YELLOW)📝 Создаем роль: $(NAME)$(RESET)"; \
 | ||
| 			echo "$(BLUE)🔧 Настройка роли...$(RESET)"; \
 | ||
| 			read -p "$(YELLOW)📝 Описание роли: $(RESET)" ROLE_DESC; \
 | ||
| 			read -p "$(YELLOW)📦 Основной пакет (по умолчанию: $(NAME)): $(RESET)" ROLE_PACKAGE; \
 | ||
| 			ROLE_PACKAGE=$${ROLE_PACKAGE:-$(NAME)}; \
 | ||
| 			read -p "$(YELLOW)🔧 Сервис (по умолчанию: $(NAME)): $(RESET)" ROLE_SERVICE; \
 | ||
| 			ROLE_SERVICE=$${ROLE_SERVICE:-$(NAME)}; \
 | ||
| 			read -p "$(YELLOW)📋 Платформы (ubuntu,centos,rhel) через запятую: $(RESET)" ROLE_PLATFORMS; \
 | ||
| 			ROLE_PLATFORMS=$${ROLE_PLATFORMS:-ubuntu,centos}; \
 | ||
| 			read -p "$(YELLOW)🏷️  Теги через запятую: $(RESET)" ROLE_TAGS; \
 | ||
| 			ROLE_TAGS=$${ROLE_TAGS:-$(NAME)}; \
 | ||
| 			echo "$(BLUE)📁 Создаем структуру роли...$(RESET)"; \
 | ||
| 			mkdir -p $(ROLES_DIR)/$(NAME)/{tasks,handlers,templates,files,vars,defaults,meta,tests,playbooks}; \
 | ||
| 			echo "$(BLUE)📝 Создаем основные файлы...$(RESET)"; \
 | ||
| 			echo "---" > $(ROLES_DIR)/$(NAME)/tasks/main.yml; \
 | ||
| 			echo "# Основные задачи роли $(NAME)" >> $(ROLES_DIR)/$(NAME)/tasks/main.yml; \
 | ||
| 			echo "# Автор: $(AUTHOR)" >> $(ROLES_DIR)/$(NAME)/tasks/main.yml; \
 | ||
| 			echo "# Сайт: $(SITE)" >> $(ROLES_DIR)/$(NAME)/tasks/main.yml; \
 | ||
| 			echo "" >> $(ROLES_DIR)/$(NAME)/tasks/main.yml; \
 | ||
| 			echo "- name: $(NAME) placeholder" >> $(ROLES_DIR)/$(NAME)/tasks/main.yml; \
 | ||
| 			echo "  debug:" >> $(ROLES_DIR)/$(NAME)/tasks/main.yml; \
 | ||
| 			echo "    msg: \"Роль $(NAME) готова для настройки\"" >> $(ROLES_DIR)/$(NAME)/tasks/main.yml; \
 | ||
| 			echo "" >> $(ROLES_DIR)/$(NAME)/tasks/main.yml; \
 | ||
| 			echo "- name: Include OS-specific tasks" >> $(ROLES_DIR)/$(NAME)/tasks/main.yml; \
 | ||
| 			echo "  include_tasks: \"{{ ansible_os_family | lower }}.yml\"" >> $(ROLES_DIR)/$(NAME)/tasks/main.yml; \
 | ||
| 			echo "" >> $(ROLES_DIR)/$(NAME)/tasks/main.yml; \
 | ||
| 			echo "- name: Start $(NAME) service" >> $(ROLES_DIR)/$(NAME)/tasks/main.yml; \
 | ||
| 			echo "  systemd:" >> $(ROLES_DIR)/$(NAME)/tasks/main.yml; \
 | ||
| 			echo "    name: \"{{ $(NAME)_service | default('$(ROLE_SERVICE)') }}\"" >> $(ROLES_DIR)/$(NAME)/tasks/main.yml; \
 | ||
| 			echo "    state: started" >> $(ROLES_DIR)/$(NAME)/tasks/main.yml; \
 | ||
| 			echo "    enabled: true" >> $(ROLES_DIR)/$(NAME)/tasks/main.yml; \
 | ||
| 			echo "  when: $(NAME)_service is defined" >> $(ROLES_DIR)/$(NAME)/tasks/main.yml; \
 | ||
| 			echo "---" > $(ROLES_DIR)/$(NAME)/tasks/debian.yml; \
 | ||
| 			echo "# Задачи для Debian/Ubuntu семейства" >> $(ROLES_DIR)/$(NAME)/tasks/debian.yml; \
 | ||
| 			echo "# Автор: $(AUTHOR)" >> $(ROLES_DIR)/$(NAME)/tasks/debian.yml; \
 | ||
| 			echo "# Сайт: $(SITE)" >> $(ROLES_DIR)/$(NAME)/tasks/debian.yml; \
 | ||
| 			echo "" >> $(ROLES_DIR)/$(NAME)/tasks/debian.yml; \
 | ||
| 			echo "- name: Update apt cache (Debian)" >> $(ROLES_DIR)/$(NAME)/tasks/debian.yml; \
 | ||
| 			echo "  apt:" >> $(ROLES_DIR)/$(NAME)/tasks/debian.yml; \
 | ||
| 			echo "    update_cache: true" >> $(ROLES_DIR)/$(NAME)/tasks/debian.yml; \
 | ||
| 			echo "    cache_valid_time: 3600" >> $(ROLES_DIR)/$(NAME)/tasks/debian.yml; \
 | ||
| 			echo "  when: ansible_os_family == 'Debian'" >> $(ROLES_DIR)/$(NAME)/tasks/debian.yml; \
 | ||
| 			echo "" >> $(ROLES_DIR)/$(NAME)/tasks/debian.yml; \
 | ||
| 			echo "- name: Install $(NAME) package (Debian)" >> $(ROLES_DIR)/$(NAME)/tasks/debian.yml; \
 | ||
| 			echo "  apt:" >> $(ROLES_DIR)/$(NAME)/tasks/debian.yml; \
 | ||
| 			echo "    name: \"{{ $(NAME)_package | default('$(ROLE_PACKAGE)') }}\"" >> $(ROLES_DIR)/$(NAME)/tasks/debian.yml; \
 | ||
| 			echo "    state: present" >> $(ROLES_DIR)/$(NAME)/tasks/debian.yml; \
 | ||
| 			echo "  when: ansible_os_family == 'Debian'" >> $(ROLES_DIR)/$(NAME)/tasks/debian.yml; \
 | ||
| 			echo "---" > $(ROLES_DIR)/$(NAME)/tasks/redhat.yml; \
 | ||
| 			echo "# Задачи для RHEL/CentOS семейства" >> $(ROLES_DIR)/$(NAME)/tasks/redhat.yml; \
 | ||
| 			echo "# Автор: $(AUTHOR)" >> $(ROLES_DIR)/$(NAME)/tasks/redhat.yml; \
 | ||
| 			echo "# Сайт: $(SITE)" >> $(ROLES_DIR)/$(NAME)/tasks/redhat.yml; \
 | ||
| 			echo "" >> $(ROLES_DIR)/$(NAME)/tasks/redhat.yml; \
 | ||
| 			echo "- name: Update yum cache (RHEL)" >> $(ROLES_DIR)/$(NAME)/tasks/redhat.yml; \
 | ||
| 			echo "  yum:" >> $(ROLES_DIR)/$(NAME)/tasks/redhat.yml; \
 | ||
| 			echo "    update_cache: true" >> $(ROLES_DIR)/$(NAME)/tasks/redhat.yml; \
 | ||
| 			echo "  when: ansible_os_family == 'RedHat'" >> $(ROLES_DIR)/$(NAME)/tasks/redhat.yml; \
 | ||
| 			echo "" >> $(ROLES_DIR)/$(NAME)/tasks/redhat.yml; \
 | ||
| 			echo "- name: Install $(NAME) package (RHEL)" >> $(ROLES_DIR)/$(NAME)/tasks/redhat.yml; \
 | ||
| 			echo "  yum:" >> $(ROLES_DIR)/$(NAME)/tasks/redhat.yml; \
 | ||
| 			echo "    name: \"{{ $(NAME)_package | default('$(ROLE_PACKAGE)') }}\"" >> $(ROLES_DIR)/$(NAME)/tasks/redhat.yml; \
 | ||
| 			echo "    state: present" >> $(ROLES_DIR)/$(NAME)/tasks/redhat.yml; \
 | ||
| 			echo "  when: ansible_os_family == 'RedHat'" >> $(ROLES_DIR)/$(NAME)/tasks/redhat.yml; \
 | ||
| 			echo "---" > $(ROLES_DIR)/$(NAME)/defaults/main.yml; \
 | ||
| 			echo "# Переменные по умолчанию для роли $(NAME)" >> $(ROLES_DIR)/$(NAME)/defaults/main.yml; \
 | ||
| 			echo "# Автор: $(AUTHOR)" >> $(ROLES_DIR)/$(NAME)/defaults/main.yml; \
 | ||
| 			echo "# Сайт: $(SITE)" >> $(ROLES_DIR)/$(NAME)/defaults/main.yml; \
 | ||
| 			echo "" >> $(ROLES_DIR)/$(NAME)/defaults/main.yml; \
 | ||
| 			echo "$(NAME)_enabled: true" >> $(ROLES_DIR)/$(NAME)/defaults/main.yml; \
 | ||
| 			echo "$(NAME)_package: $(ROLE_PACKAGE)" >> $(ROLES_DIR)/$(NAME)/defaults/main.yml; \
 | ||
| 			echo "$(NAME)_service: $(ROLE_SERVICE)" >> $(ROLES_DIR)/$(NAME)/defaults/main.yml; \
 | ||
| 			echo "---" > $(ROLES_DIR)/$(NAME)/meta/main.yml; \
 | ||
| 			echo "galaxy_info:" >> $(ROLES_DIR)/$(NAME)/meta/main.yml; \
 | ||
| 			echo "  author: $(AUTHOR)" >> $(ROLES_DIR)/$(NAME)/meta/main.yml; \
 | ||
| 			echo "  description: $(ROLE_DESC)" >> $(ROLES_DIR)/$(NAME)/meta/main.yml; \
 | ||
| 			echo "  company: $(SITE)" >> $(ROLES_DIR)/$(NAME)/meta/main.yml; \
 | ||
| 			echo "  license: MIT" >> $(ROLES_DIR)/$(NAME)/meta/main.yml; \
 | ||
| 			echo "  min_ansible_version: 2.9" >> $(ROLES_DIR)/$(NAME)/meta/main.yml; \
 | ||
| 			echo "  platforms:" >> $(ROLES_DIR)/$(NAME)/meta/main.yml; \
 | ||
| 			echo "    - name: Ubuntu" >> $(ROLES_DIR)/$(NAME)/meta/main.yml; \
 | ||
| 			echo "      versions: [focal, jammy]" >> $(ROLES_DIR)/$(NAME)/meta/main.yml; \
 | ||
| 			echo "    - name: CentOS" >> $(ROLES_DIR)/$(NAME)/meta/main.yml; \
 | ||
| 			echo "      versions: [7, 8, 9]" >> $(ROLES_DIR)/$(NAME)/meta/main.yml; \
 | ||
| 			echo "    - name: RHEL" >> $(ROLES_DIR)/$(NAME)/meta/main.yml; \
 | ||
| 			echo "      versions: [7, 8, 9]" >> $(ROLES_DIR)/$(NAME)/meta/main.yml; \
 | ||
| 			echo "  galaxy_tags: [$(ROLE_TAGS)]" >> $(ROLES_DIR)/$(NAME)/meta/main.yml; \
 | ||
| 			echo "dependencies: []" >> $(ROLES_DIR)/$(NAME)/meta/main.yml; \
 | ||
| 			echo "---" > $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "# Роль $(NAME)" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "$(ROLE_DESC)" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "## Переменные" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "| Переменная | По умолчанию | Описание |" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "|------------|--------------|----------|" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "| \`$(NAME)_enabled\` | \`true\` | Включить роль |" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "| \`$(NAME)_package\` | \`$(ROLE_PACKAGE)\` | Имя пакета |" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "| \`$(NAME)_service\` | \`$(ROLE_SERVICE)\` | Имя сервиса |" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "## Использование" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "\`\`\`yaml" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "- hosts: all" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "  roles:" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "    - role: $(NAME)" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "\`\`\`" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "## Автор" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "$(AUTHOR)" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "Сайт: $(SITE)" >> $(ROLES_DIR)/$(NAME)/README.md; \
 | ||
| 			echo "$(GREEN)✅ Роль $(NAME) создана$(RESET)"; \
 | ||
| 			echo "$(BLUE)📁 Структура: $(ROLES_DIR)/$(NAME)/$(RESET)"; \
 | ||
| 			echo "$(BLUE)📝 Основной файл: $(ROLES_DIR)/$(NAME)/tasks/main.yml$(RESET)"; \
 | ||
| 			echo "$(BLUE)📋 Playbooks: $(ROLES_DIR)/$(NAME)/playbooks/$(RESET)";; \
 | ||
| 		edit) \
 | ||
| 			if [ -z "$(NAME)" ]; then \
 | ||
| 				echo "$(RED)❌ Использование: make role edit NAME=my-role$(RESET)"; \
 | ||
| 				exit 1; \
 | ||
| 			fi; \
 | ||
| 			echo "$(BLUE)✏️  Редактируем роль: $(NAME)$(RESET)"; \
 | ||
| 			$${EDITOR:-vim} $(ROLES_DIR)/$(NAME)/tasks/main.yml;; \
 | ||
| 		test) \
 | ||
| 			if [ -z "$(NAME)" ]; then \
 | ||
| 				echo "$(RED)❌ Использование: make role test NAME=my-role$(RESET)"; \
 | ||
| 				exit 1; \
 | ||
| 			fi; \
 | ||
| 			echo "$(PURPLE)🧪 Тестируем роль: $(NAME)$(RESET)"; \
 | ||
| 			$(MAKE) lab test LAB_SPEC=molecule/presets/minimal.yml;; \
 | ||
| 		lint) \
 | ||
| 			echo "$(YELLOW)🔍 Проверяем роли...$(RESET)"; \
 | ||
| 			docker exec ansible-controller bash -lc 'ansible-lint --config-file /ansible/.ansible-lint $(ROLES_DIR)/*'; \
 | ||
| 			echo "$(GREEN)✅ Проверка завершена$(RESET)";; \
 | ||
| 		deploy) \
 | ||
| 			echo "$(PURPLE)🚀 Развертываем роли...$(RESET)"; \
 | ||
| 			docker exec ansible-controller bash -lc 'ansible-playbook -i /tmp/molecule/inventory/hosts.yml files/playbooks/site.yml'; \
 | ||
| 			echo "$(GREEN)✅ Развертывание завершено$(RESET)";; \
 | ||
| 		info) \
 | ||
| 			if [ -z "$(NAME)" ]; then \
 | ||
| 				echo "$(RED)❌ Использование: make role info NAME=my-role$(RESET)"; \
 | ||
| 				exit 1; \
 | ||
| 			fi; \
 | ||
| 			echo "$(BLUE)📋 Информация о роли: $(NAME)$(RESET)"; \
 | ||
| 			docker exec ansible-controller bash -lc 'cat /ansible/roles/$(NAME)/README.md';; \
 | ||
| 		playbook) \
 | ||
| 			if [ -z "$(NAME)" ]; then \
 | ||
| 				echo "$(RED)❌ Использование: make role playbook NAME=my-role$(RESET)"; \
 | ||
| 				exit 1; \
 | ||
| 			fi; \
 | ||
| 			echo "$(BLUE)📋 Управление playbooks для роли: $(NAME)$(RESET)"; \
 | ||
| 			echo "$(YELLOW)Доступные команды:$(RESET)"; \
 | ||
| 			echo "  $(CYAN)create$(RESET) - Создать новый playbook"; \
 | ||
| 			echo "  $(CYAN)list$(RESET) - Список playbooks"; \
 | ||
| 			echo "  $(CYAN)edit$(RESET) - Редактировать playbook"; \
 | ||
| 			echo "  $(CYAN)run$(RESET) - Запустить playbook"; \
 | ||
| 			read -p "$(YELLOW)Выберите команду: $(RESET)" PLAYBOOK_CMD; \
 | ||
| 			case "$$PLAYBOOK_CMD" in \
 | ||
| 				create) \
 | ||
| 					read -p "$(YELLOW)📝 Имя playbook: $(RESET)" PLAYBOOK_NAME; \
 | ||
| 					if [ -z "$$PLAYBOOK_NAME" ]; then \
 | ||
| 						echo "$(RED)❌ Имя playbook обязательно$(RESET)"; \
 | ||
| 						exit 1; \
 | ||
| 					fi; \
 | ||
| 					echo "---" > $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "# Playbook: $$PLAYBOOK_NAME для роли $(NAME)" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "# Автор: $(AUTHOR)" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "# Сайт: $(SITE)" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "- name: $$PLAYBOOK_NAME" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "  hosts: all" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "  become: true" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "  gather_facts: true" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "  vars:" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "    # Переменные для роли $(NAME)" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "    $(NAME)_enabled: true" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "  pre_tasks:" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "    - name: Display OS information" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "      debug:" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "        msg: \"OS Family: {{ ansible_os_family }}, OS: {{ ansible_distribution }} {{ ansible_distribution_version }}\"" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "  roles:" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "    - role: $(NAME)" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "  post_tasks:" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "    - name: Verify $(NAME) installation" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "      debug:" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "        msg: \"$(NAME) successfully deployed on {{ inventory_hostname }}\"" >> $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					echo "$(GREEN)✅ Playbook $$PLAYBOOK_NAME создан$(RESET)";; \
 | ||
| 				list) \
 | ||
| 					echo "$(CYAN)📋 Playbooks для роли $(NAME):$(RESET)"; \
 | ||
| 					if [ -d "$(ROLES_DIR)/$(NAME)/playbooks" ]; then \
 | ||
| 						ls -la $(ROLES_DIR)/$(NAME)/playbooks/*.yml 2>/dev/null | awk '{print "  " $$9}' || echo "  $(YELLOW)Нет playbooks$(RESET)"; \
 | ||
| 					else \
 | ||
| 						echo "  $(YELLOW)Директория playbooks не найдена$(RESET)"; \
 | ||
| 					fi;; \
 | ||
| 				edit) \
 | ||
| 					read -p "$(YELLOW)📝 Имя playbook для редактирования: $(RESET)" PLAYBOOK_NAME; \
 | ||
| 					if [ -f "$(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml" ]; then \
 | ||
| 						$${EDITOR:-vim} $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml; \
 | ||
| 					else \
 | ||
| 						echo "$(RED)❌ Playbook $$PLAYBOOK_NAME не найден$(RESET)"; \
 | ||
| 					fi;; \
 | ||
| 				run) \
 | ||
| 					read -p "$(YELLOW)📝 Имя playbook для запуска: $(RESET)" PLAYBOOK_NAME; \
 | ||
| 					if [ -f "$(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml" ]; then \
 | ||
| 						echo "$(BLUE)🚀 Запускаем playbook: $$PLAYBOOK_NAME$(RESET)"; \
 | ||
| 						docker exec ansible-controller bash -lc "ansible-playbook -i /tmp/molecule/inventory/hosts.yml $(ROLES_DIR)/$(NAME)/playbooks/$$PLAYBOOK_NAME.yml"; \
 | ||
| 					else \
 | ||
| 						echo "$(RED)❌ Playbook $$PLAYBOOK_NAME не найден$(RESET)"; \
 | ||
| 					fi;; \
 | ||
| 				*) \
 | ||
| 					echo "$(RED)❌ Неизвестная команда playbook$(RESET)";; \
 | ||
| 			esac;; \
 | ||
| 		*) \
 | ||
| 			echo "$(RED)❌ Неизвестная команда. Доступные: list, create, edit, test, lint, deploy, info, playbook$(RESET)";; \
 | ||
| 	esac
 | ||
| 
 | ||
| # =============================================================================
 | ||
| # УПРАВЛЕНИЕ VAULT
 | ||
| # =============================================================================
 | ||
| 
 | ||
| .PHONY: vault
 | ||
| vault: ## Управление Ansible Vault (show|create|edit|delete|rekey|decrypt|encrypt)
 | ||
| 	@case "$(word 2, $(MAKECMDGOALS))" in \
 | ||
| 		show) \
 | ||
| 			echo "$(BLUE)🔍 Показываем содержимое vault...$(RESET)"; \
 | ||
| 			docker exec ansible-controller bash -lc 'ansible-vault view --vault-password-file $(VAULT_PASSWORD_FILE) vault/secrets.yml';; \
 | ||
| 		create) \
 | ||
| 			echo "$(YELLOW)📝 Создаем vault файл...$(RESET)"; \
 | ||
| 			docker exec ansible-controller bash -lc 'echo "---" > vault/secrets.yml && ansible-vault encrypt --encrypt-vault-id default --vault-password-file $(VAULT_PASSWORD_FILE) vault/secrets.yml';; \
 | ||
| 		edit) \
 | ||
| 			echo "$(BLUE)✏️  Редактируем vault файл...$(RESET)"; \
 | ||
| 			docker exec ansible-controller bash -lc 'ansible-vault edit --vault-password-file $(VAULT_PASSWORD_FILE) vault/secrets.yml';; \
 | ||
| 		delete) \
 | ||
| 			echo "$(RED)🗑️  Удаляем vault файл...$(RESET)"; \
 | ||
| 			docker exec ansible-controller bash -lc 'rm vault/secrets.yml';; \
 | ||
| 		rekey) \
 | ||
| 			echo "$(YELLOW)🔑 Изменяем пароль vault...$(RESET)"; \
 | ||
| 			docker exec ansible-controller bash -lc 'ansible-vault rekey --vault-password-file $(VAULT_PASSWORD_FILE) vault/secrets.yml';; \
 | ||
| 		decrypt) \
 | ||
| 			echo "$(GREEN)🔓 Расшифровываем vault файл...$(RESET)"; \
 | ||
| 			docker exec ansible-controller bash -lc 'ansible-vault decrypt --vault-password-file $(VAULT_PASSWORD_FILE) vault/secrets.yml';; \
 | ||
| 		encrypt) \
 | ||
| 			echo "$(RED)🔒 Шифруем vault файл...$(RESET)"; \
 | ||
| 			docker exec ansible-controller bash -lc 'ansible-vault encrypt --encrypt-vault-id default --vault-password-file $(VAULT_PASSWORD_FILE) vault/secrets.yml';; \
 | ||
| 		*) \
 | ||
| 			echo "$(RED)❌ Неизвестная команда. Доступные: show, create, edit, delete, rekey, decrypt, encrypt$(RESET)";; \
 | ||
| 	esac
 | ||
| 
 | ||
| # =============================================================================
 | ||
| # УПРАВЛЕНИЕ GIT
 | ||
| # =============================================================================
 | ||
| 
 | ||
| .PHONY: git
 | ||
| git: ## Управление Git (status|add|commit|push|pull|branch|merge)
 | ||
| 	@case "$(word 2, $(MAKECMDGOALS))" in \
 | ||
| 		status) \
 | ||
| 			echo "$(CYAN)📊 Статус Git репозитория:$(RESET)"; \
 | ||
| 			git status;; \
 | ||
| 		add) \
 | ||
| 			echo "$(GREEN)➕ Добавляем файлы в Git...$(RESET)"; \
 | ||
| 			git add .;; \
 | ||
| 		commit) \
 | ||
| 			if [ -z "$(MESSAGE)" ]; then \
 | ||
| 				echo "$(RED)❌ Использование: make git commit MESSAGE=\"your commit message\"$(RESET)"; \
 | ||
| 				exit 1; \
 | ||
| 			fi; \
 | ||
| 			echo "$(YELLOW)💾 Создаем коммит...$(RESET)"; \
 | ||
| 			git commit -m "$(MESSAGE)";; \
 | ||
| 		push) \
 | ||
| 			echo "$(BLUE)🚀 Отправляем изменения...$(RESET)"; \
 | ||
| 			git push;; \
 | ||
| 		pull) \
 | ||
| 			echo "$(GREEN)📥 Получаем изменения...$(RESET)"; \
 | ||
| 			git pull;; \
 | ||
| 		branch) \
 | ||
| 			if [ -z "$(NAME)" ]; then \
 | ||
| 				echo "$(RED)❌ Использование: make git branch NAME=my-branch$(RESET)"; \
 | ||
| 				exit 1; \
 | ||
| 			fi; \
 | ||
| 			echo "$(PURPLE)🌿 Создаем ветку: $(NAME)$(RESET)"; \
 | ||
| 			git checkout -b $(NAME);; \
 | ||
| 		merge) \
 | ||
| 			if [ -z "$(BRANCH)" ]; then \
 | ||
| 				echo "$(RED)❌ Использование: make git merge BRANCH=my-branch$(RESET)"; \
 | ||
| 				exit 1; \
 | ||
| 			fi; \
 | ||
| 			echo "$(YELLOW)🔀 Сливаем ветку: $(BRANCH)$(RESET)"; \
 | ||
| 			git merge $(BRANCH);; \
 | ||
| 		*) \
 | ||
| 			echo "$(RED)❌ Неизвестная команда. Доступные: status, add, commit, push, pull, branch, merge$(RESET)";; \
 | ||
| 	esac
 | ||
| 
 | ||
| # =============================================================================
 | ||
| # УПРАВЛЕНИЕ DOCKER
 | ||
| # =============================================================================
 | ||
| 
 | ||
| .PHONY: docker
 | ||
| docker: ## Управление Docker (build|rebuild|prune|shell|logs|stop|start)
 | ||
| 	@case "$(word 2, $(MAKECMDGOALS))" in \
 | ||
| 		build) \
 | ||
| 			echo "$(YELLOW)🔨 Собираем Docker образы...$(RESET)"; \
 | ||
| 			$(DOCKER_COMPOSE) build;; \
 | ||
| 		rebuild) \
 | ||
| 			echo "$(YELLOW)🔨 Пересобираем Docker образы...$(RESET)"; \
 | ||
| 			$(DOCKER_COMPOSE) build --no-cache;; \
 | ||
| 		prune) \
 | ||
| 			echo "$(RED)🧹 Очищаем Docker...$(RESET)"; \
 | ||
| 			docker system prune -af;; \
 | ||
| 		shell) \
 | ||
| 			echo "$(BLUE)🐚 Входим в Docker контейнер...$(RESET)"; \
 | ||
| 			docker exec -it ansible-controller bash;; \
 | ||
| 		logs) \
 | ||
| 			echo "$(CYAN)📋 Показываем логи...$(RESET)"; \
 | ||
| 			$(DOCKER_COMPOSE) logs -f;; \
 | ||
| 		stop) \
 | ||
| 			echo "$(RED)🛑 Останавливаем контейнеры...$(RESET)"; \
 | ||
| 			$(DOCKER_COMPOSE) stop;; \
 | ||
| 		start) \
 | ||
| 			echo "$(GREEN)▶️  Запускаем контейнеры...$(RESET)"; \
 | ||
| 			$(DOCKER_COMPOSE) start;; \
 | ||
| 		*) \
 | ||
| 			echo "$(RED)❌ Неизвестная команда. Доступные: build, rebuild, prune, shell, logs, stop, start$(RESET)";; \
 | ||
| 	esac
 | ||
| 
 | ||
| # =============================================================================
 | ||
| # ОТЧЕТЫ И МОНИТОРИНГ
 | ||
| # =============================================================================
 | ||
| 
 | ||
| .PHONY: report
 | ||
| report: ## Сгенерировать HTML отчет
 | ||
| 	@echo "$(PURPLE)📊 Генерируем HTML отчет...$(RESET)"
 | ||
| 	@mkdir -p reports
 | ||
| 	@docker exec ansible-controller bash -lc 'python3 /ansible/scripts/report_html.py /ansible/reports/lab-health.json /ansible/reports/lab-report.html'
 | ||
| 	@echo "$(GREEN)✅ HTML отчет: reports/lab-report.html$(RESET)"
 | ||
| 	@echo "$(YELLOW)📖 Откройте отчет в браузере:$(RESET)"
 | ||
| 	@echo "  $(BLUE)file://$(PWD)/reports/lab-report.html$(RESET)"
 | ||
| 
 | ||
| .PHONY: kubeconfigs
 | ||
| kubeconfigs: ## Получить все kubeconfig файлы
 | ||
| 	@echo "$(BLUE)📋 Получаем все kubeconfig файлы...$(RESET)"
 | ||
| 	@mkdir -p reports/kubeconfigs
 | ||
| 	@docker exec ansible-controller bash -lc 'for cluster in $$(kind get clusters 2>/dev/null || echo ""); do \
 | ||
| 		if [ -n "$$cluster" ]; then \
 | ||
| 			echo "Получаем kubeconfig для $$cluster..."; \
 | ||
| 			kubectl --context kind-$$cluster config view --raw > /ansible/reports/kubeconfigs/kubeconfig-$$cluster.yaml; \
 | ||
| 		fi; \
 | ||
| 	done'
 | ||
| 	@echo "$(GREEN)✅ Kubeconfig файлы сохранены в reports/kubeconfigs/$(RESET)"
 | ||
| 	@if [ -d "reports/kubeconfigs" ] && [ -n "$$(ls reports/kubeconfigs/ 2>/dev/null)" ]; then \
 | ||
| 		echo "$(YELLOW)📁 Найденные kubeconfig файлы:$(RESET)"; \
 | ||
| 		ls -la reports/kubeconfigs/ | grep -v "^total" | awk '{print "  " $$9}'; \
 | ||
| 	fi
 | ||
| 
 | ||
| .PHONY: open-report
 | ||
| open-report: ## Открыть HTML отчет в браузере
 | ||
| 	@if [ -f "reports/lab-report.html" ]; then \
 | ||
| 		echo "$(BLUE)🌐 Открываем отчет в браузере...$(RESET)"; \
 | ||
| 		if command -v open >/dev/null 2>&1; then \
 | ||
| 			open reports/lab-report.html; \
 | ||
| 		elif command -v xdg-open >/dev/null 2>&1; then \
 | ||
| 			xdg-open reports/lab-report.html; \
 | ||
| 		else \
 | ||
| 			echo "$(YELLOW)⚠️  Откройте отчет вручную: file://$(PWD)/reports/lab-report.html$(RESET)"; \
 | ||
| 		fi; \
 | ||
| 		echo "$(GREEN)✅ Отчет открыт$(RESET)"; \
 | ||
| 	else \
 | ||
| 		echo "$(RED)❌ Отчет не найден. Сначала выполните: make report$(RESET)"; \
 | ||
| 	fi
 | ||
| 
 | ||
| .PHONY: full-test
 | ||
| full-test: ## Полный цикл тестирования с отчетом и kubeconfig
 | ||
| 	@echo "$(PURPLE)🚀 Запускаем полный цикл тестирования...$(RESET)"
 | ||
| 	@$(MAKE) lab test
 | ||
| 	@echo "$(BLUE)📊 Генерируем отчеты...$(RESET)"
 | ||
| 	@$(MAKE) report
 | ||
| 	@$(MAKE) kubeconfigs
 | ||
| 	@echo "$(GREEN)✅ Полный цикл завершен!$(RESET)"
 | ||
| 	@echo "$(YELLOW)📁 Результаты:$(RESET)"
 | ||
| 	@echo "  $(BLUE)📊 HTML отчет: reports/lab-report.html$(RESET)"
 | ||
| 	@echo "  $(BLUE)📋 Kubeconfig файлы: reports/kubeconfigs/$(RESET)"
 | ||
| 	@echo "$(YELLOW)🌐 Открыть отчет: make open-report$(RESET)"
 | ||
| 
 | ||
| .PHONY: chaos
 | ||
| chaos: ## Запустить Chaos Engineering тесты
 | ||
| 	@echo "$(RED)🧨 Запускаем Chaos Engineering...$(RESET)"
 | ||
| 	@docker exec ansible-controller bash -lc 'ansible-playbook -i /tmp/molecule/inventory/hosts.yml /ansible/files/playbooks/chaos.yml'
 | ||
| 	@echo "$(GREEN)✅ Chaos Engineering завершен$(RESET)"
 | ||
| 
 | ||
| .PHONY: check-secrets
 | ||
| check-secrets: ## Проверить безопасность секретов
 | ||
| 	@echo "$(YELLOW)🔍 Проверяем безопасность секретов...$(RESET)"
 | ||
| 	@docker exec ansible-controller bash -lc 'bash /ansible/scripts/secret_scan.sh'
 | ||
| 	@echo "$(GREEN)✅ Проверка секретов завершена$(RESET)"
 | ||
| 
 | ||
| .PHONY: idempotence
 | ||
| idempotence: ## Проверить идемпотентность
 | ||
| 	@echo "$(BLUE)🔄 Проверяем идемпотентность...$(RESET)"
 | ||
| 	@docker exec ansible-controller bash -lc 'ansible-playbook -i /tmp/molecule/inventory/hosts.yml /ansible/files/playbooks/site.yml --check'
 | ||
| 	@echo "$(GREEN)✅ Идемпотентность проверена$(RESET)"
 | ||
| 
 | ||
| .PHONY: snapshot
 | ||
| snapshot: ## Сохранить снапшот лаборатории
 | ||
| 	@echo "$(YELLOW)📸 Создаем снапшот...$(RESET)"
 | ||
| 	@docker exec ansible-controller bash -lc 'bash /ansible/scripts/snapshot.sh'
 | ||
| 	@echo "$(GREEN)✅ Снапшот сохранен$(RESET)"
 | ||
| 
 | ||
| .PHONY: restore
 | ||
| restore: ## Восстановить из снапшота
 | ||
| 	@echo "$(BLUE)🔄 Восстанавливаем из снапшота...$(RESET)"
 | ||
| 	@docker exec ansible-controller bash -lc 'bash /ansible/scripts/restore.sh'
 | ||
| 	@echo "$(GREEN)✅ Снапшот восстановлен$(RESET)"
 | ||
| 
 | ||
| .PHONY: cleanup
 | ||
| cleanup: ## Очистить лабораторию
 | ||
| 	@echo "$(RED)🧹 Очищаем лабораторию...$(RESET)"
 | ||
| 	@docker exec ansible-controller bash -lc 'bash /ansible/scripts/cleanup.sh'
 | ||
| 	@echo "$(GREEN)✅ Лаборатория очищена$(RESET)"
 | ||
| 
 | ||
| # =============================================================================
 | ||
| # УТИЛИТЫ
 | ||
| # =============================================================================
 | ||
| 
 | ||
| .PHONY: lint
 | ||
| lint: ## Проверить весь проект на ошибки
 | ||
| 	@echo "$(YELLOW)🔍 Проверяем весь проект...$(RESET)"
 | ||
| 	@docker exec ansible-controller bash -lc 'ansible-lint --config-file /ansible/.ansible-lint molecule/universal/'
 | ||
| 	@docker exec ansible-controller bash -lc 'ansible-lint --config-file /ansible/.ansible-lint files/playbooks/'
 | ||
| 	@if [ -d "$(ROLES_DIR)" ] && [ -n "$$(ls $(ROLES_DIR)/ 2>/dev/null)" ]; then \
 | ||
| 		docker exec ansible-controller bash -lc 'ansible-lint --config-file /ansible/.ansible-lint $(ROLES_DIR)/*'; \
 | ||
| 	fi
 | ||
| 	@echo "$(GREEN)✅ Проверка завершена$(RESET)"
 | ||
| 
 | ||
| .PHONY: env
 | ||
| env: ## Показать переменные окружения
 | ||
| 	@echo "$(CYAN)🔧 Переменные окружения:$(RESET)"
 | ||
| 	@echo "PROJECT_NAME: $(PROJECT_NAME)"
 | ||
| 	@echo "VERSION: $(VERSION)"
 | ||
| 	@echo "AUTHOR: $(AUTHOR)"
 | ||
| 	@echo "SITE: $(SITE)"
 | ||
| 	@echo "DOCKER_IMAGE: $(DOCKER_IMAGE)"
 | ||
| 	@echo "DOCKER_NETWORK: $(DOCKER_NETWORK)"
 | ||
| 	@echo "SCENARIO: $(SCENARIO)"
 | ||
| 	@echo "LAB_SPEC: $(LAB_SPEC)"
 | ||
| 	@echo "KUBE_CONTEXT: $(KUBE_CONTEXT)"
 | ||
| 	@echo "ROLES_DIR: $(ROLES_DIR)"
 | ||
| 	@echo "VAULT_PASSWORD_FILE: $(VAULT_PASSWORD_FILE)"
 | ||
| 
 | ||
| .PHONY: clean
 | ||
| clean: cleanup ## Полная очистка проекта
 | ||
| 	@echo "$(RED)🧹 Полная очистка проекта...$(RESET)"
 | ||
| 	@rm -rf .env
 | ||
| 	@rm -rf vault/
 | ||
| 	@rm -rf reports/
 | ||
| 	@rm -rf snapshots/
 | ||
| 	@echo "$(GREEN)✅ Проект очищен$(RESET)"
 | ||
| 
 | ||
| # =============================================================================
 | ||
| # ЗАГЛУШКИ ДЛЯ ПАРАМЕТРОВ
 | ||
| # =============================================================================
 | ||
| 
 | ||
| %:
 | ||
| 	@:
 | ||
| 
 | ||
| # =============================================================================
 | ||
| # ЗАГРУЗКА ПЕРЕМЕННЫХ ИЗ .env
 | ||
| # =============================================================================
 | ||
| 
 | ||
| ifneq (,$(wildcard .env))
 | ||
|     include .env
 | ||
|     export
 | ||
| endif |