# Деплой, импорт и экспорт ролей в веб-интерфейсе **Автор:** Сергей Антропов **Сайт:** https://devops.org.ru ## 🎯 Обзор Этот документ описывает функциональность деплоя на живые серверы, импорта и экспорта ролей через веб-интерфейс. --- ## 1. 🚀 Деплой на живые серверы ### 1.1. Страница деплоя роли **URL:** `/roles/{role_name}/deploy` **Интерфейс:** ``` ┌─────────────────────────────────────────────────────────┐ │ Деплой роли: nginx на продакшн серверы │ ├─────────────────────────────────────────────────────────┤ │ │ │ ⚠️ ВНИМАНИЕ: Вы собираетесь изменить реальные серверы!│ │ │ │ 📋 Шаг 1: Выбор inventory │ │ ┌─────────────────────────────────────────────────┐ │ │ │ ○ Использовать существующий inventory │ │ │ │ [production ▼] [staging] [development] │ │ │ │ │ │ │ │ ● Создать/редактировать inventory │ │ │ │ [Открыть редактор inventory] │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ 📝 Шаг 2: Настройка переменных │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Переменные для продакшн: │ │ │ │ │ │ │ │ nginx_version: [1.25.0] │ │ │ │ nginx_worker_processes: [4] │ │ │ │ nginx_worker_connections: [2048] │ │ │ │ nginx_sites: │ │ │ │ - name: example.com │ │ │ │ root: /var/www/html │ │ │ │ ssl: ☑ │ │ │ │ │ │ │ │ [📥 Загрузить из шаблона] │ │ │ │ [💾 Сохранить как шаблон] │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ ⚙️ Шаг 3: Параметры деплоя │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Режим: │ │ │ │ ○ Dry-run (только проверка, без изменений) │ │ │ │ ● Реальный деплой │ │ │ │ │ │ │ │ Опции: │ │ │ │ ☑ Проверка подключения перед деплоем │ │ │ │ ☑ Проверка синтаксиса │ │ │ │ ☐ Только определенные теги │ │ │ │ [web,config] │ │ │ │ ☐ Только определенные хосты │ │ │ │ [web1,web2] │ │ │ │ ☐ Лимит хостов (для постепенного деплоя) │ │ │ │ [2 из 5] │ │ │ │ │ │ │ │ Стратегия деплоя: │ │ │ │ ○ Все хосты одновременно │ │ │ │ ● Постепенно (по одному) │ │ │ │ ○ По группам (сначала web, потом db) │ │ │ │ │ │ │ │ Rollback: │ │ │ │ ☑ Создать backup перед деплоем │ │ │ │ ☑ Включить автоматический rollback при ошибке │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ 🔐 Шаг 4: Безопасность │ │ ┌─────────────────────────────────────────────────┐ │ │ │ ☑ Использовать Vault для секретов │ │ │ │ ☑ Шифровать SSH ключи │ │ │ │ ☑ Требовать подтверждение для критических операций│ │ │ │ │ │ │ Подтверждение: │ │ │ │ [ ] Я понимаю, что это изменит продакшн серверы │ │ │ │ [ ] Я проверил переменные │ │ │ │ [ ] У меня есть backup │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ [🔍 Dry-run] [🚀 Запустить деплой] │ └─────────────────────────────────────────────────────────┘ ``` ### 1.2. Live мониторинг деплоя **После запуска деплоя открывается страница с live логами:** ``` ┌─────────────────────────────────────────────────────────┐ │ Деплой роли: nginx | Статус: 🟢 Running │ ├─────────────────────────────────────────────────────────┤ │ │ │ 📊 Прогресс: │ │ ┌─────────────────────────────────────────────────┐ │ │ │ ████████████████░░░░ 80% │ │ │ │ Хостов обработано: 4 из 5 │ │ │ │ Текущий хост: web3.example.com │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ 🖥️ Статус хостов: │ │ ┌─────────────────────────────────────────────────┐ │ │ │ ✅ web1.example.com [Успешно] (2m 15s) │ │ │ │ ✅ web2.example.com [Успешно] (2m 10s) │ │ │ │ 🟢 web3.example.com [В процессе...] │ │ │ │ ⏳ web4.example.com [Ожидание...] │ │ │ │ ⏳ web5.example.com [Ожидание...] │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ 📝 Live логи (WebSocket): │ │ ┌─────────────────────────────────────────────────┐ │ │ │ [10:30:15] PLAY [web_servers] ****************** │ │ │ │ [10:30:16] TASK [nginx : Install packages] │ │ │ │ [10:30:17] changed: [web1.example.com] │ │ │ │ [10:30:18] TASK [nginx : Configure nginx] │ │ │ │ [10:30:19] changed: [web1.example.com] │ │ │ │ [10:30:20] TASK [nginx : Start nginx] │ │ │ │ [10:30:21] changed: [web1.example.com] │ │ │ │ [10:30:22] PLAY RECAP ************************** │ │ │ │ [10:30:23] web1.example.com : ok=5 changed=3 │ │ │ │ ... │ │ │ │ │ │ │ │ [Автоскролл: ☑] [Очистить] [📥 Скачать] │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ 📈 Статистика: │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Успешно: 2 │ │ │ │ В процессе: 1 │ │ │ │ Ожидание: 2 │ │ │ │ Ошибок: 0 │ │ │ │ Среднее время: 2m 12s │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ [⏸️ Пауза] [⏹️ Остановить] [🔄 Retry failed] │ └─────────────────────────────────────────────────────────┘ ``` ### 1.3. WebSocket реализация для live деплоя ```python # app/api/v1/endpoints/deploy.py from fastapi import WebSocket, APIRouter from app.core.make_executor import MakeExecutor from app.services.deploy_service import DeployService router = APIRouter() @router.websocket("/ws/deploy/{deploy_id}") async def deploy_websocket(websocket: WebSocket, deploy_id: str): await websocket.accept() # Получение параметров деплоя из БД deploy_config = await get_deploy_config(deploy_id) # Запуск ansible-playbook с потоковым выводом executor = MakeExecutor() process = await executor.execute_stream( f"role deploy", args=[ "--inventory", deploy_config["inventory"], "--extra-vars", json.dumps(deploy_config["vars"]), "--limit", deploy_config.get("limit", ""), "--tags", deploy_config.get("tags", "") ] ) # Отправка логов в реальном времени current_host = None async for line in process.stdout: decoded_line = line.decode() # Парсинг строки для определения хоста host_match = re.search(r'\[([^\]]+)\]', decoded_line) if host_match: current_host = host_match.group(1) # Отправка с метаданными await websocket.send_json({ "type": "log", "host": current_host, "data": decoded_line, "timestamp": datetime.now().isoformat(), "level": detect_log_level(decoded_line) # info, warning, error }) # Отправка статуса хоста if "PLAY RECAP" in decoded_line or "ok=" in decoded_line: status = parse_play_recap(decoded_line) await websocket.send_json({ "type": "host_status", "host": current_host, "status": status["status"], # success, failed, changed "changed": status.get("changed", 0), "ok": status.get("ok", 0), "failed": status.get("failed", 0) }) # Финальный статус await websocket.send_json({ "type": "complete", "status": "success" if process.returncode == 0 else "failed", "summary": await generate_deploy_summary(deploy_id) }) ``` ### 1.4. Сохранение истории деплоев **Автоматическое сохранение в БД:** ```python # app/db/models.py class DeployHistory(Base): __tablename__ = "deploy_history" id = Column(Integer, primary_key=True) role_name = Column(String, nullable=False) inventory = Column(String, nullable=False) started_at = Column(DateTime, nullable=False) completed_at = Column(DateTime) status = Column(String) # success, failed, cancelled duration = Column(Integer) # секунды # Параметры деплоя variables = Column(JSON) # Переменные роли limit = Column(String) # Ограничение хостов tags = Column(String) # Теги для выполнения # Результаты total_hosts = Column(Integer) successful_hosts = Column(Integer) failed_hosts = Column(Integer) changed_hosts = Column(Integer) # Логи logs = Column(Text) # Полные логи summary = Column(Text) # Краткое резюме # Пользователь user = Column(String) dry_run = Column(Boolean, default=False) ``` **Интерфейс истории:** ``` ┌─────────────────────────────────────────────────────────┐ │ История деплоев │ ├─────────────────────────────────────────────────────────┤ │ │ │ 📋 Фильтры: │ │ [Роль: nginx ▼] [Статус: Все ▼] [Дата: Последние 7 дней]│ │ │ │ 📊 Таблица деплоев: │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Дата | Роль | Inventory | Статус | Действия│ │ ├─────────────────────────────────────────────────┤ │ │ │ 15.01 10:30| nginx | production| ✅ | [👁️][📥]│ │ │ 15.01 09:15| nginx | staging | ✅ | [👁️][📥]│ │ │ 14.01 16:45| docker| production| ❌ | [👁️][📥]│ │ │ 14.01 14:20| python| production| ✅ | [👁️][📥]│ │ └─────────────────────────────────────────────────┘ │ │ │ │ [📥 Экспорт в CSV] [📥 Экспорт в JSON] │ └─────────────────────────────────────────────────────────┘ ``` --- ## 2. 📤 Экспорт ролей в отдельные Git репозитории ### 2.1. Страница экспорта роли **URL:** `/roles/{role_name}/export` **Интерфейс:** ``` ┌─────────────────────────────────────────────────────────┐ │ Экспорт роли: nginx в отдельный репозиторий │ ├─────────────────────────────────────────────────────────┤ │ │ │ 📋 Шаг 1: Настройка репозитория │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Тип экспорта: │ │ │ │ ○ Создать новый репозиторий │ │ │ │ ● Использовать существующий │ │ │ │ │ │ │ │ URL репозитория: │ │ │ │ [https://github.com/user/ansible-role-nginx] │ │ │ │ │ │ │ │ Ветка: [main ▼] [master] [develop] │ │ │ │ │ │ │ │ Аутентификация: │ │ │ │ ○ SSH ключ (из настроек) │ │ │ │ ● Personal Access Token │ │ │ │ [token: ****************] │ │ │ │ │ │ │ │ [🔐 Тест подключения] │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ 📦 Шаг 2: Выбор компонентов для экспорта │ │ ┌─────────────────────────────────────────────────┐ │ │ │ ☑ Структура роли (tasks, handlers, etc.) │ │ │ │ ☑ defaults/main.yml │ │ │ │ ☑ vars/main.yml │ │ │ │ ☑ templates/ │ │ │ │ ☑ files/ │ │ │ │ ☑ meta/main.yml │ │ │ │ ☑ README.md │ │ │ │ ☐ molecule/ (конфигурация тестирования) │ │ │ │ ☐ tests/ (тестовые playbook'и) │ │ │ │ ☐ .github/ (CI/CD workflows) │ │ │ │ │ │ │ │ ⚠️ Секреты (vars/main.yml): │ │ │ │ ○ Экспортировать зашифрованными │ │ │ │ ● Экспортировать без секретов (оставить пустыми)│ │ │ ○ Не экспортировать │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ 📝 Шаг 3: Метаданные и версионирование │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Версия: [1.0.0] │ │ │ │ Тег: [v1.0.0] (создать git tag) │ │ │ │ │ │ │ │ Описание изменений: │ │ │ │ [Исправлена конфигурация nginx...] │ │ │ │ │ │ │ │ CHANGELOG.md: │ │ │ │ ☑ Автоматически обновить │ │ │ │ ☑ Создать release notes │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ 📋 Шаг 4: Предпросмотр │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Структура репозитория: │ │ │ │ ansible-role-nginx/ │ │ │ │ ├── tasks/ │ │ │ │ ├── handlers/ │ │ │ │ ├── defaults/ │ │ │ │ ├── vars/ │ │ │ │ ├── templates/ │ │ │ │ ├── files/ │ │ │ │ ├── meta/ │ │ │ │ ├── README.md │ │ │ │ └── .gitignore │ │ │ │ │ │ │ │ Файлов для экспорта: 47 │ │ │ │ Размер: 2.3 MB │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ [👁️ Предпросмотр файлов] [📤 Экспортировать] │ └─────────────────────────────────────────────────────────┘ ``` ### 2.2. Процесс экспорта с прогрессом **После нажатия "Экспортировать":** ``` ┌─────────────────────────────────────────────────────────┐ │ Экспорт роли: nginx │ ├─────────────────────────────────────────────────────────┤ │ │ │ 📊 Прогресс: │ │ ┌─────────────────────────────────────────────────┐ │ │ │ ████████████████████░░ 90% │ │ │ │ Этап: Загрузка в репозиторий │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ 📋 Этапы: │ │ ✅ Подготовка файлов │ │ │ ✅ Обработка секретов │ │ │ ✅ Создание .gitignore │ │ │ ✅ Клонирование репозитория │ │ │ ✅ Копирование файлов │ │ │ 🟢 Загрузка в репозиторий (git push) │ │ │ ⏳ Создание тега │ │ │ ⏳ Создание release │ │ │ │ │ 📝 Логи: │ │ ┌─────────────────────────────────────────────────┐ │ │ │ [10:35:15] Подготовка файлов... │ │ │ │ [10:35:16] Обработка секретов... │ │ │ │ [10:35:17] Клонирование репозитория... │ │ │ │ [10:35:20] Копирование 47 файлов... │ │ │ │ [10:35:22] git add . │ │ │ │ [10:35:23] git commit -m "v1.0.0: ..." │ │ │ │ [10:35:25] git push origin main... │ │ │ │ ... │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ [✅ Экспорт завершен] [🔗 Открыть репозиторий] │ └─────────────────────────────────────────────────────────┘ ``` ### 2.3. Реализация экспорта ```python # app/services/export_service.py from git import Repo import shutil from pathlib import Path class ExportService: async def export_role( self, role_name: str, repo_url: str, branch: str = "main", version: str = None, components: List[str] = None, include_secrets: bool = False ) -> dict: """Экспорт роли в Git репозиторий""" # 1. Подготовка файлов role_path = Path(f"roles/{role_name}") temp_dir = Path(f"/tmp/export_{role_name}") temp_dir.mkdir(exist_ok=True) # 2. Копирование выбранных компонентов components = components or ["tasks", "handlers", "defaults", "meta", "README.md"] for component in components: src = role_path / component if src.exists(): if src.is_dir(): shutil.copytree(src, temp_dir / component) else: shutil.copy2(src, temp_dir / component) # 3. Обработка секретов if "vars" in components: vars_file = role_path / "vars" / "main.yml" if vars_file.exists(): if include_secrets: # Копировать зашифрованным shutil.copy2(vars_file, temp_dir / "vars" / "main.yml") else: # Создать без секретов self.create_vars_without_secrets(vars_file, temp_dir / "vars" / "main.yml") # 4. Создание .gitignore self.create_gitignore(temp_dir) # 5. Клонирование/обновление репозитория repo_dir = Path(f"/tmp/repo_{role_name}") if repo_dir.exists(): repo = Repo(repo_dir) repo.remote().pull() else: repo = Repo.clone_from(repo_url, repo_dir, branch=branch) # 6. Копирование файлов в репозиторий for item in temp_dir.iterdir(): dest = repo_dir / item.name if dest.exists(): if dest.is_dir(): shutil.rmtree(dest) else: dest.unlink() if item.is_dir(): shutil.copytree(item, dest) else: shutil.copy2(item, dest) # 7. Коммит и push repo.git.add(A=True) commit_message = f"{version}: {self.get_changelog_entry(role_name, version)}" repo.index.commit(commit_message) repo.remote().push() # 8. Создание тега if version: repo.create_tag(f"v{version}") repo.remote().push(tags=True) # 9. Очистка shutil.rmtree(temp_dir) shutil.rmtree(repo_dir) return { "success": True, "repo_url": repo_url, "version": version, "commit": repo.head.commit.hexsha } ``` --- ## 3. 📥 Импорт ролей из других репозиториев ### 3.1. Страница импорта роли **URL:** `/roles/import` **Интерфейс:** ``` ┌─────────────────────────────────────────────────────────┐ │ Импорт роли из репозитория │ ├─────────────────────────────────────────────────────────┤ │ │ │ 📋 Шаг 1: Источник роли │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Тип источника: │ │ │ │ ○ Git репозиторий │ │ │ │ ● Ansible Galaxy │ │ │ │ ○ Локальный архив (tar.gz, zip) │ │ │ │ ○ URL (прямая ссылка на архив) │ │ │ │ │ │ │ │ ┌─ Git репозиторий ─────────────────────────┐ │ │ │ │ │ URL: │ │ │ │ │ │ [https://github.com/user/ansible-role-nginx]│ │ │ │ │ │ │ │ │ │ │ │ Ветка/тег: [main ▼] [v1.0.0] [latest] │ │ │ │ │ │ │ │ │ │ │ │ Путь к роли (если не в корне): │ │ │ │ │ │ [/roles/nginx] (оставить пустым если в корне)│ │ │ │ │ └────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ ┌─ Ansible Galaxy ───────────────────────────┐ │ │ │ │ │ Роль: [geerlingguy.nginx] │ │ │ │ │ │ Версия: [latest ▼] [1.0.0] [0.9.0] │ │ │ │ │ └────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ [🔍 Проверить доступность] │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ 📝 Шаг 2: Настройка импорта │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Имя роли в проекте: │ │ │ │ [nginx] (автоматически из источника) │ │ │ │ │ │ │ │ Действие при конфликте: │ │ │ │ ○ Пропустить (не импортировать) │ │ │ │ ● Перезаписать существующую роль │ │ │ │ ○ Создать копию (nginx-imported) │ │ │ │ │ │ │ │ Импортировать: │ │ │ │ ☑ Структуру роли │ │ │ │ ☑ molecule конфигурацию (если есть) │ │ │ │ ☑ tests/ (тестовые playbook'и) │ │ │ │ ☐ .github/ (CI/CD workflows) │ │ │ │ ☐ Документацию (если отличается) │ │ │ │ │ │ │ │ Обработка зависимостей: │ │ │ │ ○ Импортировать зависимости автоматически │ │ │ │ ● Показать список для подтверждения │ │ │ │ ○ Пропустить зависимости │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ 📋 Шаг 3: Предпросмотр │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Информация о роли: │ │ │ │ ├─ Название: nginx │ │ │ │ ├─ Версия: 1.0.0 │ │ │ │ ├─ Автор: geerlingguy │ │ │ │ ├─ Описание: Install and configure nginx │ │ │ │ ├─ Платформы: Ubuntu, Debian, CentOS, RHEL │ │ │ │ └─ Зависимости: common (geerlingguy.common) │ │ │ │ │ │ │ │ Структура: │ │ │ │ roles/nginx/ │ │ │ │ ├── tasks/ (12 файлов) │ │ │ │ ├── handlers/ (2 файла) │ │ │ │ ├── defaults/ (1 файл) │ │ │ │ ├── templates/ (5 файлов) │ │ │ │ ├── meta/ (1 файл) │ │ │ │ └── README.md │ │ │ │ │ │ │ │ Файлов: 23 │ │ │ │ Размер: 1.8 MB │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ [👁️ Предпросмотр файлов] [📥 Импортировать] │ └─────────────────────────────────────────────────────────┘ ``` ### 3.2. Процесс импорта **После нажатия "Импортировать":** ``` ┌─────────────────────────────────────────────────────────┐ │ Импорт роли: nginx │ ├─────────────────────────────────────────────────────────┤ │ │ │ 📊 Прогресс: │ │ ┌─────────────────────────────────────────────────┐ │ │ │ ████████████████████░░ 95% │ │ │ │ Этап: Интеграция в проект │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ 📋 Этапы: │ │ ✅ Клонирование репозитория │ │ │ ✅ Извлечение роли │ │ │ ✅ Проверка структуры │ │ │ ✅ Копирование файлов │ │ │ 🟢 Интеграция в проект (обновление deploy.yml) │ │ │ ⏳ Импорт зависимостей │ │ │ │ │ 📝 Логи: │ │ ┌─────────────────────────────────────────────────┐ │ │ │ [10:40:15] Клонирование репозитория... │ │ │ │ [10:40:18] Извлечение роли nginx... │ │ │ │ [10:40:19] Проверка структуры... │ │ │ │ [10:40:20] Копирование 23 файлов... │ │ │ │ [10:40:22] Обновление roles/deploy.yml... │ │ │ │ [10:40:23] Импорт зависимости: common... │ │ │ │ ... │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ [✅ Импорт завершен] [👁️ Просмотреть роль] │ └─────────────────────────────────────────────────────────┘ ``` ### 3.3. Реализация импорта ```python # app/services/import_service.py from git import Repo import shutil from pathlib import Path import ansible_galaxy class ImportService: async def import_role( self, source: str, source_type: str, # git, galaxy, archive, url role_name: str = None, version: str = None, conflict_action: str = "overwrite" # skip, overwrite, copy ) -> dict: """Импорт роли из внешнего источника""" role_path = Path(f"roles/{role_name}") # Проверка конфликтов if role_path.exists() and conflict_action == "skip": return {"success": False, "error": "Роль уже существует"} # Импорт в зависимости от типа if source_type == "git": await self.import_from_git(source, role_name, version) elif source_type == "galaxy": await self.import_from_galaxy(source, role_name, version) elif source_type == "archive": await self.import_from_archive(source, role_name) elif source_type == "url": await self.import_from_url(source, role_name) # Обработка конфликтов if conflict_action == "copy" and role_path.exists(): role_path = Path(f"roles/{role_name}-imported") # Интеграция в проект await self.integrate_role(role_name) # Импорт зависимостей dependencies = await self.get_role_dependencies(role_name) if dependencies: await self.import_dependencies(dependencies) return { "success": True, "role_name": role_name, "path": str(role_path), "dependencies": dependencies } async def import_from_git(self, repo_url: str, role_name: str, version: str = None): """Импорт из Git репозитория""" temp_dir = Path(f"/tmp/import_{role_name}") temp_dir.mkdir(exist_ok=True) # Клонирование repo = Repo.clone_from(repo_url, temp_dir) if version: repo.git.checkout(version) # Поиск роли в репозитории role_source = self.find_role_in_repo(temp_dir, role_name) # Копирование role_path = Path(f"roles/{role_name}") if role_path.exists(): shutil.rmtree(role_path) shutil.copytree(role_source, role_path) # Очистка shutil.rmtree(temp_dir) async def import_from_galaxy(self, role_id: str, role_name: str, version: str = None): """Импорт из Ansible Galaxy""" # Использование ansible-galaxy import subprocess cmd = ["ansible-galaxy", "install", role_id] if version: cmd.extend(["--version", version]) subprocess.run(cmd, cwd="roles") # Переименование если нужно installed_name = role_id.replace(".", "-") if installed_name != role_name: Path(f"roles/{installed_name}").rename(f"roles/{role_name}") async def integrate_role(self, role_name: str): """Интеграция роли в проект""" # Обновление roles/deploy.yml await self.update_deploy_yml(role_name) # Создание molecule конфигурации если нужно await self.setup_molecule_config(role_name) # Обновление документации await self.update_project_docs(role_name) ``` --- ## 4. 📊 История команд и результатов ### 4.1. База данных для истории ```python # app/db/models.py class CommandHistory(Base): __tablename__ = "command_history" id = Column(Integer, primary_key=True) command = Column(String, nullable=False) # make role test nginx command_type = Column(String) # test, deploy, export, import role_name = Column(String) started_at = Column(DateTime, nullable=False) completed_at = Column(DateTime) status = Column(String) # success, failed, cancelled, running duration = Column(Integer) # секунды # Параметры parameters = Column(JSON) # Все параметры команды # Результаты stdout = Column(Text) stderr = Column(Text) return_code = Column(Integer) # Пользователь user = Column(String) ip_address = Column(String) # Связи test_result_id = Column(Integer, ForeignKey("test_results.id")) deploy_id = Column(Integer, ForeignKey("deploy_history.id")) ``` ### 4.2. Интерфейс истории команд ``` ┌─────────────────────────────────────────────────────────┐ │ История команд │ ├─────────────────────────────────────────────────────────┤ │ │ │ 📋 Фильтры: │ │ [Тип: Все ▼] [Роль: Все ▼] [Статус: Все ▼] │ │ [Дата: Последние 7 дней ▼] │ │ │ │ 📊 Таблица команд: │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Дата | Команда | Роль | Статус | Действия │ │ │ ├─────────────────────────────────────────────────┤ │ │ │ 15.01 10:30| test | nginx| ✅ | [👁️][📥]│ │ │ 15.01 09:15| deploy | nginx| ✅ | [👁️][📥]│ │ │ 15.01 08:45| export | nginx| ✅ | [👁️][📥]│ │ │ 14.01 16:30| import | docker| ✅ | [👁️][📥]│ │ │ 14.01 15:20| test | python| ❌ | [👁️][📥]│ │ └─────────────────────────────────────────────────┘ │ │ │ │ 📈 Статистика: │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Всего команд: 127 │ │ │ │ Успешных: 115 (90.6%) │ │ │ │ Ошибок: 12 (9.4%) │ │ │ │ Среднее время: 2m 15s │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ [📥 Экспорт в CSV] [📥 Экспорт в JSON] │ └─────────────────────────────────────────────────────────┘ ``` ### 4.3. Детали команды **При клике на команду:** ``` ┌─────────────────────────────────────────────────────────┐ │ Детали команды: make role test nginx default │ ├─────────────────────────────────────────────────────────┤ │ │ │ 📋 Информация: │ │ ┌─────────────────────────────────────────────────┐ │ │ │ Команда: make role test nginx default │ │ │ │ Тип: test │ │ │ │ Роль: nginx │ │ │ │ Preset: default │ │ │ │ Статус: ✅ Успешно │ │ │ │ Начало: 15.01.2024 10:30:15 │ │ │ │ Завершение: 15.01.2024 10:32:30 │ │ │ │ Длительность: 2m 15s │ │ │ │ Пользователь: admin │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ 📝 Параметры: │ │ ┌─────────────────────────────────────────────────┐ │ │ │ { │ │ │ │ "role": "nginx", │ │ │ │ "preset": "default", │ │ │ │ "parallel": false, │ │ │ │ "lint": true │ │ │ │ } │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ 📄 Вывод (stdout): │ │ ┌─────────────────────────────────────────────────┐ │ │ │ [Показать полный вывод] │ │ │ │ ... │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ ❌ Ошибки (stderr): │ │ ┌─────────────────────────────────────────────────┐ │ │ │ (нет ошибок) │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ [📥 Скачать логи] [🔄 Повторить команду] │ └─────────────────────────────────────────────────────────┘ ``` --- ## 5. 🔄 Автоматическое сохранение истории ### 5.1. Middleware для логирования команд ```python # app/core/command_logger.py from app.db.database import SessionLocal from app.db.models import CommandHistory from datetime import datetime class CommandLogger: async def log_command( self, command: str, command_type: str, parameters: dict, user: str = None ) -> int: """Сохранение команды в историю""" db = SessionLocal() history = CommandHistory( command=command, command_type=command_type, role_name=parameters.get("role_name"), started_at=datetime.now(), status="running", parameters=parameters, user=user ) db.add(history) db.commit() return history.id async def update_command( self, history_id: int, status: str, stdout: str = None, stderr: str = None, return_code: int = None ): """Обновление результата команды""" db = SessionLocal() history = db.query(CommandHistory).filter_by(id=history_id).first() history.completed_at = datetime.now() history.status = status history.stdout = stdout history.stderr = stderr history.return_code = return_code history.duration = (history.completed_at - history.started_at).seconds db.commit() ``` ### 5.2. Интеграция с MakeExecutor ```python # app/core/make_executor.py class MakeExecutor: def __init__(self): self.logger = CommandLogger() async def execute( self, command: str, args: List[str] = None, user: str = None ) -> Dict: """Выполнение команды с логированием""" # Сохранение в историю history_id = await self.logger.log_command( command=command, command_type=self.detect_command_type(command), parameters=self.parse_parameters(command, args), user=user ) # Выполнение команды cmd = ["make"] + command.split() + (args or []) result = subprocess.run( cmd, capture_output=True, text=True, cwd=PROJECT_ROOT ) # Обновление истории await self.logger.update_command( history_id=history_id, status="success" if result.returncode == 0 else "failed", stdout=result.stdout, stderr=result.stderr, return_code=result.returncode ) return { "success": result.returncode == 0, "stdout": result.stdout, "stderr": result.stderr, "history_id": history_id } ``` --- ## 📝 Резюме Все функции доступны через веб-интерфейс: 1. **Деплой на живые серверы:** - Выбор inventory - Настройка переменных - Live логи через WebSocket - Сохранение истории 2. **Экспорт ролей:** - В отдельные Git репозитории - С версионированием и тегами - С обработкой секретов 3. **Импорт ролей:** - Из Git репозиториев - Из Ansible Galaxy - С автоматической интеграцией 4. **История команд:** - Автоматическое сохранение всех команд - Детальные логи - Статистика и аналитика Всё с live обновлениями, сохранением истории и удобным интерфейсом! --- **Автор:** Сергей Антропов **Сайт:** https://devops.org.ru