refactor: extract all routes to app/api/v1/endpoints/ with proper structure
This commit is contained in:
82
app/core/auth.py
Normal file
82
app/core/auth.py
Normal file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
LogBoard+ - Аутентификация
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from passlib.context import CryptContext
|
||||
import jwt
|
||||
|
||||
from app.core.config import (
|
||||
SECRET_KEY,
|
||||
ALGORITHM,
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES,
|
||||
ADMIN_USERNAME,
|
||||
ADMIN_PASSWORD
|
||||
)
|
||||
|
||||
# Инициализация безопасности
|
||||
security = HTTPBearer()
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
# Функции для работы с паролями
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""Проверяет пароль"""
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
"""Хеширует пароль"""
|
||||
return pwd_context.hash(password)
|
||||
|
||||
# Функции для работы с JWT токенами
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
||||
"""Создает JWT токен"""
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
def verify_token(token: str) -> Optional[str]:
|
||||
"""Проверяет JWT токен и возвращает имя пользователя"""
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
username: str = payload.get("sub")
|
||||
if username is None:
|
||||
return None
|
||||
return username
|
||||
except jwt.PyJWTError:
|
||||
return None
|
||||
|
||||
# Функция для проверки аутентификации
|
||||
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> str:
|
||||
"""Получает текущего пользователя из токена"""
|
||||
token = credentials.credentials
|
||||
username = verify_token(token)
|
||||
if username is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Недействительный токен. Требуется авторизация.",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
return username
|
||||
|
||||
# Функция для проверки пользователя
|
||||
def authenticate_user(username: str, password: str) -> bool:
|
||||
"""Аутентифицирует пользователя"""
|
||||
if username == ADMIN_USERNAME:
|
||||
# Для простоты используем прямое сравнение паролей
|
||||
# В продакшене рекомендуется использовать хешированные пароли
|
||||
return password == ADMIN_PASSWORD
|
||||
return False
|
||||
42
app/core/config.py
Normal file
42
app/core/config.py
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
LogBoard+ - Конфигурация приложения
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
"""
|
||||
|
||||
import os
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
# Настройки приложения
|
||||
APP_PORT = int(os.getenv("LOGBOARD_PORT", "9001"))
|
||||
DEFAULT_TAIL = int(os.getenv("LOGBOARD_TAIL", "500"))
|
||||
DEFAULT_PROJECT = os.getenv("COMPOSE_PROJECT_NAME")
|
||||
DEFAULT_PROJECTS = os.getenv("LOGBOARD_PROJECTS")
|
||||
SKIP_UNHEALTHY = os.getenv("LOGBOARD_SKIP_UNHEALTHY", "true").lower() == "true"
|
||||
CONTAINER_LIST_TIMEOUT = int(os.getenv("LOGBOARD_CONTAINER_LIST_TIMEOUT", "10"))
|
||||
CONTAINER_INFO_TIMEOUT = int(os.getenv("LOGBOARD_CONTAINER_INFO_TIMEOUT", "3"))
|
||||
HEALTH_CHECK_TIMEOUT = int(os.getenv("LOGBOARD_HEALTH_CHECK_TIMEOUT", "2"))
|
||||
|
||||
# Настройки безопасности
|
||||
SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-here-change-in-production")
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("SESSION_TIMEOUT", "3600")) // 60 # 1 час по умолчанию
|
||||
|
||||
# Настройки пользователей
|
||||
ADMIN_USERNAME = os.getenv("LOGBOARD_USER", "admin")
|
||||
ADMIN_PASSWORD = os.getenv("LOGBOARD_PASS", "admin")
|
||||
|
||||
# Настройки AJAX обновления
|
||||
AJAX_UPDATE_INTERVAL = int(os.getenv("LOGBOARD_AJAX_UPDATE_INTERVAL", "2000"))
|
||||
|
||||
# Настройки режима отладки
|
||||
DEBUG_MODE = os.getenv("DEBUG_MODE", "false").lower() == "true"
|
||||
|
||||
# Инициализация шаблонов
|
||||
templates = Jinja2Templates(directory="app/templates")
|
||||
|
||||
# Директории
|
||||
SNAP_DIR = os.getenv("LOGBOARD_SNAPSHOT_DIR", "./snapshots")
|
||||
STATIC_DIR = os.getenv("LOGBOARD_STATIC_DIR", "./app/static")
|
||||
243
app/core/docker.py
Normal file
243
app/core/docker.py
Normal file
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
LogBoard+ - Docker функции
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
import docker
|
||||
|
||||
from app.core.config import (
|
||||
DEFAULT_TAIL,
|
||||
DEFAULT_PROJECT,
|
||||
DEFAULT_PROJECTS,
|
||||
SKIP_UNHEALTHY
|
||||
)
|
||||
|
||||
# Инициализация Docker клиента
|
||||
docker_client = docker.from_env()
|
||||
|
||||
def load_excluded_containers() -> List[str]:
|
||||
"""
|
||||
Загружает список исключенных контейнеров из JSON файла
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
"""
|
||||
try:
|
||||
with open("app/excluded_containers.json", "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return data.get("excluded_containers", [])
|
||||
except FileNotFoundError:
|
||||
print("⚠️ Файл app/excluded_containers.json не найден, используем пустой список")
|
||||
return []
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"❌ Ошибка парсинга app/excluded_containers.json: {e}")
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка загрузки app/excluded_containers.json: {e}")
|
||||
return []
|
||||
|
||||
def save_excluded_containers(containers: List[str]) -> bool:
|
||||
"""
|
||||
Сохраняет список исключенных контейнеров в JSON файл
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
"""
|
||||
try:
|
||||
data = {
|
||||
"excluded_containers": containers,
|
||||
"description": "Список контейнеров, которые генерируют слишком много логов и исключаются из отображения"
|
||||
}
|
||||
with open("app/excluded_containers.json", "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка сохранения app/excluded_containers.json: {e}")
|
||||
return False
|
||||
|
||||
def get_all_projects() -> List[str]:
|
||||
"""
|
||||
Получает список всех проектов Docker Compose с учетом исключенных контейнеров
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
"""
|
||||
projects = set()
|
||||
excluded_containers = load_excluded_containers()
|
||||
|
||||
try:
|
||||
containers = docker_client.containers.list(all=True)
|
||||
|
||||
# Словарь для подсчета контейнеров по проектам
|
||||
project_containers = {}
|
||||
standalone_containers = []
|
||||
|
||||
for c in containers:
|
||||
try:
|
||||
# Пропускаем исключенные контейнеры
|
||||
if c.name in excluded_containers:
|
||||
continue
|
||||
|
||||
labels = c.labels or {}
|
||||
project = labels.get("com.docker.compose.project")
|
||||
|
||||
if project:
|
||||
if project not in project_containers:
|
||||
project_containers[project] = 0
|
||||
project_containers[project] += 1
|
||||
else:
|
||||
standalone_containers.append(c.name)
|
||||
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# Добавляем проекты, у которых есть хотя бы один неисключенный контейнер
|
||||
for project, count in project_containers.items():
|
||||
if count > 0:
|
||||
projects.add(project)
|
||||
|
||||
# Добавляем standalone, если есть неисключенные контейнеры без проекта
|
||||
if standalone_containers:
|
||||
projects.add("standalone")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка получения списка проектов: {e}")
|
||||
return []
|
||||
|
||||
result = sorted(list(projects))
|
||||
print(f"📋 Доступные проекты (с учетом исключенных контейнеров): {result}")
|
||||
return result
|
||||
|
||||
def list_containers(projects: Optional[List[str]] = None, include_stopped: bool = False) -> List[Dict]:
|
||||
"""
|
||||
Получает список контейнеров с поддержкой множественных проектов
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
"""
|
||||
# Загружаем список исключенных контейнеров из JSON файла
|
||||
excluded_containers = load_excluded_containers()
|
||||
|
||||
print(f"🚫 Список исключенных контейнеров: {excluded_containers}")
|
||||
|
||||
items = []
|
||||
excluded_count = 0
|
||||
|
||||
try:
|
||||
# Получаем список контейнеров с базовой обработкой ошибок
|
||||
containers = docker_client.containers.list(all=include_stopped)
|
||||
|
||||
for c in containers:
|
||||
try:
|
||||
# Базовая информация о контейнере (без health check)
|
||||
basic_info = {
|
||||
"id": c.id[:12],
|
||||
"name": c.name,
|
||||
"status": c.status,
|
||||
"image": "unknown",
|
||||
"service": c.name,
|
||||
"project": None,
|
||||
"health": None,
|
||||
"ports": [],
|
||||
"url": None,
|
||||
}
|
||||
|
||||
# Безопасно получаем метки
|
||||
try:
|
||||
labels = c.labels or {}
|
||||
basic_info["project"] = labels.get("com.docker.compose.project")
|
||||
basic_info["service"] = labels.get("com.docker.compose.service") or c.name
|
||||
except Exception:
|
||||
pass # Используем значения по умолчанию
|
||||
|
||||
# Безопасно получаем информацию об образе
|
||||
try:
|
||||
if c.image and c.image.tags:
|
||||
basic_info["image"] = c.image.tags[0]
|
||||
elif c.image:
|
||||
basic_info["image"] = c.image.short_id
|
||||
except Exception:
|
||||
pass # Оставляем "unknown"
|
||||
|
||||
# Безопасно получаем информацию о портах
|
||||
try:
|
||||
ports = c.ports or {}
|
||||
if ports:
|
||||
basic_info["ports"] = list(ports.keys())
|
||||
|
||||
# Пытаемся найти HTTP/HTTPS порт для создания URL
|
||||
for port_mapping in ports.values():
|
||||
if port_mapping:
|
||||
for mapping in port_mapping:
|
||||
if isinstance(mapping, dict) and mapping.get("HostPort"):
|
||||
host_port = mapping["HostPort"]
|
||||
# Проверяем, что это HTTP порт (80, 443, 8080, 3000, etc.)
|
||||
host_port_int = int(host_port)
|
||||
if (host_port_int in [80, 443] or
|
||||
(1 <= host_port_int <= 7999) or
|
||||
(8000 <= host_port_int <= 65535)):
|
||||
protocol = "https" if host_port == "443" else "http"
|
||||
basic_info["url"] = f"{protocol}://localhost:{host_port}"
|
||||
basic_info["host_port"] = host_port
|
||||
break
|
||||
if basic_info["url"]:
|
||||
break
|
||||
except Exception:
|
||||
pass # Оставляем пустые значения
|
||||
|
||||
# Фильтрация по проектам
|
||||
if projects:
|
||||
# Если проект не указан, считаем его standalone
|
||||
container_project = basic_info["project"] or "standalone"
|
||||
if container_project not in projects:
|
||||
continue
|
||||
|
||||
# Фильтрация исключенных контейнеров
|
||||
if basic_info["name"] in excluded_containers:
|
||||
excluded_count += 1
|
||||
print(f"⚠️ Пропускаем исключенный контейнер: {basic_info['name']}")
|
||||
continue
|
||||
|
||||
# Добавляем контейнер в список
|
||||
items.append(basic_info)
|
||||
|
||||
except Exception as e:
|
||||
# Пропускаем контейнеры с критическими ошибками
|
||||
print(f"⚠️ Пропускаем проблемный контейнер {c.name if hasattr(c, 'name') else 'unknown'} (ID: {c.id[:12]}): {e}")
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка получения списка контейнеров: {e}")
|
||||
return []
|
||||
|
||||
# Сортируем по проекту, сервису и имени
|
||||
items.sort(key=lambda x: (x.get("project") or "", x.get("service") or "", x.get("name") or ""))
|
||||
|
||||
# Подсчитываем статистику по проектам
|
||||
project_stats = {}
|
||||
for item in items:
|
||||
project = item.get("project") or "standalone"
|
||||
if project not in project_stats:
|
||||
project_stats[project] = {"visible": 0, "excluded": 0}
|
||||
project_stats[project]["visible"] += 1
|
||||
|
||||
# Подсчитываем исключенные контейнеры по проектам
|
||||
for c in containers:
|
||||
try:
|
||||
if c.name in excluded_containers:
|
||||
labels = c.labels or {}
|
||||
project = labels.get("com.docker.compose.project") or "standalone"
|
||||
if project not in project_stats:
|
||||
project_stats[project] = {"visible": 0, "excluded": 0}
|
||||
project_stats[project]["excluded"] += 1
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
print(f"📊 Статистика: найдено {len(items)} контейнеров, исключено {excluded_count} контейнеров")
|
||||
for project, stats in project_stats.items():
|
||||
print(f" 📦 {project}: {stats['visible']} видимых, {stats['excluded']} исключенных")
|
||||
|
||||
return items
|
||||
Reference in New Issue
Block a user