All checks were successful
continuous-integration/drone/push Build is passing
332 lines
11 KiB
Python
332 lines
11 KiB
Python
"""
|
||
Управление демонами (удаленными серверами)
|
||
"""
|
||
|
||
from fastapi import APIRouter, HTTPException, Depends, Header
|
||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||
from pydantic import BaseModel
|
||
from typing import List, Optional
|
||
import json
|
||
import httpx
|
||
from pathlib import Path
|
||
from jose import JWTError, jwt
|
||
|
||
router = APIRouter()
|
||
security = HTTPBearer(auto_error=False)
|
||
|
||
# Файл с конфигурацией демонов
|
||
DAEMONS_FILE = Path("backend/data/daemons.json")
|
||
DAEMONS_FILE.parent.mkdir(exist_ok=True)
|
||
|
||
# Файл с пользователями - проверяем оба возможных пути
|
||
USERS_FILE = Path("backend/users.json") if Path("backend/users.json").exists() else Path("users.json")
|
||
|
||
# Настройки JWT (должны совпадать с main.py)
|
||
SECRET_KEY = "your-secret-key-change-this-in-production-12345"
|
||
ALGORITHM = "HS256"
|
||
|
||
|
||
def load_users_dict():
|
||
"""Загрузить пользователей из файла"""
|
||
if USERS_FILE.exists():
|
||
with open(USERS_FILE, 'r', encoding='utf-8') as f:
|
||
return json.load(f)
|
||
return {}
|
||
|
||
|
||
def get_current_user_from_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
|
||
"""Получить текущего пользователя из токена"""
|
||
if not credentials:
|
||
raise HTTPException(status_code=401, detail="Требуется авторизация")
|
||
|
||
token = credentials.credentials
|
||
try:
|
||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||
username: str = payload.get("sub")
|
||
|
||
if username is None:
|
||
raise HTTPException(status_code=401, detail="Неверный токен")
|
||
|
||
# Пытаемся получить роль из токена
|
||
role = payload.get("role")
|
||
|
||
print(f"[DEBUG] Username from token: {username}")
|
||
print(f"[DEBUG] Role from token: {role}")
|
||
|
||
# Если роли нет в токене, загружаем из базы
|
||
if not role:
|
||
print(f"[DEBUG] Role not in token, loading from database...")
|
||
print(f"[DEBUG] USERS_FILE path: {USERS_FILE}")
|
||
print(f"[DEBUG] USERS_FILE exists: {USERS_FILE.exists()}")
|
||
|
||
users = load_users_dict()
|
||
print(f"[DEBUG] Loaded users: {list(users.keys())}")
|
||
|
||
if username not in users:
|
||
raise HTTPException(status_code=401, detail="Пользователь не найден")
|
||
role = users[username].get("role", "user")
|
||
print(f"[DEBUG] Role from database: {role}")
|
||
|
||
print(f"[DEBUG] Final role: {role}")
|
||
return {"username": username, "role": role}
|
||
except JWTError as e:
|
||
print(f"[DEBUG] JWT Error: {e}")
|
||
raise HTTPException(status_code=401, detail="Неверный токен")
|
||
|
||
|
||
class DaemonCreate(BaseModel):
|
||
name: str
|
||
address: str
|
||
port: int
|
||
key: str
|
||
remarks: Optional[str] = ""
|
||
|
||
|
||
class DaemonUpdate(BaseModel):
|
||
name: Optional[str] = None
|
||
address: Optional[str] = None
|
||
port: Optional[int] = None
|
||
key: Optional[str] = None
|
||
remarks: Optional[str] = None
|
||
|
||
|
||
def load_daemons():
|
||
"""Загрузить список демонов"""
|
||
if not DAEMONS_FILE.exists():
|
||
return {}
|
||
|
||
with open(DAEMONS_FILE, 'r', encoding='utf-8') as f:
|
||
return json.load(f)
|
||
|
||
|
||
def save_daemons(daemons: dict):
|
||
"""Сохранить список демонов"""
|
||
with open(DAEMONS_FILE, 'w', encoding='utf-8') as f:
|
||
json.dump(daemons, f, indent=2, ensure_ascii=False)
|
||
|
||
|
||
async def check_daemon_connection(address: str, port: int, key: str) -> dict:
|
||
"""Проверить подключение к демону"""
|
||
url = f"http://{address}:{port}/api/status"
|
||
headers = {"Authorization": f"Bearer {key}"}
|
||
|
||
print(f"[DEBUG] Checking daemon connection:")
|
||
print(f"[DEBUG] URL: {url}")
|
||
print(f"[DEBUG] Key: {key[:20]}...")
|
||
|
||
try:
|
||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||
response = await client.get(url, headers=headers)
|
||
print(f"[DEBUG] Status: {response.status_code}")
|
||
|
||
if response.status_code == 200:
|
||
data = response.json()
|
||
print(f"[DEBUG] Response: {data}")
|
||
return data
|
||
else:
|
||
print(f"[DEBUG] Error response: {response.text}")
|
||
raise HTTPException(status_code=400, detail=f"Failed to connect to daemon: {response.status_code}")
|
||
except httpx.RequestError as e:
|
||
print(f"[DEBUG] Connection error: {e}")
|
||
raise HTTPException(status_code=400, detail=f"Connection error: {str(e)}")
|
||
|
||
|
||
@router.get("/api/daemons")
|
||
async def get_daemons(current_user: dict = Depends(get_current_user_from_token)):
|
||
"""Получить список всех демонов"""
|
||
# Только админы и владельцы могут видеть демоны
|
||
if current_user["role"] not in ["owner", "admin"]:
|
||
raise HTTPException(status_code=403, detail="Access denied")
|
||
|
||
daemons = load_daemons()
|
||
|
||
# Проверяем статус каждого демона
|
||
result = []
|
||
for daemon_id, daemon in daemons.items():
|
||
daemon_info = {
|
||
"id": daemon_id,
|
||
**daemon,
|
||
"status": "offline"
|
||
}
|
||
|
||
try:
|
||
# Пытаемся получить статус
|
||
status = await check_daemon_connection(
|
||
daemon["address"],
|
||
daemon["port"],
|
||
daemon["key"]
|
||
)
|
||
daemon_info["status"] = "online"
|
||
daemon_info["system"] = status.get("system", {})
|
||
daemon_info["servers"] = status.get("servers", {})
|
||
except:
|
||
pass
|
||
|
||
result.append(daemon_info)
|
||
|
||
return result
|
||
|
||
|
||
@router.post("/api/daemons")
|
||
async def create_daemon(
|
||
daemon: DaemonCreate,
|
||
current_user: dict = Depends(get_current_user_from_token)
|
||
):
|
||
"""Добавить новый демон"""
|
||
if current_user["role"] not in ["owner", "admin"]:
|
||
raise HTTPException(status_code=403, detail="Access denied")
|
||
|
||
# Проверяем подключение
|
||
await check_daemon_connection(daemon.address, daemon.port, daemon.key)
|
||
|
||
daemons = load_daemons()
|
||
|
||
# Генерируем ID
|
||
daemon_id = f"daemon-{len(daemons) + 1}"
|
||
|
||
daemons[daemon_id] = {
|
||
"name": daemon.name,
|
||
"address": daemon.address,
|
||
"port": daemon.port,
|
||
"key": daemon.key,
|
||
"remarks": daemon.remarks,
|
||
"created_at": str(Path().cwd()) # Временная метка
|
||
}
|
||
|
||
save_daemons(daemons)
|
||
|
||
return {
|
||
"success": True,
|
||
"daemon_id": daemon_id,
|
||
"message": "Daemon added successfully"
|
||
}
|
||
|
||
|
||
@router.get("/api/daemons/{daemon_id}")
|
||
async def get_daemon(
|
||
daemon_id: str,
|
||
current_user: dict = Depends(get_current_user_from_token)
|
||
):
|
||
"""Получить информацию о демоне"""
|
||
if current_user["role"] not in ["owner", "admin"]:
|
||
raise HTTPException(status_code=403, detail="Access denied")
|
||
|
||
daemons = load_daemons()
|
||
|
||
if daemon_id not in daemons:
|
||
raise HTTPException(status_code=404, detail="Daemon not found")
|
||
|
||
daemon = daemons[daemon_id]
|
||
|
||
# Получаем статус
|
||
try:
|
||
status = await check_daemon_connection(
|
||
daemon["address"],
|
||
daemon["port"],
|
||
daemon["key"]
|
||
)
|
||
daemon["status"] = "online"
|
||
daemon["system"] = status.get("system", {})
|
||
daemon["servers"] = status.get("servers", {})
|
||
except:
|
||
daemon["status"] = "offline"
|
||
|
||
return {
|
||
"id": daemon_id,
|
||
**daemon
|
||
}
|
||
|
||
|
||
@router.put("/api/daemons/{daemon_id}")
|
||
async def update_daemon(
|
||
daemon_id: str,
|
||
daemon_update: DaemonUpdate,
|
||
current_user: dict = Depends(get_current_user_from_token)
|
||
):
|
||
"""Обновить демон"""
|
||
if current_user["role"] not in ["owner", "admin"]:
|
||
raise HTTPException(status_code=403, detail="Access denied")
|
||
|
||
daemons = load_daemons()
|
||
|
||
if daemon_id not in daemons:
|
||
raise HTTPException(status_code=404, detail="Daemon not found")
|
||
|
||
# Обновляем поля
|
||
daemon = daemons[daemon_id]
|
||
|
||
if daemon_update.name:
|
||
daemon["name"] = daemon_update.name
|
||
if daemon_update.address:
|
||
daemon["address"] = daemon_update.address
|
||
if daemon_update.port:
|
||
daemon["port"] = daemon_update.port
|
||
if daemon_update.key:
|
||
daemon["key"] = daemon_update.key
|
||
if daemon_update.remarks is not None:
|
||
daemon["remarks"] = daemon_update.remarks
|
||
|
||
# Проверяем подключение с новыми данными
|
||
await check_daemon_connection(
|
||
daemon["address"],
|
||
daemon["port"],
|
||
daemon["key"]
|
||
)
|
||
|
||
save_daemons(daemons)
|
||
|
||
return {
|
||
"success": True,
|
||
"message": "Daemon updated successfully"
|
||
}
|
||
|
||
|
||
@router.delete("/api/daemons/{daemon_id}")
|
||
async def delete_daemon(
|
||
daemon_id: str,
|
||
current_user: dict = Depends(get_current_user_from_token)
|
||
):
|
||
"""Удалить демон"""
|
||
if current_user["role"] not in ["owner", "admin"]:
|
||
raise HTTPException(status_code=403, detail="Access denied")
|
||
|
||
daemons = load_daemons()
|
||
|
||
if daemon_id not in daemons:
|
||
raise HTTPException(status_code=404, detail="Daemon not found")
|
||
|
||
del daemons[daemon_id]
|
||
save_daemons(daemons)
|
||
|
||
return {
|
||
"success": True,
|
||
"message": "Daemon deleted successfully"
|
||
}
|
||
|
||
|
||
@router.get("/api/daemons/{daemon_id}/servers")
|
||
async def get_daemon_servers(
|
||
daemon_id: str,
|
||
current_user: dict = Depends(get_current_user_from_token)
|
||
):
|
||
"""Получить список серверов на демоне"""
|
||
daemons = load_daemons()
|
||
|
||
if daemon_id not in daemons:
|
||
raise HTTPException(status_code=404, detail="Daemon not found")
|
||
|
||
daemon = daemons[daemon_id]
|
||
|
||
url = f"http://{daemon['address']}:{daemon['port']}/api/servers"
|
||
headers = {"Authorization": f"Bearer {daemon['key']}"}
|
||
|
||
try:
|
||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||
response = await client.get(url, headers=headers)
|
||
if response.status_code == 200:
|
||
return response.json()
|
||
else:
|
||
raise HTTPException(status_code=400, detail="Failed to get servers from daemon")
|
||
except httpx.RequestError as e:
|
||
raise HTTPException(status_code=400, detail=f"Connection error: {str(e)}")
|