Files
DevOpsLab/app/core/molecule_executor.py
Сергей Антропов d4b0d6f848 Исправление синтаксической ошибки в molecule_executor.py и обновление k8s preset'ов
- Исправлена незакрытая скобка в _build_test_command (строка 745)
- Добавлена поддержка k8s preset'ов: выполнение create_k8s_cluster.py перед create.yml
- Обновлены образы в k8s preset'ах: заменен недоступный ghcr.io/ansible-community/molecule-ubuntu-systemd:jammy на inecs/ansible-lab:ubuntu22-latest
- Обновлены preset'ы в базе данных через SQL
- Обновлены файлы: k8s-single.yml, k8s-multi.yml, k8s-istio-full.yml
2026-02-16 00:31:09 +03:00

779 lines
43 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Исполнитель Molecule тестов
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
import asyncio
import subprocess
from pathlib import Path
from typing import Optional, AsyncGenerator, Dict
from datetime import datetime
import yaml
import tempfile
import logging
from app.core.config import settings
from app.core.docker_client import DockerClient
logger = logging.getLogger(__name__)
class MoleculeExecutor:
"""Выполнение Molecule тестов без использования Makefile"""
def __init__(self):
# Определяем реальный путь к проекту для монтирования в docker run
# В docker-compose.yml проект монтируется как ../:/workspace:rw
# Когда мы запускаем docker run из контейнера, Docker Desktop на macOS
# не знает о пути /workspace внутри контейнера. Нужно использовать реальный путь на хосте.
project_root = settings.PROJECT_ROOT
# Если PROJECT_ROOT = /workspace (внутри контейнера), но мы запускаем docker run,
# нужно использовать реальный путь на хосте.
# В docker-compose.yml монтируется ../:/workspace, значит на хосте это родительская директория от app/
if str(project_root) == "/workspace":
# Получаем путь к app/ и поднимаемся на уровень выше (это и есть реальный путь на хосте)
current_file = Path(__file__)
# Внутри контейнера: /app/app/core/molecule_executor.py
# current_file.parent = /app/app/core
# current_file.parent.parent = /app/app
# current_file.parent.parent.parent = /app
# Нам нужно получить путь на хосте, который соответствует /workspace
# В docker-compose.yml: ../:/workspace, значит на хосте это родительская директория от app/
# Но внутри контейнера код находится в /app/app/, а проект в /workspace
# Поэтому нужно использовать переменную окружения или определить путь по-другому
# Попробуем получить путь из переменной окружения или использовать текущую рабочую директорию
import os
# Если есть переменная окружения с реальным путем на хосте
host_project_root = os.getenv("HOST_PROJECT_ROOT")
if host_project_root:
project_root = Path(host_project_root)
else:
# Используем путь относительно текущего файла
# Внутри контейнера: /app/app/core/molecule_executor.py
# На хосте: /Users/inecs/Documents/DevOpsLab/app/core/molecule_executor.py
# Нужно подняться на 2 уровня выше от app/ чтобы получить корень проекта
app_dir = current_file.parent.parent.parent # /app/app или /Users/.../app
# Если мы в /app/app, то родительская директория - это /app, но нам нужен путь на хосте
# Поэтому используем абсолютный путь, который будет правильно разрешен Docker Desktop
# Docker Desktop автоматически преобразует пути из контейнера в пути на хосте
# Но для этого нужно использовать путь, который существует на хосте
# В docker-compose.yml: ../:/workspace, значит на хосте это родительская директория от app/
# На хосте: /Users/inecs/Documents/DevOpsLab
# Внутри контейнера: /workspace
# Когда мы запускаем docker run, нужно использовать путь на хосте
# Но мы не знаем его напрямую, поэтому используем относительный путь
# или определяем его через переменную окружения
project_root = Path("/workspace") # Оставляем как есть, но для docker run нужно использовать реальный путь
self.project_root = project_root.resolve() if isinstance(project_root, Path) else Path(project_root).resolve()
# Для docker run нужно использовать реальный путь на хосте
# Определяем его через переменную окружения или используем путь относительно app/
self.host_project_root = os.getenv("HOST_PROJECT_ROOT", str(self.project_root))
if self.host_project_root == "/workspace":
# Если не задана переменная, пытаемся определить путь на хосте
# В docker-compose.yml монтируется ../:/workspace
# Значит на хосте это родительская директория от app/
current_file = Path(__file__)
app_dir = current_file.parent.parent.parent # app/
# На хосте app/ находится в /Users/.../DevOpsLab/app
# Значит корень проекта на хосте - это родительская директория от app/
# Но внутри контейнера мы не знаем этот путь напрямую
# Поэтому используем путь, который Docker Desktop может разрешить
# Docker Desktop автоматически преобразует пути из volume mounts
# Но для docker run нужно использовать путь на хосте
# Лучше всего использовать переменную окружения HOST_PROJECT_ROOT
self.host_project_root = str(self.project_root)
self.molecule_dir = self.project_root / "molecule" / "default"
# Пресеты для Molecule должны находиться в molecule/presets
# чтобы create.yml мог их найти по пути /workspace/molecule/presets/
# ВАЖНО: presets_dir должен быть относительно project_root, который внутри контейнера = /workspace
# Это гарантирует, что файлы будут доступны внутри ansible-controller контейнера
self.presets_dir = Path("/workspace") / "molecule" / "presets"
self.docker_client = DockerClient()
self._temp_preset_files = {} # Кэш временных файлов preset'ов
def load_preset_from_db(self, preset_content: str) -> Dict:
"""Загрузка конфигурации preset'а из содержимого (из БД)"""
return yaml.safe_load(preset_content) or {}
def create_temp_preset_file(self, preset_name: str, preset_content: str, category: str = "main") -> Path:
"""Создание временного файла preset'а из содержимого БД"""
# Создаем временную директорию для preset'ов если её нет
temp_presets_dir = self.presets_dir
if category == "k8s":
temp_presets_dir = temp_presets_dir / "k8s"
temp_presets_dir.mkdir(parents=True, exist_ok=True)
# Создаем временный файл
preset_file = temp_presets_dir / f"{preset_name}.yml"
preset_file.write_text(preset_content)
# Сохраняем путь для последующего удаления
if preset_name not in self._temp_preset_files:
self._temp_preset_files[preset_name] = preset_file
return preset_file
def load_preset(self, preset_name: str, category: str = "main") -> Dict:
"""Загрузка конфигурации preset'а (из файла или БД)"""
# Сначала пробуем загрузить из файла (для обратной совместимости)
if category == "k8s":
preset_file = self.presets_dir / "k8s" / f"{preset_name}.yml"
else:
preset_file = self.presets_dir / f"{preset_name}.yml"
if preset_file.exists():
with open(preset_file) as f:
return yaml.safe_load(f) or {}
# Если файла нет, значит preset в БД и должен быть передан через create_temp_preset_file
raise FileNotFoundError(f"Preset '{preset_name}' не найден. Убедитесь, что preset создан через create_temp_preset_file")
async def decrypt_vault_files(self) -> bool:
"""Расшифровка vault файлов перед тестированием"""
vault_dir = self.project_root / "vault"
if not vault_dir.exists():
return True
vault_password_file = vault_dir / ".vault"
if not vault_password_file.exists():
return True
# Запускаем ansible-vault decrypt для всех .yml файлов
vault_files = list(vault_dir.glob("*.yml"))
if not vault_files:
return True
try:
for vault_file in vault_files:
# Проверяем, зашифрован ли файл
with open(vault_file, 'rb') as f:
content = f.read(100)
if b'$ANSIBLE_VAULT' not in content:
continue
# Расшифровываем
cmd = [
"ansible-vault", "decrypt",
str(vault_file),
"--vault-password-file", str(vault_password_file)
]
result = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=str(self.project_root)
)
await result.wait()
return True
except Exception:
return False
async def encrypt_vault_files(self) -> bool:
"""Шифрование vault файлов после тестирования"""
vault_dir = self.project_root / "vault"
if not vault_dir.exists():
return True
vault_password_file = vault_dir / ".vault"
if not vault_password_file.exists():
return True
# Находим расшифрованные .yml файлы
vault_files = list(vault_dir.glob("*.yml"))
if not vault_files:
return True
try:
for vault_file in vault_files:
# Проверяем, не зашифрован ли уже файл
with open(vault_file, 'rb') as f:
content = f.read(100)
if b'$ANSIBLE_VAULT' in content:
continue
# Шифруем
cmd = [
"ansible-vault", "encrypt",
str(vault_file),
"--vault-password-file", str(vault_password_file)
]
result = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=str(self.project_root)
)
await result.wait()
return True
except Exception:
return False
async def test_role(
self,
role_name: Optional[str] = None,
preset_name: str = "default",
preset_content: Optional[str] = None,
preset_category: str = "main",
stream: bool = False,
stop_event: Optional[callable] = None,
container_ref: Optional[object] = None
) -> AsyncGenerator[str, None]:
"""
Тестирование роли через Molecule
Args:
role_name: Имя роли (опционально, если None - тестируются все роли)
preset_name: Имя preset'а
preset_content: Содержимое preset'а из БД (если None - загружается из файла)
preset_category: Категория preset'а (main или k8s)
stream: Если True, возвращает генератор строк для WebSocket
stop_event: Функция для проверки флага остановки
container_ref: Объект для хранения ссылки на контейнер
Yields:
Строки вывода команды
"""
container = None
try:
# Если preset_content передан, создаем временный файл из БД
if preset_content:
try:
self.create_temp_preset_file(preset_name, preset_content, preset_category)
preset_data = self.load_preset_from_db(preset_content)
except Exception as e:
yield f"❌ Ошибка создания временного файла preset'а: {str(e)}\n"
return
else:
# Проверка существования preset'а в файловой системе
try:
preset_data = self.load_preset(preset_name, preset_category)
except FileNotFoundError as e:
yield f"❌ Ошибка: {str(e)}\n"
yield "💡 Убедитесь, что preset существует в БД или файловой системе\n"
return
# Расшифровка vault файлов
yield "🔓 Расшифровка vault файлов...\n"
await self.decrypt_vault_files()
# Запуск ansible-controller контейнера
yield "🔧 Запуск ansible-controller контейнера...\n"
# Подготовка переменных окружения
env = {
"ANSIBLE_FORCE_COLOR": "1",
"MOLECULE_PRESET": preset_name,
"MOLECULE_EPHEMERAL_DIRECTORY": "/tmp/molecule_workspace",
"MOLECULE_VAULT_ENABLED": "false"
}
if role_name:
env["MOLECULE_ROLE_NAME"] = role_name
yield f"📋 Тестируется роль: {role_name}\n"
yield f"📋 Используется пресет: {preset_name}\n\n"
# Используем Docker SDK вместо subprocess для запуска контейнера
# Команда docker может быть недоступна внутри контейнера
container_name = f"ansible-controller-{datetime.now().strftime('%Y%m%d%H%M%S')}"
try:
# Подготавливаем volumes для монтирования
volumes = {
str(self.host_project_root): {"bind": "/workspace", "mode": "rw"},
"/var/run/docker.sock": {"bind": "/var/run/docker.sock", "mode": "rw"}
}
# Запускаем контейнер
container = self.docker_client.client.containers.run(
image="inecs/ansible-lab:ansible-controller-latest",
name=container_name,
command=["bash", "-c", self._build_test_command(role_name, preset_name, preset_category)],
environment=env,
volumes=volumes,
working_dir="/workspace",
user="root",
detach=True,
remove=False, # Не удаляем автоматически, чтобы можно было получить логи
auto_remove=False
)
# Сохраняем ссылку на контейнер для возможности остановки
if container_ref and hasattr(container_ref, 'set'):
container_ref.set(container)
# Потоковый вывод логов (асинхронно)
try:
# Используем обычную очередь для передачи данных между потоками
import queue
import threading
log_queue = queue.Queue()
loop = asyncio.get_event_loop()
def read_logs():
"""Синхронная функция для чтения логов"""
try:
for line in container.logs(stream=True, follow=True, stdout=True, stderr=True):
log_queue.put(line)
except Exception as e:
log_queue.put(None)
# Запускаем чтение логов в отдельном потоке
log_thread = threading.Thread(target=read_logs, daemon=True)
log_thread.start()
# Читаем логи из очереди асинхронно
while True:
# Проверяем флаг остановки
if stop_event and stop_event():
try:
yield "\n⏹️ Остановка тестирования по запросу пользователя...\n"
except GeneratorExit:
# Генератор уже закрыт - выполняем cleanup без yield
pass
# Останавливаем и удаляем контейнер ansible-controller
try:
if container:
container.stop(timeout=10)
logger.info(f"Container {container_name} stopped")
try:
yield "🛑 Контейнер ansible-controller остановлен\n"
except GeneratorExit:
pass
except Exception as e:
logger.error(f"Error stopping container: {e}")
try:
yield f"⚠️ Ошибка при остановке контейнера: {e}\n"
except GeneratorExit:
pass
# Запускаем destroy.yml для очистки контейнеров из preset'а
try:
yield "\n🧹 Очистка контейнеров из preset'а...\n"
except GeneratorExit:
pass
# Выполняем cleanup (синхронно, без yield)
cleanup_success = False
try:
await self._cleanup_preset_containers(preset_name, preset_category)
logger.info(f"Cleanup completed for preset {preset_name}")
cleanup_success = True
try:
yield "✅ Контейнеры из preset'а удалены\n"
except GeneratorExit:
pass
except Exception as e:
logger.error(f"Error cleaning up preset containers: {e}")
try:
yield f"⚠️ Ошибка при очистке контейнеров preset'а: {e}\n"
except GeneratorExit:
pass
# Удаляем контейнер ansible-controller
try:
if container:
container.remove(force=True)
logger.info(f"Container {container_name} removed")
try:
yield "🗑️ Контейнер ansible-controller удален\n"
except GeneratorExit:
pass
except Exception as e:
logger.error(f"Error removing container: {e}")
try:
yield f"⚠️ Ошибка при удалении контейнера: {e}\n"
except GeneratorExit:
pass
# Сохраняем флаг, что cleanup был выполнен
cleanup_done = cleanup_success
# Выходим из цикла
break
try:
# Используем run_in_executor для чтения из обычной очереди
line = await asyncio.get_event_loop().run_in_executor(
None,
lambda: log_queue.get(timeout=0.1)
)
if line is None:
break
yield line.decode('utf-8', errors='replace')
except queue.Empty:
# Проверяем, завершился ли контейнер
try:
container.reload()
if container.status == 'exited':
# Читаем оставшиеся логи из очереди
while not log_queue.empty():
try:
line = log_queue.get_nowait()
if line is not None:
yield line.decode('utf-8', errors='replace')
except queue.Empty:
break
# Читаем финальные логи из контейнера
remaining_logs = container.logs(stdout=True, stderr=True)
if remaining_logs:
yield remaining_logs.decode('utf-8', errors='replace')
break
except Exception as e:
logger.error(f"Error reloading container: {e}")
break
continue
except GeneratorExit:
# Генератор закрыт - останавливаем контейнер и очищаем ресурсы
# НЕ используем yield здесь, так как генератор уже закрывается
logger.info(f"GeneratorExit caught, cleaning up container {container_name if container else 'unknown'}")
try:
if container:
container.stop(timeout=5)
container.remove(force=True)
logger.info(f"Container {container_name} stopped and removed on GeneratorExit")
except Exception as e:
logger.error(f"Error cleaning up container on GeneratorExit: {e}")
# Запускаем cleanup при закрытии генератора (без yield)
try:
await self._cleanup_preset_containers(preset_name, preset_category)
logger.info(f"Cleanup completed for preset {preset_name} on GeneratorExit")
except Exception as e:
logger.error(f"Error in cleanup on GeneratorExit: {e}")
# Обязательно поднимаем исключение дальше
raise
except Exception as e:
logger.error(f"Error reading logs: {e}")
break
finally:
# Ожидаем завершения контейнера (если он еще не остановлен)
# cleanup_done может быть уже установлен при остановке по запросу
if 'cleanup_done' not in locals():
cleanup_done = False
if container:
try:
container.reload()
if container.status != 'exited':
try:
exit_code = container.wait(timeout=5)["StatusCode"]
except:
# Если не удалось дождаться, останавливаем принудительно
container.stop(timeout=5)
exit_code = -1
else:
exit_code = container.attrs.get("State", {}).get("ExitCode", -1)
except Exception as e:
logger.error(f"Error waiting for container: {e}")
exit_code = -1
# Получаем финальные логи, если есть
try:
final_logs = container.logs(stdout=True, stderr=True, tail=100)
if final_logs:
yield final_logs.decode('utf-8', errors='replace')
except Exception as e:
logger.error(f"Error getting final logs: {e}")
# Удаляем контейнер ansible-controller
try:
if container:
container.stop(timeout=5)
container.remove(force=True)
try:
yield f"\n🗑️ Контейнер {container_name} удален\n"
except GeneratorExit:
pass
except Exception as e:
logger.error(f"Error removing container {container_name}: {e}")
try:
yield f"\n⚠️ Предупреждение: не удалось удалить контейнер {container_name}: {e}\n"
except GeneratorExit:
pass
# Запускаем cleanup контейнеров из preset'а (всегда, даже если был вызван ранее)
# Это гарантирует, что контейнеры будут удалены даже при ошибках
try:
logger.info(f"Running cleanup in finally block for preset {preset_name}")
await self._cleanup_preset_containers(preset_name, preset_category)
logger.info(f"Cleanup completed in finally block for preset {preset_name}")
try:
yield "\n🧹 Контейнеры из preset'а очищены\n"
except GeneratorExit:
pass
except Exception as e:
logger.error(f"Error in cleanup during finally: {e}", exc_info=True)
try:
yield f"\n⚠️ Ошибка при очистке контейнеров preset'а: {e}\n"
except GeneratorExit:
pass
except Exception as e:
yield f"❌ Ошибка при запуске контейнера: {str(e)}\n"
import traceback
yield f"Детали: {traceback.format_exc()}\n"
# Пытаемся удалить контейнер, если он был создан
try:
container = self.docker_client.client.containers.get(container_name)
container.remove(force=True)
except:
pass
# Шифрование vault файлов
yield "\n🔒 Шифрование vault файлов...\n"
await self.encrypt_vault_files()
# Проверяем код возврата контейнера (если не было остановки)
if not (stop_event and stop_event()) and container:
try:
container.reload()
exit_code = container.attrs.get("State", {}).get("ExitCode", -1)
if exit_code == 0:
yield "\n✅ Тестирование завершено успешно\n"
elif exit_code == -1:
yield "\n✅ Тестирование preset'а завершено\n"
else:
yield f"\n❌ Тестирование завершено с ошибкой (код: {exit_code})\n"
except:
yield "\n✅ Тестирование preset'а завершено\n"
except GeneratorExit:
# Генератор закрыт - останавливаем контейнер и очищаем ресурсы
# НЕ используем yield здесь, так как генератор уже закрывается
if container:
try:
container.stop(timeout=5)
container.remove(force=True)
logger.info(f"Container {container_name} stopped and removed due to GeneratorExit")
except Exception as e:
logger.error(f"Error removing container on GeneratorExit: {e}")
# Запускаем cleanup при закрытии генератора (без yield)
try:
if preset_content or preset_name:
await self._cleanup_preset_containers(preset_name, preset_category)
logger.info(f"Cleanup completed for preset {preset_name} on GeneratorExit")
except Exception as e:
logger.error(f"Error in cleanup on GeneratorExit: {e}")
raise
except Exception as e:
# Обрабатываем другие исключения
if container:
try:
container.stop(timeout=5)
container.remove(force=True)
except:
pass
# Запускаем cleanup при ошибке (без yield, так как генератор может быть закрыт)
try:
if preset_content or preset_name:
await self._cleanup_preset_containers(preset_name, preset_category)
except:
pass
# Пытаемся отправить сообщение об ошибке, если генератор еще открыт
try:
yield f"\n❌ Критическая ошибка: {str(e)}\n"
except GeneratorExit:
# Генератор закрыт - это нормально
raise
except:
# Другие ошибки при yield - игнорируем
pass
async def _cleanup_preset_containers(self, preset_name: str, preset_category: str = "main"):
"""Очистка контейнеров из preset'а через destroy.yml"""
logger.info(f"Starting cleanup for preset {preset_name} (category: {preset_category})")
# Сначала пытаемся удалить контейнеры напрямую (быстрее и надежнее)
cleanup_success = False
try:
# Получаем список всех контейнеров
all_containers = self.docker_client.client.containers.list(all=True)
removed_count = 0
for cont in all_containers:
try:
cont.reload()
cont_name = cont.name
# Проверяем, что контейнер в сети labnet или имеет короткое имя (тестовые контейнеры u1, u2 и т.д.)
networks = cont.attrs.get("NetworkSettings", {}).get("Networks", {})
is_test_container = False
if "labnet" in networks:
is_test_container = True
elif cont_name and len(cont_name) <= 10 and (cont_name.startswith("u") or cont_name.startswith("test-")):
is_test_container = True
if is_test_container:
try:
if cont.status != 'exited':
cont.stop(timeout=5)
cont.remove(force=True)
removed_count += 1
logger.info(f"Removed container {cont_name} during direct cleanup")
except Exception as remove_error:
logger.warning(f"Error removing container {cont_name}: {remove_error}")
except Exception as cont_error:
logger.debug(f"Error processing container: {cont_error}")
if removed_count > 0:
logger.info(f"Direct cleanup removed {removed_count} containers")
cleanup_success = True
except Exception as direct_cleanup_error:
logger.error(f"Error in direct cleanup: {direct_cleanup_error}")
# Также пытаемся запустить destroy.yml для полной очистки
try:
# Запускаем destroy.yml в отдельном контейнере для очистки
env = {
"ANSIBLE_FORCE_COLOR": "1",
"MOLECULE_PRESET": preset_name,
"MOLECULE_EPHEMERAL_DIRECTORY": "/tmp/molecule_workspace",
"MOLECULE_VAULT_ENABLED": "false"
}
cleanup_container_name = f"ansible-cleanup-{datetime.now().strftime('%Y%m%d%H%M%S')}"
volumes = {
str(self.host_project_root): {"bind": "/workspace", "mode": "rw"},
"/var/run/docker.sock": {"bind": "/var/run/docker.sock", "mode": "rw"}
}
cleanup_container = self.docker_client.client.containers.run(
image="inecs/ansible-lab:ansible-controller-latest",
name=cleanup_container_name,
command=["bash", "-c", "cd molecule/default && ansible-playbook -i localhost, destroy.yml --connection=local -e molecule_ephemeral_directory=/tmp/molecule_workspace"],
environment=env,
volumes=volumes,
working_dir="/workspace",
user="root",
detach=True,
remove=False,
auto_remove=False
)
# Ждем завершения cleanup
try:
exit_code = cleanup_container.wait(timeout=30)["StatusCode"]
if exit_code == 0:
logger.info(f"Destroy.yml cleanup completed successfully")
cleanup_success = True
else:
logger.warning(f"Cleanup container exited with code {exit_code}")
except Exception as e:
logger.error(f"Error waiting for cleanup container: {e}")
finally:
# Удаляем cleanup контейнер
try:
cleanup_container.stop(timeout=5)
cleanup_container.remove(force=True)
except Exception as e:
logger.debug(f"Error removing cleanup container: {e}")
except Exception as e:
logger.error(f"Error running destroy.yml cleanup: {e}")
if not cleanup_success:
logger.warning(f"Cleanup may not have completed successfully for preset {preset_name}")
def _build_test_command(self, role_name: Optional[str] = None, preset_name: Optional[str] = None, preset_category: str = "main") -> str:
"""Построение команды для тестирования"""
commands = [
"echo -e '\\033[33m=== СОЗДАНИЕ ТЕСТОВЫХ КОНТЕЙНЕРОВ ==='",
"echo ''",
"mkdir -p /tmp/molecule_workspace/inventory",
]
# Для k8s пресетов сначала запускаем create_k8s_cluster.py
is_k8s_preset = False
if preset_category == "k8s":
is_k8s_preset = True
elif preset_name:
if preset_name.startswith("k8s-") or preset_name in ["kubernetes", "k8s-full"]:
is_k8s_preset = True
if is_k8s_preset:
# Определяем путь к preset файлу
# Для k8s пресетов файл должен быть в /workspace/molecule/presets/k8s/
if preset_category == "k8s":
preset_file_path = f"/workspace/molecule/presets/k8s/{preset_name}.yml"
elif preset_name:
k8s_names = ["kubernetes", "k8s-full"]
if preset_name.startswith("k8s-") or preset_name in k8s_names:
preset_file_path = f"/workspace/molecule/presets/k8s/{preset_name}.yml"
else:
preset_file_path = f"/workspace/molecule/presets/k8s/{preset_name}.yml"
else:
preset_file_path = f"/workspace/molecule/presets/k8s/{preset_name}.yml"
# Формируем команду для создания Kind кластера
script_path = "/workspace/scripts/create_k8s_cluster.py"
k8s_cmd_str = f"test -f {preset_file_path} && python3 {script_path} {preset_file_path} ansible-controller || echo Ошибка при создании Kind кластера"
commands.extend([
"echo -e '\\033[33m=== СОЗДАНИЕ KUBERNETES КЛАСТЕРА ==='",
"echo ''",
k8s_cmd_str,
"echo ''",
"echo -e '\\033[33m=== СОЗДАНИЕ DOCKER КОНТЕЙНЕРОВ ==='",
"echo ''",
])
commands.extend([
"cd molecule/default",
"ansible-playbook -i localhost, create.yml --connection=local -e molecule_ephemeral_directory=/tmp/molecule_workspace",
"echo ''",
"echo -e '\\033[33m=== НАСТРОЙКА VAULT И ПЕРЕМЕННЫХ ==='",
"echo ''",
"ansible-playbook -i localhost, converge.yml --connection=local -e molecule_ephemeral_directory=/tmp/molecule_workspace",
"echo ''",
"echo -e '\\033[33m=== ПРОВЕРКА ПОДКЛЮЧЕНИЯ К КОНТЕЙНЕРАМ ==='",
"echo ''",
"ansible all -i /tmp/molecule_workspace/inventory/hosts.ini -m ping",
"echo ''",
"echo -e '\\033[33m=== ЗАПУСК CONVERGE.YML НА ТЕСТОВЫХ КОНТЕЙНЕРАХ ===\\033[0m'",
"echo ''",
"ansible-playbook -i /tmp/molecule_workspace/inventory/hosts.ini converge.yml",
"echo ''",
"echo -e '\\033[33m=== ЗАПУСК ROLES/DEPLOY.YML НА ТЕСТОВЫХ КОНТЕЙНЕРАХ ===\\033[0m'",
"echo ''"
])
# Добавляем команду для deploy.yml с фильтрацией по роли
if role_name:
commands.append(
f"ansible-playbook -i /tmp/molecule_workspace/inventory/hosts.ini ../../roles/deploy.yml --tags {role_name} || true"
)
else:
commands.append(
"ansible-playbook -i /tmp/molecule_workspace/inventory/hosts.ini ../../roles/deploy.yml || true"
)
commands.extend([
"echo ''",
"echo -e '\\033[33m=== ОЧИСТКА РЕСУРСОВ ==='",
"echo ''",
"ansible-playbook -i localhost, destroy.yml --connection=local -e molecule_ephemeral_directory=/tmp/molecule_workspace",
"echo ''",
"echo '✅ Тестирование завершено'"
])
return " && ".join(commands)
def detect_log_level(self, line: str) -> str:
"""Определение уровня лога из строки"""
line_lower = line.lower()
if any(word in line_lower for word in ["error", "failed", "fatal"]):
return "error"
elif any(word in line_lower for word in ["warning", "warn"]):
return "warning"
elif any(word in line_lower for word in ["changed", "ok"]):
return "info"
else:
return "debug"