Исправление синтаксической ошибки в 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
This commit is contained in:
@@ -11,24 +11,88 @@ 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):
|
||||
self.project_root = settings.PROJECT_ROOT
|
||||
# Определяем реальный путь к проекту для монтирования в 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"
|
||||
# Пресеты теперь находятся в alembic/presets
|
||||
# Находим путь к alembic относительно текущего файла
|
||||
current_file = Path(__file__)
|
||||
alembic_dir = current_file.parent.parent / "alembic"
|
||||
self.presets_dir = alembic_dir / "presets"
|
||||
# Если не найдено, пробуем старый путь (для обратной совместимости)
|
||||
if not self.presets_dir.exists():
|
||||
self.presets_dir = self.project_root / "molecule" / "presets"
|
||||
# Пресеты для 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'ов
|
||||
|
||||
@@ -158,7 +222,9 @@ class MoleculeExecutor:
|
||||
preset_name: str = "default",
|
||||
preset_content: Optional[str] = None,
|
||||
preset_category: str = "main",
|
||||
stream: bool = False
|
||||
stream: bool = False,
|
||||
stop_event: Optional[callable] = None,
|
||||
container_ref: Optional[object] = None
|
||||
) -> AsyncGenerator[str, None]:
|
||||
"""
|
||||
Тестирование роли через Molecule
|
||||
@@ -169,96 +235,496 @@ class MoleculeExecutor:
|
||||
preset_content: Содержимое preset'а из БД (если None - загружается из файла)
|
||||
preset_category: Категория preset'а (main или k8s)
|
||||
stream: Если True, возвращает генератор строк для WebSocket
|
||||
stop_event: Функция для проверки флага остановки
|
||||
container_ref: Объект для хранения ссылки на контейнер
|
||||
|
||||
Yields:
|
||||
Строки вывода команды
|
||||
"""
|
||||
# Если preset_content передан, создаем временный файл из БД
|
||||
if preset_content:
|
||||
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:
|
||||
self.create_temp_preset_file(preset_name, preset_content, preset_category)
|
||||
preset_data = self.load_preset_from_db(preset_content)
|
||||
# Подготавливаем 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"❌ Ошибка создания временного файла preset'а: {str(e)}\n"
|
||||
return
|
||||
else:
|
||||
# Проверка существования preset'а в файловой системе
|
||||
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:
|
||||
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_cmd = [
|
||||
"docker", "run", "--rm",
|
||||
"--name", f"ansible-controller-{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||||
"-v", f"{self.project_root}:/workspace",
|
||||
"-w", "/workspace",
|
||||
"-v", "/var/run/docker.sock:/var/run/docker.sock",
|
||||
"-u", "root"
|
||||
]
|
||||
|
||||
# Добавляем переменные окружения
|
||||
for key, value in env.items():
|
||||
docker_cmd.extend(["-e", f"{key}={value}"])
|
||||
|
||||
docker_cmd.extend([
|
||||
"inecs/ansible-lab:ansible-controller-latest",
|
||||
"bash", "-c", self._build_test_command(role_name)
|
||||
])
|
||||
|
||||
# Запуск процесса
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*docker_cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.STDOUT,
|
||||
cwd=str(self.project_root)
|
||||
)
|
||||
|
||||
# Потоковый вывод
|
||||
async for line in process.stdout:
|
||||
yield line.decode('utf-8', errors='replace')
|
||||
|
||||
await process.wait()
|
||||
|
||||
# Шифрование vault файлов
|
||||
yield "\n🔒 Шифрование vault файлов...\n"
|
||||
await self.encrypt_vault_files()
|
||||
|
||||
if process.returncode == 0:
|
||||
yield "\n✅ Тестирование завершено успешно\n"
|
||||
else:
|
||||
yield f"\n❌ Тестирование завершено с ошибкой (код: {process.returncode})\n"
|
||||
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
|
||||
|
||||
def _build_test_command(self, role_name: Optional[str] = None) -> str:
|
||||
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 ''",
|
||||
@@ -276,7 +742,7 @@ class MoleculeExecutor:
|
||||
"echo ''",
|
||||
"echo -e '\\033[33m=== ЗАПУСК ROLES/DEPLOY.YML НА ТЕСТОВЫХ КОНТЕЙНЕРАХ ===\\033[0m'",
|
||||
"echo ''"
|
||||
]
|
||||
])
|
||||
|
||||
# Добавляем команду для deploy.yml с фильтрацией по роли
|
||||
if role_name:
|
||||
|
||||
Reference in New Issue
Block a user