feat: добавлена пометка типа операции (Build/Push) в истории сборок Dockerfile
- Добавлена колонка 'Тип' во все таблицы истории сборок - Для push операций отображается registry вместо платформ - Сохранение пользователя при создании push лога - Исправлена ошибка с logger в push_docker_image endpoint - Улучшено отображение истории сборок с визуальными индикаторами
This commit is contained in:
985
docs/WEB_INTERFACE_DEPLOY_IMPORT_EXPORT.md
Normal file
985
docs/WEB_INTERFACE_DEPLOY_IMPORT_EXPORT.md
Normal file
@@ -0,0 +1,985 @@
|
||||
# Деплой, импорт и экспорт ролей в веб-интерфейсе
|
||||
|
||||
**Автор:** Сергей Антропов
|
||||
**Сайт:** 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
|
||||
Reference in New Issue
Block a user