- Добавлена колонка 'Тип' во все таблицы истории сборок - Для push операций отображается registry вместо платформ - Сохранение пользователя при создании push лога - Исправлена ошибка с logger в push_docker_image endpoint - Улучшено отображение истории сборок с визуальными индикаторами
986 lines
57 KiB
Markdown
986 lines
57 KiB
Markdown
# Деплой, импорт и экспорт ролей в веб-интерфейсе
|
||
|
||
**Автор:** Сергей Антропов
|
||
**Сайт:** 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
|