feat: добавлена пометка типа операции (Build/Push) в истории сборок Dockerfile

- Добавлена колонка 'Тип' во все таблицы истории сборок
- Для push операций отображается registry вместо платформ
- Сохранение пользователя при создании push лога
- Исправлена ошибка с logger в push_docker_image endpoint
- Улучшено отображение истории сборок с визуальными индикаторами
This commit is contained in:
Сергей Антропов
2026-02-15 22:59:02 +03:00
parent 23e1a6037b
commit 1fbf9185a2
232 changed files with 38075 additions and 5 deletions

View File

@@ -0,0 +1 @@
# Endpoints package

View File

@@ -0,0 +1,184 @@
"""
API endpoints для аутентификации
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from fastapi import APIRouter, Request, HTTPException, Depends, status, Form
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from fastapi.security import OAuth2PasswordRequestForm
from pathlib import Path
from datetime import timedelta
from app.core.config import settings
from app.auth.security import create_access_token
from app.auth.deps import get_current_user
from app.db.session import get_async_db
from app.services.user_service import UserService
from sqlalchemy.ext.asyncio import AsyncSession
router = APIRouter()
templates_path = Path(__file__).parent.parent.parent.parent / "templates"
templates = Jinja2Templates(directory=str(templates_path))
@router.get("/login", response_class=HTMLResponse)
async def login_page(request: Request):
"""Страница входа"""
return templates.TemplateResponse(
"pages/auth/login.html",
{"request": request}
)
@router.post("/api/v1/auth/login")
async def login(
form_data: OAuth2PasswordRequestForm = Depends(),
db: AsyncSession = Depends(get_async_db)
):
"""API endpoint для входа"""
# Убеждаемся, что пользователь admin существует
await UserService.ensure_admin_user(db)
# Получаем пользователя из БД
user = await UserService.get_user_by_username(db, form_data.username)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Неверное имя пользователя или пароль",
headers={"WWW-Authenticate": "Bearer"},
)
# Проверяем пароль
if not await UserService.verify_user_password(user, form_data.password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Неверное имя пользователя или пароль",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Пользователь отключен"
)
# Если пароль был "admin" и не хеширован, обновляем его
if user.hashed_password == "admin" or len(user.hashed_password) < 50:
await UserService.update_password(db, user, form_data.password)
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username},
expires_delta=access_token_expires
)
response = JSONResponse(content={
"access_token": access_token,
"token_type": "bearer"
})
# Устанавливаем токен в cookie
response.set_cookie(
key="access_token",
value=access_token,
max_age=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
httponly=True,
secure=False, # В продакшене должно быть True для HTTPS
samesite="lax"
)
return response
@router.get("/logout")
@router.head("/logout") # Поддержка HEAD запросов
async def logout():
"""Выход (удаляет токен из cookie и перенаправляет на страницу входа)
Доступен по путям:
- /logout (через прямой router в main.py)
- /api/v1/auth/logout (через api_router с префиксом /auth)
"""
from fastapi.responses import RedirectResponse
response = RedirectResponse(url="/login", status_code=302)
response.delete_cookie(
key="access_token",
httponly=True,
secure=False,
samesite="lax"
)
return response
@router.get("/api/v1/auth/me")
async def get_current_user_info(
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""Получение информации о текущем пользователе"""
username = current_user.get("username")
if username:
user = await UserService.get_user_by_username(db, username)
if user:
return {
"username": user.username,
"is_active": user.is_active,
"is_superuser": user.is_superuser,
"created_at": user.created_at.isoformat() if user.created_at else None
}
return current_user
@router.get("/change-password", response_class=HTMLResponse)
async def change_password_page(request: Request, current_user: dict = Depends(get_current_user)):
"""Страница смены пароля"""
return templates.TemplateResponse(
"pages/auth/change-password.html",
{
"request": request,
"current_user": current_user
}
)
@router.post("/api/v1/auth/change-password")
async def change_password(
current_password: str = Form(...),
new_password: str = Form(...),
new_password_confirm: str = Form(...),
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""Смена пароля пользователя"""
# Проверка совпадения паролей
if new_password != new_password_confirm:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Новые пароли не совпадают"
)
username = current_user.get("username")
if not username:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Пользователь не авторизован"
)
user = await UserService.get_user_by_username(db, username)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пользователь не найден"
)
# Проверяем текущий пароль
if not await UserService.verify_user_password(user, current_password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Неверный текущий пароль"
)
# Обновляем пароль
await UserService.update_password(db, user, new_password)
return {"message": "Пароль успешно изменен"}

View File

@@ -0,0 +1,254 @@
"""
API endpoints для деплоя на живые серверы
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from fastapi import APIRouter, Request, WebSocket, WebSocketDisconnect, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from pathlib import Path
from typing import Dict, Optional, List
import yaml
import json
from app.core.config import settings
from app.services.deployment_service import DeploymentService
router = APIRouter()
templates_path = Path(__file__).parent.parent.parent.parent / "templates"
templates = Jinja2Templates(directory=str(templates_path))
deployment_service = DeploymentService()
@router.get("/deploy", response_class=HTMLResponse)
async def deploy_page(request: Request):
"""Страница деплоя"""
# Проверка наличия inventory
inventory_file = settings.PROJECT_ROOT / "inventory" / "hosts.ini"
inventory_exists = inventory_file.exists()
# Чтение inventory если существует
inventory_content = ""
inventory_data = {}
if inventory_exists:
try:
inventory_content = inventory_file.read_text()
# Парсинг inventory для отображения групп и хостов
inventory_data = parse_inventory(inventory_content)
except Exception:
pass
# Получение списка ролей
roles_dir = settings.PROJECT_ROOT / "roles"
roles = []
if roles_dir.exists():
for role_dir in roles_dir.iterdir():
if role_dir.is_dir() and role_dir.name != "deploy.yml":
roles.append(role_dir.name)
return templates.TemplateResponse(
"pages/deploy/index.html",
{
"request": request,
"inventory_exists": inventory_exists,
"inventory_content": inventory_content,
"inventory_data": inventory_data,
"roles": sorted(roles)
}
)
@router.get("/deploy/inventory", response_class=HTMLResponse)
async def inventory_page(request: Request):
"""Страница управления inventory"""
inventory_file = settings.PROJECT_ROOT / "inventory" / "hosts.ini"
inventory_exists = inventory_file.exists()
inventory_content = ""
if inventory_exists:
inventory_content = inventory_file.read_text()
return templates.TemplateResponse(
"pages/deploy/inventory.html",
{
"request": request,
"inventory_exists": inventory_exists,
"inventory_content": inventory_content
}
)
@router.post("/api/v1/deploy/inventory")
async def save_inventory(request: Request):
"""Сохранение inventory файла"""
form_data = await request.form()
content = form_data.get("content", "")
if not content:
return JSONResponse(
status_code=400,
content={"success": False, "message": "Содержимое inventory не может быть пустым"}
)
inventory_dir = settings.PROJECT_ROOT / "inventory"
inventory_dir.mkdir(parents=True, exist_ok=True)
inventory_file = inventory_dir / "hosts.ini"
inventory_file.write_text(content)
return JSONResponse(
content={
"success": True,
"message": "Inventory файл успешно сохранен"
}
)
@router.post("/api/v1/deploy/start")
async def start_deploy(
role_name: Optional[str] = None,
tags: Optional[str] = None,
limit: Optional[str] = None,
check: bool = False,
extra_vars: Optional[str] = None
):
"""Запуск деплоя"""
# TODO: Запуск через Celery для фонового выполнения
# Пока просто возвращаем информацию
deploy_id = f"deploy-{role_name or 'all'}-{tags or 'none'}"
return {
"success": True,
"deploy_id": deploy_id,
"role_name": role_name,
"tags": tags,
"limit": limit,
"check": check,
"message": "Деплой запущен"
}
@router.websocket("/ws/deploy/{deploy_id}")
async def deploy_websocket(websocket: WebSocket, deploy_id: str):
"""WebSocket для live логов деплоя"""
await websocket.accept()
try:
# Ждем сообщение от клиента с параметрами деплоя
message = await websocket.receive_json()
if message.get("type") != "start":
await websocket.send_json({
"type": "error",
"data": "Ожидается сообщение типа 'start' с параметрами деплоя"
})
await websocket.close()
return
# Получаем параметры из сообщения
role_name = message.get("role_name")
inventory = message.get("inventory", "inventory/hosts.ini")
limit = message.get("limit")
tags = message.get("tags")
check = message.get("check", False)
extra_vars = message.get("extra_vars")
if not role_name:
await websocket.send_json({
"type": "error",
"data": "Не указано имя роли"
})
await websocket.close()
return
# Отправка начального сообщения
await websocket.send_json({
"type": "info",
"data": f"🚀 Запуск деплоя роли '{role_name}'..."
})
if check:
await websocket.send_json({
"type": "warning",
"data": "⚠️ Режим dry-run (--check) - изменения не будут применены"
})
# Запуск деплоя через DeploymentService
async for line in deployment_service.deploy_role(
role_name=role_name,
inventory=inventory,
limit=limit,
tags=tags,
check=check,
extra_vars=extra_vars,
stream=True
):
line = line.rstrip()
if not line:
continue
# Определение типа лога
log_type = deployment_service.detect_log_level(line)
await websocket.send_json({
"type": "log",
"level": log_type,
"data": line
})
# Завершение
await websocket.send_json({
"type": "complete",
"status": "success",
"data": "✅ Деплой завершен успешно"
})
except WebSocketDisconnect:
pass
except Exception as e:
import traceback
error_msg = f"❌ Ошибка: {str(e)}\n{traceback.format_exc()}"
await websocket.send_json({
"type": "error",
"data": error_msg
})
finally:
try:
await websocket.close()
except:
pass
def parse_inventory(content: str) -> Dict:
"""Парсинг inventory файла для отображения структуры"""
result = {
"groups": {},
"hosts": []
}
current_group = None
for line in content.split("\n"):
line = line.strip()
if not line or line.startswith("#"):
continue
# Группа
if line.startswith("[") and line.endswith("]"):
group_name = line[1:-1]
if ":" in group_name:
group_name = group_name.split(":")[0]
current_group = group_name
if current_group not in result["groups"]:
result["groups"][current_group] = []
# Хост
elif current_group and " " in line:
host_parts = line.split()
host_name = host_parts[0]
result["groups"][current_group].append(host_name)
result["hosts"].append({
"name": host_name,
"group": current_group,
"vars": " ".join(host_parts[1:]) if len(host_parts) > 1 else ""
})
return result

View File

@@ -0,0 +1,116 @@
"""
API endpoints для управления Docker образами
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from fastapi import APIRouter, Request, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from pathlib import Path
from typing import List, Dict
from app.core.config import settings
from app.core.docker_client import DockerClient
router = APIRouter()
templates_path = Path(__file__).parent.parent.parent.parent / "templates"
templates = Jinja2Templates(directory=str(templates_path))
def get_docker_client():
"""Получение Docker клиента с обработкой ошибок"""
try:
return DockerClient()
except Exception:
return None
@router.get("/docker", response_class=HTMLResponse)
async def docker_page(request: Request):
"""Страница управления Docker образами"""
docker_client = get_docker_client()
images = []
if docker_client:
try:
images = docker_client.list_images()
except Exception:
pass
return templates.TemplateResponse(
"pages/docker/index.html",
{
"request": request,
"images": images
}
)
@router.get("/api/v1/docker/images", response_class=HTMLResponse)
async def get_docker_images():
"""API endpoint для получения списка Docker образов"""
docker_client = get_docker_client()
if not docker_client:
return """
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i>
<strong>Docker недоступен</strong>
<br><small class="text-muted">Убедитесь, что Docker запущен и доступен. Проверьте, что Docker socket доступен: <code>/var/run/docker.sock</code></small>
</div>
"""
try:
images = docker_client.list_images()
if not images:
return """
<div class="text-center py-5">
<i class="fab fa-docker fa-3x text-muted mb-3"></i>
<p class="text-muted">Docker образы не найдены</p>
</div>
"""
html = '<div class="table-responsive"><table class="table table-hover"><thead><tr><th>ID</th><th>Теги</th><th>Размер</th><th>Создан</th></tr></thead><tbody>'
for img in images:
size_mb = (img.get("size", 0) / 1024 / 1024) if img.get("size") else 0
size_str = f"{size_mb:.2f} MB" if size_mb > 0 else "N/A"
tags_list = img.get("tags", [])
if tags_list:
tags = " ".join([f'<span class="badge bg-info me-1">{tag}</span>' for tag in tags_list])
else:
tags = '<span class="text-muted">нет тегов</span>'
img_id = img.get("id", "")[:12] if img.get("id") else "N/A"
created = img.get("created", "")
if created:
try:
# Парсим ISO формат даты или timestamp
from datetime import datetime
if isinstance(created, (int, float)):
# Unix timestamp
dt = datetime.fromtimestamp(created)
created_str = dt.strftime('%d.%m.%Y %H:%M')
elif isinstance(created, str):
# ISO формат
try:
dt = datetime.fromisoformat(created.replace('Z', '+00:00'))
except:
# Пробуем другой формат
dt = datetime.strptime(created.split('.')[0], '%Y-%m-%dT%H:%M:%S')
created_str = dt.strftime('%d.%m.%Y %H:%M')
else:
created_str = str(created)
except Exception as e:
created_str = str(created)
else:
created_str = "N/A"
html += f'<tr><td><code>{img_id}</code></td><td>{tags}</td><td>{size_str}</td><td>{created_str}</td></tr>'
html += '</tbody></table></div>'
return html
except Exception as e:
return f"""
<div class="alert alert-danger">
<i class="fas fa-exclamation-circle me-2"></i>
<strong>Ошибка при получении списка образов:</strong> {str(e)}
</div>
"""

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,86 @@
"""
API endpoints для экспорта ролей
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from fastapi import APIRouter, Request, HTTPException, Form
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from pathlib import Path
from typing import List, Optional
import json
from app.core.config import settings
from app.services.export_service import ExportService
router = APIRouter()
templates_path = Path(__file__).parent.parent.parent.parent / "templates"
templates = Jinja2Templates(directory=str(templates_path))
export_service = ExportService()
@router.get("/roles/{role_name}/export", response_class=HTMLResponse)
async def export_role_page(request: Request, role_name: str):
"""Страница экспорта роли"""
# Проверка существования роли
roles_dir = settings.PROJECT_ROOT / "roles" / role_name
if not roles_dir.exists():
raise HTTPException(status_code=404, detail=f"Роль '{role_name}' не найдена")
# Получение доступных компонентов
components = export_service.get_role_components(role_name)
return templates.TemplateResponse(
"pages/roles/export.html",
{
"request": request,
"role_name": role_name,
"components": components
}
)
@router.post("/api/v1/roles/{role_name}/export")
async def export_role_api(
role_name: str,
repo_url: str = Form(...),
branch: str = Form("main"),
version: Optional[str] = Form(None),
components: str = Form(""), # JSON строка
include_secrets: bool = Form(False),
commit_message: Optional[str] = Form(None)
):
"""API endpoint для экспорта роли"""
try:
# Парсинг компонентов
components_list = []
if components:
components_list = json.loads(components)
# Запуск экспорта через Celery (в будущем)
# Пока синхронно
result = await export_service.export_role(
role_name=role_name,
repo_url=repo_url,
branch=branch,
version=version,
components=components_list if components_list else None,
include_secrets=include_secrets,
commit_message=commit_message
)
return JSONResponse(content=result, status_code=200)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка при экспорте роли: {str(e)}")
@router.get("/api/v1/roles/{role_name}/export/components")
async def get_role_components_api(role_name: str):
"""Получение списка компонентов роли"""
try:
components = export_service.get_role_components(role_name)
return {"components": components}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,82 @@
"""
API endpoints для импорта ролей
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from fastapi import APIRouter, Request, HTTPException, Form
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from pathlib import Path
from typing import Optional
from app.core.config import settings
from app.services.import_service import ImportService
router = APIRouter()
templates_path = Path(__file__).parent.parent.parent.parent / "templates"
templates = Jinja2Templates(directory=str(templates_path))
import_service = ImportService()
@router.get("/roles/import", response_class=HTMLResponse)
async def import_role_page(request: Request):
"""Страница импорта роли"""
return templates.TemplateResponse(
"pages/roles/import.html",
{"request": request}
)
@router.post("/api/v1/roles/import/git")
async def import_from_git_api(
repo_url: str = Form(...),
role_name: Optional[str] = Form(None),
branch: str = Form("main"),
subdirectory: Optional[str] = Form(None)
):
"""API endpoint для импорта роли из Git репозитория"""
try:
result = await import_service.import_from_git(
repo_url=repo_url,
role_name=role_name,
branch=branch,
subdirectory=subdirectory
)
return JSONResponse(content=result, status_code=201)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка при импорте роли: {str(e)}")
@router.post("/api/v1/roles/import/galaxy")
async def import_from_galaxy_api(
role_name: str = Form(...),
version: Optional[str] = Form(None),
namespace: Optional[str] = Form(None)
):
"""API endpoint для импорта роли из Ansible Galaxy"""
try:
result = await import_service.import_from_galaxy(
role_name=role_name,
version=version,
namespace=namespace
)
return JSONResponse(content=result, status_code=201)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка при импорте роли: {str(e)}")
@router.post("/api/v1/roles/import/validate")
async def validate_repo_api(
repo_url: str = Form(...),
branch: str = Form("main")
):
"""Проверка доступности репозитория"""
try:
result = await import_service.validate_repo(repo_url, branch)
return JSONResponse(content=result)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,83 @@
"""
API endpoints для импорта ролей
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from fastapi import APIRouter, Request, HTTPException, Form
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from pathlib import Path
from typing import Optional
from app.core.config import settings
from app.services.import_service import ImportService
router = APIRouter()
templates_path = Path(__file__).parent.parent.parent.parent / "templates"
templates = Jinja2Templates(directory=str(templates_path))
import_service = ImportService()
@router.get("/roles/import", response_class=HTMLResponse)
async def import_role_page(request: Request):
"""Страница импорта роли"""
# Этот роут должен быть зарегистрирован ПЕРЕД /roles/{role_name}
return templates.TemplateResponse(
"pages/roles/import.html",
{"request": request}
)
@router.post("/api/v1/roles/import/git")
async def import_from_git_api(
repo_url: str = Form(...),
role_name: Optional[str] = Form(None),
branch: str = Form("main"),
subdirectory: Optional[str] = Form(None)
):
"""API endpoint для импорта роли из Git репозитория"""
try:
result = await import_service.import_from_git(
repo_url=repo_url,
role_name=role_name,
branch=branch,
subdirectory=subdirectory
)
return JSONResponse(content=result, status_code=201)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка при импорте роли: {str(e)}")
@router.post("/api/v1/roles/import/galaxy")
async def import_from_galaxy_api(
role_name: str = Form(...),
version: Optional[str] = Form(None),
namespace: Optional[str] = Form(None)
):
"""API endpoint для импорта роли из Ansible Galaxy"""
try:
result = await import_service.import_from_galaxy(
role_name=role_name,
version=version,
namespace=namespace
)
return JSONResponse(content=result, status_code=201)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка при импорте роли: {str(e)}")
@router.post("/api/v1/roles/import/validate")
async def validate_repo_api(
repo_url: str = Form(...),
branch: str = Form("main")
):
"""Проверка доступности репозитория"""
try:
result = await import_service.validate_repo(repo_url, branch)
return JSONResponse(content=result)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View File

@@ -0,0 +1,62 @@
"""
API endpoints для управления Kubernetes
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from fastapi import APIRouter, Request, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from pathlib import Path
from typing import List, Dict
from app.core.config import settings
from app.core.make_executor import MakeExecutor
router = APIRouter()
templates_path = Path(__file__).parent.parent.parent.parent / "templates"
templates = Jinja2Templates(directory=str(templates_path))
executor = MakeExecutor()
@router.get("/k8s", response_class=HTMLResponse)
async def k8s_page(request: Request):
"""Страница управления Kubernetes"""
return templates.TemplateResponse(
"pages/k8s/index.html",
{"request": request}
)
@router.get("/api/v1/k8s/clusters", response_class=HTMLResponse)
async def get_k8s_clusters():
"""API endpoint для получения списка K8s кластеров"""
try:
# Попытка получить кластеры через kind
result = executor.execute("kind get clusters", capture_output=True)
if result.returncode == 0 and result.stdout.strip():
clusters = [c.strip() for c in result.stdout.strip().split('\n') if c.strip()]
if clusters:
html = '<div class="table-responsive"><table class="table table-hover"><thead><tr><th>Имя кластера</th><th>Действия</th></tr></thead><tbody>'
for cluster in clusters:
html += f'<tr><td><code>{cluster}</code></td><td><button class="btn btn-sm btn-outline-primary">Детали</button></td></tr>'
html += '</tbody></table></div>'
return html
# Если кластеров нет или kind не установлен
return """
<div class="text-center py-5">
<i class="fas fa-cube fa-3x text-muted mb-3"></i>
<p class="text-muted mb-2">Kubernetes кластеры не найдены</p>
<p class="text-muted small">
Используйте команду <code>make k8s create kubernetes</code> для создания кластера через Kind.
</p>
</div>
"""
except Exception as e:
return f"""
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i>
<strong>Ошибка при получении списка кластеров:</strong> {str(e)}
<br><small class="text-muted">Убедитесь, что Kind установлен и доступен</small>
</div>
"""

View File

@@ -0,0 +1,132 @@
"""
API endpoints для проверки синтаксиса ролей (lint)
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from fastapi import APIRouter, Request, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from pathlib import Path
from typing import Optional
from app.core.config import settings
from app.services.lint_service import LintService
router = APIRouter()
templates_path = Path(__file__).parent.parent.parent.parent / "templates"
templates = Jinja2Templates(directory=str(templates_path))
lint_service = LintService()
@router.get("/lint", response_class=HTMLResponse)
async def lint_page(request: Request):
"""Страница проверки синтаксиса ролей"""
# Получение списка ролей
roles_dir = settings.PROJECT_ROOT / "roles"
roles = []
if roles_dir.exists():
for role_dir in roles_dir.iterdir():
if role_dir.is_dir() and role_dir.name != "deploy.yml":
roles.append({
"name": role_dir.name,
"path": str(role_dir)
})
return templates.TemplateResponse(
"pages/lint/index.html",
{
"request": request,
"roles": sorted(roles, key=lambda x: x["name"])
}
)
@router.get("/roles/{role_name}/lint", response_class=HTMLResponse)
async def lint_role_page(request: Request, role_name: str):
"""Страница проверки синтаксиса конкретной роли"""
# Проверка существования роли
roles_dir = settings.PROJECT_ROOT / "roles" / role_name
if not roles_dir.exists():
from fastapi import HTTPException
raise HTTPException(status_code=404, detail=f"Роль '{role_name}' не найдена")
return templates.TemplateResponse(
"pages/lint/role.html",
{
"request": request,
"role_name": role_name
}
)
@router.websocket("/ws/lint/{role_name}")
async def lint_websocket(websocket: WebSocket, role_name: str):
"""WebSocket для live логов линтинга"""
await websocket.accept()
try:
# Проверка существования роли (если указана)
if role_name != "all":
roles_dir = settings.PROJECT_ROOT / "roles" / role_name
if not roles_dir.exists():
await websocket.send_json({
"type": "error",
"data": f"Роль '{role_name}' не найдена"
})
await websocket.close()
return
# Отправка начального сообщения
if role_name == "all":
await websocket.send_json({
"type": "info",
"data": "🔍 Запуск проверки синтаксиса всех ролей..."
})
else:
await websocket.send_json({
"type": "info",
"data": f"🔍 Запуск проверки синтаксиса роли '{role_name}'..."
})
# Запуск линтинга
role_name_param = None if role_name == "all" else role_name
async for line in lint_service.lint_role(
role_name=role_name_param,
stream=True
):
# Очистка строки от лишних символов
line = line.rstrip()
if not line:
continue
# Определение типа лога
log_type = lint_service.detect_log_level(line)
await websocket.send_json({
"type": "log",
"level": log_type,
"data": line
})
# Завершение
await websocket.send_json({
"type": "complete",
"status": "success",
"data": "✅ Линтинг завершен"
})
except WebSocketDisconnect:
pass
except Exception as e:
import traceback
error_msg = f"❌ Ошибка: {str(e)}\n{traceback.format_exc()}"
await websocket.send_json({
"type": "error",
"data": error_msg
})
finally:
try:
await websocket.close()
except:
pass

View File

@@ -0,0 +1,439 @@
"""
API endpoints для управления playbook
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from fastapi import APIRouter, Request, HTTPException, Depends, status, Form, WebSocket
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from pathlib import Path
from typing import List, Optional, Dict
from pydantic import BaseModel
from app.db.session import get_async_db
from app.services.playbook_service import PlaybookService
from app.auth.deps import get_current_user
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.models.database import PlaybookTestRun, PlaybookDeployment
router = APIRouter()
templates_path = Path(__file__).parent.parent.parent.parent / "templates"
templates = Jinja2Templates(directory=str(templates_path))
class PlaybookCreate(BaseModel):
name: str
description: Optional[str] = None
roles: List[str]
variables: Optional[Dict] = None
inventory: Optional[str] = None
class PlaybookUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
roles: Optional[List[str]] = None
variables: Optional[Dict] = None
inventory: Optional[str] = None
content: Optional[str] = None
@router.get("/playbooks", response_class=HTMLResponse)
async def playbooks_list(
request: Request,
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""Список всех playbook"""
playbooks = await PlaybookService.list_playbooks(db)
return templates.TemplateResponse(
"pages/playbooks/list.html",
{
"request": request,
"playbooks": playbooks
}
)
@router.get("/playbooks/create", response_class=HTMLResponse)
async def playbook_create_page(
request: Request,
current_user: dict = Depends(get_current_user)
):
"""Страница создания playbook"""
from app.core.config import settings
# Получаем список доступных ролей
roles_dir = settings.PROJECT_ROOT / "roles"
roles = []
if roles_dir.exists():
roles = [d.name for d in roles_dir.iterdir() if d.is_dir() and (d / "tasks").exists()]
return templates.TemplateResponse(
"pages/playbooks/create.html",
{
"request": request,
"roles": sorted(roles)
}
)
@router.get("/playbooks/{playbook_id}", response_class=HTMLResponse)
async def playbook_detail(
request: Request,
playbook_id: int,
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""Детали playbook"""
playbook = await PlaybookService.get_playbook(db, playbook_id)
if not playbook:
raise HTTPException(status_code=404, detail="Playbook не найден")
# Получаем историю тестов и деплоев
test_runs = await db.execute(
select(PlaybookTestRun)
.where(PlaybookTestRun.playbook_id == playbook_id)
.order_by(PlaybookTestRun.started_at.desc())
.limit(10)
)
deployments = await db.execute(
select(PlaybookDeployment)
.where(PlaybookDeployment.playbook_id == playbook_id)
.order_by(PlaybookDeployment.started_at.desc())
.limit(10)
)
return templates.TemplateResponse(
"pages/playbooks/detail.html",
{
"request": request,
"playbook": playbook,
"test_runs": test_runs.scalars().all(),
"deployments": deployments.scalars().all()
}
)
@router.get("/playbooks/{playbook_id}/edit", response_class=HTMLResponse)
async def playbook_edit_page(
request: Request,
playbook_id: int,
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""Страница редактирования playbook"""
playbook = await PlaybookService.get_playbook(db, playbook_id)
if not playbook:
raise HTTPException(status_code=404, detail="Playbook не найден")
from app.core.config import settings
# Получаем список доступных ролей
roles_dir = settings.PROJECT_ROOT / "roles"
all_roles = []
if roles_dir.exists():
all_roles = [d.name for d in roles_dir.iterdir() if d.is_dir() and (d / "tasks").exists()]
return templates.TemplateResponse(
"pages/playbooks/edit.html",
{
"request": request,
"playbook": playbook,
"all_roles": sorted(all_roles)
}
)
@router.post("/api/v1/playbooks")
async def create_playbook(
playbook: PlaybookCreate,
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""Создание нового playbook"""
# Проверяем, что playbook с таким именем не существует
existing = await PlaybookService.get_playbook_by_name(db, playbook.name)
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Playbook с именем '{playbook.name}' уже существует"
)
new_playbook = await PlaybookService.create_playbook(
db=db,
name=playbook.name,
roles=playbook.roles,
description=playbook.description,
variables=playbook.variables,
inventory=playbook.inventory,
created_by=current_user.get("username")
)
return {"id": new_playbook.id, "name": new_playbook.name, "message": "Playbook создан успешно"}
@router.put("/api/v1/playbooks/{playbook_id}")
async def update_playbook(
playbook_id: int,
playbook: PlaybookUpdate,
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""Обновление playbook"""
updated = await PlaybookService.update_playbook(
db=db,
playbook_id=playbook_id,
name=playbook.name,
description=playbook.description,
roles=playbook.roles,
variables=playbook.variables,
inventory=playbook.inventory,
content=playbook.content,
updated_by=current_user.get("username")
)
if not updated:
raise HTTPException(status_code=404, detail="Playbook не найден")
return {"message": "Playbook обновлен успешно"}
@router.delete("/api/v1/playbooks/{playbook_id}")
async def delete_playbook(
playbook_id: int,
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""Удаление playbook"""
deleted = await PlaybookService.delete_playbook(db, playbook_id)
if not deleted:
raise HTTPException(status_code=404, detail="Playbook не найден")
return {"message": "Playbook удален успешно"}
@router.get("/api/v1/playbooks")
async def list_playbooks_api(
status_filter: Optional[str] = None,
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""API: Список всех playbook"""
playbooks = await PlaybookService.list_playbooks(db, status=status_filter)
return [
{
"id": p.id,
"name": p.name,
"description": p.description,
"roles": p.roles,
"status": p.status,
"created_at": p.created_at.isoformat() if p.created_at else None,
"updated_at": p.updated_at.isoformat() if p.updated_at else None
}
for p in playbooks
]
@router.post("/api/v1/playbooks/{playbook_id}/test")
async def test_playbook(
playbook_id: int,
preset: Optional[str] = Form("default"),
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""Запуск тестирования playbook"""
playbook = await PlaybookService.get_playbook(db, playbook_id)
if not playbook:
raise HTTPException(status_code=404, detail="Playbook не найден")
# Сохраняем запись о тесте
test_run = await PlaybookService.save_test_run(
db=db,
playbook_id=playbook_id,
preset_name=preset,
status="running",
user=current_user.get("username")
)
# Запускаем тест в фоне через Celery
from app.tasks.celery_tasks import run_playbook_test
task = run_playbook_test.delay(playbook_id, preset, test_run.id)
return {
"message": "Тест запущен",
"test_run_id": test_run.id,
"task_id": task.id,
"websocket_url": f"/ws/playbook-test/{test_run.id}"
}
@router.post("/api/v1/playbooks/{playbook_id}/deploy")
async def deploy_playbook(
playbook_id: int,
inventory: Optional[str] = Form(None),
limit: Optional[str] = Form(None),
tags: Optional[str] = Form(None),
check: bool = Form(False),
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""Запуск деплоя playbook"""
playbook = await PlaybookService.get_playbook(db, playbook_id)
if not playbook:
raise HTTPException(status_code=404, detail="Playbook не найден")
# Используем inventory из playbook или переданный
deploy_inventory = inventory or playbook.inventory
if not deploy_inventory:
raise HTTPException(
status_code=400,
detail="Inventory не указан. Укажите inventory в playbook или передайте его в запросе."
)
# Сохраняем запись о деплое
deployment = await PlaybookService.save_deployment(
db=db,
playbook_id=playbook_id,
inventory=deploy_inventory,
hosts=None, # Будет заполнено после парсинга inventory
status="running",
user=current_user.get("username")
)
# Запускаем деплой в фоне через Celery
from app.tasks.celery_tasks import run_playbook_deploy
task = run_playbook_deploy.delay(
playbook_id,
deploy_inventory,
limit,
tags,
check,
deployment.id
)
return {
"message": "Деплой запущен",
"deployment_id": deployment.id,
"task_id": task.id,
"websocket_url": f"/ws/playbook-deploy/{deployment.id}"
}
@router.websocket("/ws/playbook-test/{test_run_id}")
async def playbook_test_websocket(websocket: WebSocket, test_run_id: int):
"""WebSocket для live логов тестирования playbook"""
await websocket.accept()
try:
from app.db.session import get_async_db
from app.services.playbook_service import PlaybookService
from sqlalchemy.ext.asyncio import AsyncSession
# Получаем информацию о тесте
async for db in get_async_db():
test_run = await db.execute(
select(PlaybookTestRun).where(PlaybookTestRun.id == test_run_id)
)
test_run = test_run.scalar_one_or_none()
if not test_run:
await websocket.send_json({
"type": "error",
"data": f"Тест #{test_run_id} не найден"
})
await websocket.close()
return
playbook = await PlaybookService.get_playbook(db, test_run.playbook_id)
if not playbook:
await websocket.send_json({
"type": "error",
"data": "Playbook не найден"
})
await websocket.close()
return
# Подключаемся к Redis для получения логов из Celery
# Пока отправляем заглушку
await websocket.send_json({
"type": "info",
"data": f"Тестирование playbook '{playbook.name}' запущено..."
})
# TODO: Реализовать получение логов из Celery task
# Пока просто ждем и отправляем статус
import asyncio
await asyncio.sleep(1)
await websocket.send_json({
"type": "complete",
"status": "running",
"data": "Тест выполняется..."
})
except Exception as e:
await websocket.send_json({
"type": "error",
"data": f"Ошибка: {str(e)}"
})
await websocket.close()
@router.websocket("/ws/playbook-deploy/{deployment_id}")
async def playbook_deploy_websocket(websocket: WebSocket, deployment_id: int):
"""WebSocket для live логов деплоя playbook"""
await websocket.accept()
try:
from app.db.session import get_async_db
from app.services.playbook_service import PlaybookService
from sqlalchemy import select
from app.models.database import PlaybookDeployment
# Получаем информацию о деплое
async for db in get_async_db():
deployment = await db.execute(
select(PlaybookDeployment).where(PlaybookDeployment.id == deployment_id)
)
deployment = deployment.scalar_one_or_none()
if not deployment:
await websocket.send_json({
"type": "error",
"data": f"Деплой #{deployment_id} не найден"
})
await websocket.close()
return
playbook = await PlaybookService.get_playbook(db, deployment.playbook_id)
if not playbook:
await websocket.send_json({
"type": "error",
"data": "Playbook не найден"
})
await websocket.close()
return
# Подключаемся к Redis для получения логов из Celery
# Пока отправляем заглушку
await websocket.send_json({
"type": "info",
"data": f"Деплой playbook '{playbook.name}' запущен..."
})
# TODO: Реализовать получение логов из Celery task
# Пока просто ждем и отправляем статус
import asyncio
await asyncio.sleep(1)
await websocket.send_json({
"type": "complete",
"status": "running",
"data": "Деплой выполняется..."
})
except Exception as e:
await websocket.send_json({
"type": "error",
"data": f"Ошибка: {str(e)}"
})
await websocket.close()

View File

@@ -0,0 +1,359 @@
"""
API endpoints для управления preset'ами
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from fastapi import APIRouter, Request, HTTPException, Form, Depends, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from pathlib import Path
from typing import List, Dict, Optional
import yaml
import json
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import settings
from app.services.preset_service import PresetService
from app.db.session import get_async_db
router = APIRouter()
templates_path = Path(__file__).parent.parent.parent.parent / "templates"
templates = Jinja2Templates(directory=str(templates_path))
@router.get("/presets", response_class=HTMLResponse)
async def list_presets(
request: Request,
page: int = 1,
per_page: int = 10,
search: Optional[str] = None,
category: Optional[str] = None,
db: AsyncSession = Depends(get_async_db)
):
"""Страница списка preset'ов с пагинацией"""
presets = await PresetService.get_all_presets(db, category=category)
# Фильтрация по поиску
if search:
search_lower = search.lower()
presets = [
p for p in presets
if search_lower in p.get("name", "").lower() or
search_lower in (p.get("description", "") or "").lower()
]
# Пагинация
total = len(presets)
total_pages = (total + per_page - 1) // per_page if total > 0 else 1
page = max(1, min(page, total_pages))
start = (page - 1) * per_page
end = start + per_page
paginated_presets = presets[start:end]
return templates.TemplateResponse(
"pages/presets/list.html",
{
"request": request,
"presets": paginated_presets,
"total": total,
"page": page,
"per_page": per_page,
"total_pages": total_pages,
"search": search or "",
"category": category or ""
}
)
@router.get("/api/v1/presets", response_model=List[Dict])
async def get_presets_api(
category: Optional[str] = None,
db: AsyncSession = Depends(get_async_db)
):
"""API endpoint для получения списка preset'ов"""
return await PresetService.get_all_presets(db, category=category)
@router.get("/presets/create", response_class=HTMLResponse)
async def create_preset_page(request: Request):
"""Страница создания preset'а"""
return templates.TemplateResponse(
"pages/presets/create.html",
{"request": request}
)
@router.get("/api/v1/presets/{preset_name}")
async def get_preset_api(
preset_name: str,
category: str = "main",
db: AsyncSession = Depends(get_async_db)
):
"""Получение preset'а по имени"""
preset = await PresetService.get_preset(db, preset_name, category)
if not preset:
raise HTTPException(status_code=404, detail=f"Preset '{preset_name}' не найден")
return {
"name": preset.name,
"category": preset.category,
"description": preset.description,
"content": preset.content,
"data": yaml.safe_load(preset.content) if preset.content else {}
}
@router.get("/presets/{preset_name}", response_class=HTMLResponse)
async def preset_detail(
request: Request,
preset_name: str,
category: str = "main",
db: AsyncSession = Depends(get_async_db)
):
"""Страница деталей preset'а"""
try:
preset = await PresetService.get_preset_dict(db, preset_name, category)
return templates.TemplateResponse(
"pages/presets/detail.html",
{
"request": request,
"preset": preset
}
)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
@router.post("/api/v1/presets/create")
async def create_preset_api(
preset_name: str = Form(...),
description: str = Form(""),
category: str = Form("main"),
hosts: str = Form(""),
db: AsyncSession = Depends(get_async_db)
):
"""API endpoint для создания preset'а"""
try:
hosts_list = []
if hosts:
hosts_list = json.loads(hosts)
preset = await PresetService.create_preset(
db=db,
preset_name=preset_name,
description=description,
hosts=hosts_list,
category=category
)
return JSONResponse(content={
"success": True,
"preset_name": preset.name,
"message": f"Preset '{preset_name}' успешно создан"
}, status_code=201)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка при создании preset'а: {str(e)}")
@router.get("/presets/{preset_name}/edit", response_class=HTMLResponse)
async def edit_preset_page(
request: Request,
preset_name: str,
category: str = "main",
db: AsyncSession = Depends(get_async_db)
):
"""Страница редактирования preset'а"""
try:
preset = await PresetService.get_preset_dict(db, preset_name, category)
return templates.TemplateResponse(
"pages/presets/edit.html",
{
"request": request,
"preset": preset
}
)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
@router.post("/api/v1/presets/{preset_name}/update")
async def update_preset_api(
preset_name: str,
description: str = Form(""),
category: str = Form("main"),
docker_network: str = Form("labnet"),
hosts: str = Form(""),
images: str = Form(""),
systemd_defaults: str = Form(""),
kind_clusters: str = Form(""),
content: Optional[str] = Form(None),
db: AsyncSession = Depends(get_async_db)
):
"""API endpoint для обновления preset'а"""
try:
# Если передан content (YAML), используем старый метод
if content:
preset = await PresetService.update_preset(
db=db,
preset_name=preset_name,
content=content,
category=category
)
return JSONResponse(content={
"success": True,
"preset_name": preset.name,
"message": f"Preset '{preset_name}' успешно обновлен"
})
# Новый метод - из формы
hosts_list = []
if hosts:
hosts_list = json.loads(hosts)
images_dict = {}
if images:
images_dict = json.loads(images)
systemd_defaults_dict = {}
if systemd_defaults:
systemd_defaults_dict = json.loads(systemd_defaults)
kind_clusters_list = []
if kind_clusters:
kind_clusters_list = json.loads(kind_clusters)
preset = await PresetService.update_preset_from_form(
db=db,
preset_name=preset_name,
description=description,
category=category,
docker_network=docker_network,
hosts=hosts_list,
images=images_dict,
systemd_defaults=systemd_defaults_dict,
kind_clusters=kind_clusters_list
)
return JSONResponse(content={
"success": True,
"preset_name": preset.name,
"message": f"Preset '{preset_name}' успешно обновлен"
})
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка при обновлении preset'а: {str(e)}")
@router.delete("/api/v1/presets/{preset_name}")
async def delete_preset_api(
preset_name: str,
category: str = "main",
db: AsyncSession = Depends(get_async_db)
):
"""API endpoint для удаления preset'а"""
try:
await PresetService.delete_preset(db, preset_name, category)
return JSONResponse(content={
"success": True,
"preset_name": preset_name,
"message": f"Preset '{preset_name}' успешно удален"
})
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка при удалении preset'а: {str(e)}")
@router.websocket("/ws/preset/test/{preset_name}")
async def preset_test_websocket(websocket: WebSocket, preset_name: str, category: str = "main"):
"""WebSocket для live логов тестирования preset'а"""
await websocket.accept()
try:
# Получаем preset из БД
async for db in get_async_db():
preset = await PresetService.get_preset(db, preset_name, category)
if not preset:
await websocket.send_json({
"type": "error",
"data": f"Preset '{preset_name}' не найден"
})
await websocket.close()
return
preset_content = preset.content
break
# Получаем действие от клиента
data = await websocket.receive_json()
action = data.get("action", "start")
if action == "stop":
await websocket.send_json({
"type": "info",
"data": "⏹️ Остановка тестирования..."
})
await websocket.close()
return
# Запуск тестирования preset'а
from app.core.molecule_executor import MoleculeExecutor
executor = MoleculeExecutor()
# Создаем временный файл preset'а из БД
executor.create_temp_preset_file(preset_name, preset_content, category)
await websocket.send_json({
"type": "info",
"data": f"🚀 Запуск тестирования preset'а '{preset_name}'..."
})
# Запускаем тест (без указания роли - тестируем все роли)
async for line in executor.test_role(
role_name=None,
preset_name=preset_name,
preset_content=preset_content,
preset_category=category,
stream=True
):
line = line.rstrip()
if not line:
continue
log_type = executor.detect_log_level(line)
await websocket.send_json({
"type": "log",
"level": log_type,
"data": line
})
await websocket.send_json({
"type": "complete",
"status": "success",
"data": "✅ Тестирование preset'а завершено"
})
# Удаляем временный файл
if preset_name in executor._temp_preset_files:
try:
executor._temp_preset_files[preset_name].unlink()
del executor._temp_preset_files[preset_name]
except:
pass
except WebSocketDisconnect:
pass
except Exception as e:
import traceback
error_msg = f"❌ Ошибка: {str(e)}\n{traceback.format_exc()}"
await websocket.send_json({
"type": "error",
"data": error_msg
})
finally:
try:
await websocket.close()
except:
pass

View File

@@ -0,0 +1,217 @@
"""
API endpoints для управления профилем пользователя
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from fastapi import APIRouter, Request, HTTPException, Depends, Form
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from pathlib import Path
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from app.db.session import get_async_db
from app.services.user_service import UserService
from app.models.user import User, UserProfile
from app.models.database import Preset, Dockerfile, Playbook, CommandHistory
from app.auth.deps import get_current_user
router = APIRouter()
templates_path = Path(__file__).parent.parent.parent.parent / "templates"
templates = Jinja2Templates(directory=str(templates_path))
@router.get("/profile", response_class=HTMLResponse)
async def profile_page(
request: Request,
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""Страница профиля пользователя"""
user = await UserService.get_user_by_username(db, current_user.get("username"))
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
# Получаем или создаем профиль
result = await db.execute(select(UserProfile).where(UserProfile.user_id == user.id))
profile = result.scalar_one_or_none()
if not profile:
profile = UserProfile(user_id=user.id)
db.add(profile)
await db.commit()
await db.refresh(profile)
# Получаем статистику пользователя
username = user.username
# Количество созданных preset'ов
presets_count = await db.execute(
select(func.count(Preset.id)).where(Preset.created_by == username)
)
presets_count = presets_count.scalar() or 0
# Количество созданных Dockerfile'ов
dockerfiles_count = await db.execute(
select(func.count(Dockerfile.id)).where(Dockerfile.created_by == username)
)
dockerfiles_count = dockerfiles_count.scalar() or 0
# Количество созданных playbook'ов
playbooks_count = await db.execute(
select(func.count(Playbook.id)).where(Playbook.created_by == username)
)
playbooks_count = playbooks_count.scalar() or 0
# Количество выполненных команд
commands_count = await db.execute(
select(func.count(CommandHistory.id)).where(CommandHistory.user == username)
)
commands_count = commands_count.scalar() or 0
# Количество успешных тестов
successful_tests = await db.execute(
select(func.count(CommandHistory.id)).where(
CommandHistory.user == username,
CommandHistory.command_type == "test",
CommandHistory.status == "success"
)
)
successful_tests = successful_tests.scalar() or 0
return templates.TemplateResponse(
"pages/profile/index.html",
{
"request": request,
"user": user,
"profile": profile,
"stats": {
"presets": presets_count,
"dockerfiles": dockerfiles_count,
"playbooks": playbooks_count,
"commands": commands_count,
"successful_tests": successful_tests
}
}
)
@router.post("/api/v1/profile")
async def update_profile(
request: Request,
email: Optional[str] = Form(None),
full_name: Optional[str] = Form(None),
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""Обновление профиля пользователя"""
user = await UserService.get_user_by_username(db, current_user.get("username"))
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
# Получаем или создаем профиль
result = await db.execute(select(UserProfile).where(UserProfile.user_id == user.id))
profile = result.scalar_one_or_none()
if not profile:
profile = UserProfile(user_id=user.id)
db.add(profile)
if email:
profile.email = email
if full_name:
profile.full_name = full_name
await db.commit()
await db.refresh(profile)
return JSONResponse(content={
"success": True,
"message": "Профиль обновлен успешно"
})
@router.get("/profile/docker-settings", response_class=HTMLResponse)
async def docker_settings_page(
request: Request,
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""Страница настроек Docker (Harbor и Docker Hub)"""
user = await UserService.get_user_by_username(db, current_user.get("username"))
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
# Получаем или создаем профиль
result = await db.execute(select(UserProfile).where(UserProfile.user_id == user.id))
profile = result.scalar_one_or_none()
if not profile:
profile = UserProfile(user_id=user.id)
db.add(profile)
await db.commit()
await db.refresh(profile)
return templates.TemplateResponse(
"pages/profile/docker-settings.html",
{
"request": request,
"profile": profile
}
)
@router.post("/api/v1/profile/docker-settings")
async def update_docker_settings(
request: Request,
dockerhub_username: Optional[str] = Form(None),
dockerhub_password: Optional[str] = Form(None),
dockerhub_repository: Optional[str] = Form(None),
harbor_url: Optional[str] = Form(None),
harbor_username: Optional[str] = Form(None),
harbor_password: Optional[str] = Form(None),
harbor_project: Optional[str] = Form(None),
current_user: dict = Depends(get_current_user),
db: AsyncSession = Depends(get_async_db)
):
"""Обновление настроек Docker"""
user = await UserService.get_user_by_username(db, current_user.get("username"))
if not user:
raise HTTPException(status_code=404, detail="Пользователь не найден")
# Получаем или создаем профиль
result = await db.execute(select(UserProfile).where(UserProfile.user_id == user.id))
profile = result.scalar_one_or_none()
if not profile:
profile = UserProfile(user_id=user.id)
db.add(profile)
# Обновляем настройки Docker Hub
if dockerhub_username is not None:
profile.dockerhub_username = dockerhub_username
if dockerhub_password:
# TODO: Зашифровать пароль перед сохранением
profile.dockerhub_password = dockerhub_password
if dockerhub_repository is not None:
profile.dockerhub_repository = dockerhub_repository
# Обновляем настройки Harbor
if harbor_url is not None:
profile.harbor_url = harbor_url
if harbor_username is not None:
profile.harbor_username = harbor_username
if harbor_password:
# TODO: Зашифровать пароль перед сохранением
profile.harbor_password = harbor_password
if harbor_project is not None:
profile.harbor_project = harbor_project
await db.commit()
await db.refresh(profile)
return JSONResponse(content={
"success": True,
"message": "Настройки Docker обновлены успешно"
})

View File

@@ -0,0 +1,312 @@
"""
API endpoints для управления ролями
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from fastapi import APIRouter, Request, HTTPException, Form
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from pathlib import Path
from typing import Dict, List, Optional
import yaml
import json
from app.core.config import settings
from app.services.role_service import RoleService
router = APIRouter()
templates_path = Path(__file__).parent.parent.parent.parent / "templates"
templates = Jinja2Templates(directory=str(templates_path))
role_service = RoleService()
def get_roles_list() -> List[Dict]:
"""Получение списка ролей"""
roles_dir = settings.PROJECT_ROOT / "roles"
if not roles_dir.exists():
return []
roles = []
for role_dir in roles_dir.iterdir():
if role_dir.is_dir():
role_info = {
"name": role_dir.name,
"path": str(role_dir),
"has_tasks": (role_dir / "tasks" / "main.yml").exists(),
"has_defaults": (role_dir / "defaults" / "main.yml").exists(),
"has_handlers": (role_dir / "handlers" / "main.yml").exists(),
"has_meta": (role_dir / "meta" / "main.yml").exists(),
"has_readme": (role_dir / "README.md").exists(),
"description": ""
}
# Чтение описания из meta/main.yml
meta_file = role_dir / "meta" / "main.yml"
if meta_file.exists():
try:
with open(meta_file) as f:
meta_data = yaml.safe_load(f)
if meta_data and isinstance(meta_data, dict):
role_info["description"] = meta_data.get("galaxy_info", {}).get("description", "")
role_info["author"] = meta_data.get("galaxy_info", {}).get("author", "")
role_info["platforms"] = meta_data.get("galaxy_info", {}).get("platforms", [])
except:
pass
roles.append(role_info)
return sorted(roles, key=lambda x: x["name"])
@router.get("/roles/create", response_class=HTMLResponse)
async def create_role_page(request: Request):
"""Страница создания роли"""
return templates.TemplateResponse(
"pages/roles/create.html",
{"request": request}
)
@router.get("/roles/{role_name}/edit", response_class=HTMLResponse)
async def edit_role_page(request: Request, role_name: str):
"""Страница редактирования роли"""
roles = get_roles_list()
role = next((r for r in roles if r["name"] == role_name), None)
if not role:
raise HTTPException(status_code=404, detail=f"Роль '{role_name}' не найдена")
role_dir = settings.PROJECT_ROOT / "roles" / role_name
# Чтение всех файлов роли
files_content = {}
for file_type in ["tasks", "handlers", "defaults", "vars", "meta"]:
file_path = role_dir / file_type / "main.yml"
if file_path.exists():
files_content[file_type] = file_path.read_text()
else:
files_content[file_type] = ""
readme_content = ""
readme_file = role_dir / "README.md"
if readme_file.exists():
readme_content = readme_file.read_text()
else:
readme_content = ""
return templates.TemplateResponse(
"pages/roles/edit.html",
{
"request": request,
"role": role,
"files_content": files_content,
"readme_content": readme_content
}
)
@router.get("/roles", response_class=HTMLResponse)
async def list_roles(
request: Request,
page: int = 1,
per_page: int = 10,
search: Optional[str] = None
):
"""Страница списка ролей с пагинацией"""
"""Страница списка ролей с пагинацией"""
roles = get_roles_list()
# Фильтрация по поиску
if search:
search_lower = search.lower()
roles = [
r for r in roles
if search_lower in r["name"].lower() or
search_lower in (r.get("description", "") or "").lower()
]
# Пагинация
total = len(roles)
total_pages = (total + per_page - 1) // per_page if total > 0 else 1
page = max(1, min(page, total_pages))
start = (page - 1) * per_page
end = start + per_page
paginated_roles = roles[start:end]
return templates.TemplateResponse(
"pages/roles/list.html",
{
"request": request,
"roles": paginated_roles,
"total": total,
"page": page,
"per_page": per_page,
"total_pages": total_pages,
"search": search or ""
}
)
@router.get("/api/v1/roles")
async def get_roles_api():
"""API endpoint для получения списка ролей"""
return get_roles_list()
@router.get("/api/v1/roles/{role_name}")
async def get_role_info(role_name: str):
"""API endpoint для получения информации о роли"""
roles = get_roles_list()
role = next((r for r in roles if r["name"] == role_name), None)
if not role:
raise HTTPException(status_code=404, detail=f"Роль '{role_name}' не найдена")
return role
@router.get("/roles/{role_name}", response_class=HTMLResponse)
async def role_detail(request: Request, role_name: str):
"""Страница деталей роли"""
roles = get_roles_list()
role = next((r for r in roles if r["name"] == role_name), None)
if not role:
raise HTTPException(status_code=404, detail=f"Роль '{role_name}' не найдена")
# Чтение содержимого файлов
role_dir = settings.PROJECT_ROOT / "roles" / role_name
tasks_content = ""
tasks_file = role_dir / "tasks" / "main.yml"
if tasks_file.exists():
tasks_content = tasks_file.read_text()
defaults_content = ""
defaults_file = role_dir / "defaults" / "main.yml"
if defaults_file.exists():
defaults_content = defaults_file.read_text()
readme_content = ""
readme_file = role_dir / "README.md"
if readme_file.exists():
readme_content = readme_file.read_text()
return templates.TemplateResponse(
"pages/roles/detail.html",
{
"request": request,
"role": role,
"tasks_content": tasks_content,
"defaults_content": defaults_content,
"readme_content": readme_content
}
)
@router.post("/api/v1/roles/create")
async def create_role_api(
role_name: str = Form(...),
template: str = Form("default"),
description: str = Form(""),
platforms: str = Form(""), # JSON строка или через запятую
variables: str = Form("") # JSON строка
):
"""API endpoint для создания роли"""
try:
# Парсинг platforms
platforms_list = []
if platforms:
if platforms.startswith("["):
platforms_list = json.loads(platforms)
else:
platforms_list = [p.strip() for p in platforms.split(",") if p.strip()]
# Парсинг variables
variables_list = []
if variables:
variables_list = json.loads(variables)
result = role_service.create_role(
role_name=role_name,
template=template,
description=description,
platforms=platforms_list,
variables=variables_list
)
return JSONResponse(content=result, status_code=201)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка при создании роли: {str(e)}")
@router.post("/api/v1/roles/{role_name}/update")
async def update_role_api(
role_name: str,
file_type: str = Form(...),
content: str = Form(...)
):
"""API endpoint для обновления файла роли"""
try:
role_dir = settings.PROJECT_ROOT / "roles" / role_name
if not role_dir.exists():
raise HTTPException(status_code=404, detail=f"Роль '{role_name}' не найдена")
# Определение пути к файлу
if file_type == "readme":
file_path = role_dir / "README.md"
elif file_type in ["tasks", "handlers", "defaults", "vars", "meta"]:
file_path = role_dir / file_type / "main.yml"
file_path.parent.mkdir(parents=True, exist_ok=True)
else:
raise HTTPException(status_code=400, detail=f"Неверный тип файла: {file_type}")
# Сохранение файла
file_path.write_text(content)
return JSONResponse(content={
"success": True,
"message": f"Файл {file_type} успешно обновлен"
})
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка при обновлении роли: {str(e)}")
@router.get("/roles/{role_name}/deploy", response_class=HTMLResponse)
async def deploy_role_page(request: Request, role_name: str):
"""Страница деплоя роли"""
roles = get_roles_list()
role = next((r for r in roles if r["name"] == role_name), None)
if not role:
raise HTTPException(status_code=404, detail=f"Роль '{role_name}' не найдена")
# Проверка наличия inventory
inventory_file = settings.PROJECT_ROOT / "inventory" / "hosts.ini"
inventory_exists = inventory_file.exists()
# Чтение inventory если существует
inventory_content = ""
if inventory_exists:
try:
inventory_content = inventory_file.read_text()
except Exception:
pass
# Проверка наличия deploy.yml
deploy_playbook = settings.PROJECT_ROOT / "roles" / "deploy.yml"
deploy_playbook_exists = deploy_playbook.exists()
return templates.TemplateResponse(
"pages/roles/deploy.html",
{
"request": request,
"role": role,
"role_name": role_name,
"inventory_exists": inventory_exists,
"inventory_content": inventory_content,
"deploy_playbook_exists": deploy_playbook_exists
}
)

View File

@@ -0,0 +1,69 @@
"""
API endpoints для статистики
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from fastapi import APIRouter
from fastapi.responses import HTMLResponse
from pathlib import Path
from app.core.config import settings
router = APIRouter()
@router.get("/")
async def get_stats():
"""Общая статистика"""
roles_dir = settings.PROJECT_ROOT / "roles"
roles_count = 0
if roles_dir.exists():
roles = [d for d in roles_dir.iterdir()
if d.is_dir() and d.name != "deploy.yml"]
roles_count = len(roles)
return {
"roles": roles_count,
"tests": 0, # TODO: из БД
"success": 0, # TODO: из БД
"docker": 0 # TODO: из Docker API
}
@router.get("/roles", response_class=HTMLResponse)
async def get_roles_count():
"""Количество ролей"""
try:
roles_dir = settings.PROJECT_ROOT / "roles"
if not roles_dir.exists():
return "0"
# Подсчет ролей (исключая deploy.yml)
roles = [d for d in roles_dir.iterdir()
if d.is_dir() and d.name != "deploy.yml" and not d.name.startswith(".")]
return str(len(roles))
except Exception as e:
# В случае ошибки возвращаем 0
return "0"
@router.get("/tests", response_class=HTMLResponse)
async def get_tests_count():
"""Количество тестов"""
# TODO: Реализовать через БД
return "0"
@router.get("/success", response_class=HTMLResponse)
async def get_success_count():
"""Количество успешных тестов"""
# TODO: Реализовать через БД
return "0"
@router.get("/docker", response_class=HTMLResponse)
async def get_docker_count():
"""Количество Docker образов"""
# TODO: Реализовать через Docker API
return "0"

View File

@@ -0,0 +1,273 @@
"""
API endpoints для тестирования ролей
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from fastapi import APIRouter, Request, WebSocket, WebSocketDisconnect, Depends
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from pathlib import Path
from typing import Dict, Optional
import json
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import settings
from app.core.molecule_executor import MoleculeExecutor
from app.services.preset_service import PresetService
from app.db.session import get_async_db
router = APIRouter()
templates_path = Path(__file__).parent.parent.parent.parent / "templates"
templates = Jinja2Templates(directory=str(templates_path))
molecule_executor = MoleculeExecutor()
@router.get("/roles/{role_name}/test", response_class=HTMLResponse)
async def test_role_page(
request: Request,
role_name: str,
db: AsyncSession = Depends(get_async_db)
):
"""Страница тестирования роли"""
# Проверка существования роли
roles_dir = settings.PROJECT_ROOT / "roles" / role_name
if not roles_dir.exists():
from fastapi import HTTPException
raise HTTPException(status_code=404, detail=f"Роль '{role_name}' не найдена")
# Получение списка preset'ов из БД
presets = await PresetService.get_all_presets(db)
return templates.TemplateResponse(
"pages/roles/test.html",
{
"request": request,
"role_name": role_name,
"presets": presets
}
)
@router.post("/api/v1/roles/{role_name}/test")
async def start_role_test(
role_name: str,
preset: str = "default",
variables: Optional[str] = None
):
"""Запуск теста роли"""
# TODO: Запуск через Celery для фонового выполнения
# Пока просто возвращаем информацию
return {
"success": True,
"role_name": role_name,
"preset": preset,
"message": "Тест запущен",
"test_id": f"{role_name}-{preset}"
}
@router.get("/tests", response_class=HTMLResponse)
async def tests_history_page(
request: Request,
role_name: Optional[str] = None,
page: int = 1,
per_page: int = 20,
db: AsyncSession = Depends(get_async_db)
):
"""Страница истории тестов"""
try:
from app.services.history_service import HistoryService
from sqlalchemy import select, func
from app.models.database import CommandHistory, TestResult
history_service = HistoryService()
# Получение тестов из БД
query = select(CommandHistory).where(
CommandHistory.command_type == "test"
)
if role_name:
query = query.where(CommandHistory.command.like(f"%{role_name}%"))
query = query.order_by(CommandHistory.created_at.desc())
# Подсчет общего количества
count_query = select(func.count(CommandHistory.id)).where(
CommandHistory.command_type == "test"
)
if role_name:
count_query = count_query.where(CommandHistory.command.like(f"%{role_name}%"))
total = (await db.execute(count_query)).scalar() or 0
total_pages = (total + per_page - 1) // per_page if total > 0 else 1
page = max(1, min(page, total_pages))
# Пагинация
offset = (page - 1) * per_page
query = query.offset(offset).limit(per_page)
result = await db.execute(query)
tests = result.scalars().all()
# Получение списка ролей для фильтра
roles_dir = settings.PROJECT_ROOT / "roles"
roles = []
if roles_dir.exists():
for role_dir in roles_dir.iterdir():
if role_dir.is_dir() and role_dir.name != "deploy.yml":
roles.append(role_dir.name)
return templates.TemplateResponse(
"pages/tests/index.html",
{
"request": request,
"tests": tests,
"roles": sorted(roles),
"role_name": role_name or "",
"total": total,
"page": page,
"per_page": per_page,
"total_pages": total_pages
}
)
except Exception as e:
import logging
logging.error(f"Error loading tests history: {e}", exc_info=True)
# Если база данных не настроена, показываем пустую страницу
return templates.TemplateResponse(
"pages/tests/index.html",
{
"request": request,
"tests": [],
"roles": [],
"role_name": role_name or "",
"total": 0,
"page": 1,
"per_page": per_page,
"total_pages": 1,
"error": "База данных не настроена или недоступна"
}
)
@router.get("/api/v1/tests/recent")
async def get_recent_tests(limit: int = 10):
"""Получение последних тестов"""
try:
from app.services.history_service import HistoryService
history_service = HistoryService()
tests = history_service.get_command_history(
limit=limit,
command_type="test"
)
return tests
except Exception:
# Если база данных не настроена, возвращаем пустой список
return []
@router.websocket("/ws/test/{test_id}")
async def test_websocket(websocket: WebSocket, test_id: str):
"""WebSocket для live логов тестирования"""
await websocket.accept()
try:
# Парсинг test_id (формат: role_name-preset или role_name-preset-category)
parts = test_id.rsplit("-", 2)
if len(parts) < 2:
await websocket.send_json({
"type": "error",
"data": "Неверный формат test_id. Ожидается: role_name-preset или role_name-preset-category"
})
await websocket.close()
return
role_name = parts[0]
preset_name = parts[1] if len(parts) > 1 else "default"
preset_category = parts[2] if len(parts) > 2 else "main"
# Проверка существования роли
roles_dir = settings.PROJECT_ROOT / "roles" / role_name
if not roles_dir.exists():
await websocket.send_json({
"type": "error",
"data": f"Роль '{role_name}' не найдена"
})
await websocket.close()
return
# Получаем preset из БД
async for db in get_async_db():
preset = await PresetService.get_preset(db, preset_name, preset_category)
if not preset:
await websocket.send_json({
"type": "error",
"data": f"Preset '{preset_name}' не найден"
})
await websocket.close()
return
preset_content = preset.content
break
# Отправка начального сообщения
await websocket.send_json({
"type": "info",
"data": f"🚀 Запуск теста роли '{role_name}' с preset '{preset_name}'..."
})
# Создаем временный файл preset'а из БД
molecule_executor.create_temp_preset_file(preset_name, preset_content, preset_category)
# Запуск теста через MoleculeExecutor (без Makefile)
async for line in molecule_executor.test_role(
role_name=role_name,
preset_name=preset_name,
preset_content=preset_content,
preset_category=preset_category,
stream=True
):
# Очистка строки от лишних символов
line = line.rstrip()
if not line:
continue
# Определение типа лога
log_type = molecule_executor.detect_log_level(line)
await websocket.send_json({
"type": "log",
"level": log_type,
"data": line
})
# Завершение
await websocket.send_json({
"type": "complete",
"status": "success",
"data": "✅ Тест завершен успешно"
})
# Удаляем временный файл
if preset_name in molecule_executor._temp_preset_files:
try:
molecule_executor._temp_preset_files[preset_name].unlink()
del molecule_executor._temp_preset_files[preset_name]
except:
pass
except WebSocketDisconnect:
pass
except Exception as e:
import traceback
error_msg = f"❌ Ошибка: {str(e)}\n{traceback.format_exc()}"
await websocket.send_json({
"type": "error",
"data": error_msg
})
finally:
try:
await websocket.close()
except:
pass

View File

@@ -0,0 +1,113 @@
"""
API endpoints для управления Ansible Vault
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
from fastapi import APIRouter, Request, HTTPException, Form
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from pathlib import Path
from typing import List, Dict
from app.core.config import settings
router = APIRouter()
templates_path = Path(__file__).parent.parent.parent.parent / "templates"
templates = Jinja2Templates(directory=str(templates_path))
@router.get("/vault", response_class=HTMLResponse)
async def vault_page(request: Request):
"""Страница управления Ansible Vault"""
vault_dir = settings.PROJECT_ROOT / "vault"
vault_files = []
if vault_dir.exists():
# Ищем все .yml и .yaml файлы
for pattern in ["*.yml", "*.yaml"]:
for vault_file in vault_dir.glob(pattern):
if vault_file.is_file():
try:
vault_files.append({
"name": vault_file.name,
"path": str(vault_file.relative_to(settings.PROJECT_ROOT)),
"size": vault_file.stat().st_size
})
except Exception:
pass
return templates.TemplateResponse(
"pages/vault/index.html",
{
"request": request,
"vault_files": vault_files,
"vault_dir": str(vault_dir.relative_to(settings.PROJECT_ROOT)) if vault_dir.exists() else None
}
)
@router.get("/api/v1/vault/files")
async def get_vault_files():
"""API endpoint для получения списка Vault файлов"""
vault_dir = settings.PROJECT_ROOT / "vault"
vault_files = []
if vault_dir.exists():
for vault_file in vault_dir.glob("*.yml"):
vault_files.append({
"name": vault_file.name,
"path": str(vault_file),
"size": vault_file.stat().st_size
})
return {"files": vault_files}
@router.post("/api/v1/vault/encrypt")
async def encrypt_string(request: Request):
"""API endpoint для шифрования строки через Vault"""
from fastapi import Body
from app.services.vault_service import VaultService
data = await request.json()
content = data.get("content", "")
if not content:
from fastapi import HTTPException
raise HTTPException(status_code=400, detail="Содержимое не указано")
try:
vault_service = VaultService()
result = vault_service.encrypt_string(content)
return result
except ValueError as e:
from fastapi import HTTPException
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
from fastapi import HTTPException
raise HTTPException(status_code=500, detail=f"Ошибка шифрования: {str(e)}")
@router.post("/api/v1/vault/decrypt")
async def decrypt_string(request: Request):
"""API endpoint для расшифровки строки из Vault"""
from fastapi import Body
from app.services.vault_service import VaultService
data = await request.json()
content = data.get("content", "")
if not content:
from fastapi import HTTPException
raise HTTPException(status_code=400, detail="Содержимое не указано")
try:
vault_service = VaultService()
result = vault_service.decrypt_string(content)
return result
except ValueError as e:
from fastapi import HTTPException
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
from fastapi import HTTPException
raise HTTPException(status_code=500, detail=f"Ошибка расшифровки: {str(e)}")