All checks were successful
continuous-integration/drone/push Build is passing
308 lines
9.9 KiB
Python
308 lines
9.9 KiB
Python
"""
|
||
MC Panel Daemon - Удаленный демон для управления серверами
|
||
Устанавливается на отдельные серверы и подключается к основной панели
|
||
"""
|
||
|
||
import asyncio
|
||
import json
|
||
import os
|
||
import psutil
|
||
import subprocess
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from typing import Dict, Optional
|
||
import websockets
|
||
from fastapi import FastAPI, HTTPException, Header
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
import uvicorn
|
||
from dotenv import load_dotenv
|
||
|
||
load_dotenv()
|
||
|
||
app = FastAPI(title="MC Panel Daemon")
|
||
|
||
# CORS
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=["*"],
|
||
allow_credentials=True,
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
# Конфигурация
|
||
DAEMON_ID = os.getenv("DAEMON_ID", "daemon-1")
|
||
DAEMON_NAME = os.getenv("DAEMON_NAME", "Main Server")
|
||
DAEMON_PORT = int(os.getenv("DAEMON_PORT", "24444"))
|
||
DAEMON_KEY = os.getenv("DAEMON_KEY", "") # Ключ для аутентификации
|
||
PANEL_URL = os.getenv("PANEL_URL", "") # URL основной панели для WebSocket
|
||
SERVERS_DIR = Path(os.getenv("SERVERS_DIR", "./servers"))
|
||
|
||
# Создаем директорию для серверов
|
||
SERVERS_DIR.mkdir(exist_ok=True)
|
||
|
||
# Хранилище процессов серверов
|
||
server_processes: Dict[str, subprocess.Popen] = {}
|
||
|
||
|
||
def verify_key(authorization: str = Header(None)) -> bool:
|
||
"""Проверка API ключа"""
|
||
if not DAEMON_KEY:
|
||
return True # Если ключ не установлен, разрешаем доступ
|
||
|
||
if not authorization:
|
||
raise HTTPException(status_code=401, detail="Missing authorization header")
|
||
|
||
if authorization != f"Bearer {DAEMON_KEY}":
|
||
raise HTTPException(status_code=403, detail="Invalid daemon key")
|
||
|
||
return True
|
||
|
||
|
||
@app.get("/")
|
||
async def root():
|
||
"""Информация о демоне"""
|
||
return {
|
||
"daemon_id": DAEMON_ID,
|
||
"daemon_name": DAEMON_NAME,
|
||
"status": "online",
|
||
"version": "1.0.0"
|
||
}
|
||
|
||
|
||
@app.get("/api/status")
|
||
async def get_status(authorization: str = Header(None)):
|
||
"""Получить статус демона и системы"""
|
||
# Проверка ключа опциональна для статуса
|
||
if DAEMON_KEY and authorization:
|
||
if authorization != f"Bearer {DAEMON_KEY}":
|
||
raise HTTPException(status_code=403, detail="Invalid daemon key")
|
||
|
||
cpu_percent = psutil.cpu_percent(interval=1)
|
||
memory = psutil.virtual_memory()
|
||
disk = psutil.disk_usage('/')
|
||
|
||
return {
|
||
"daemon_id": DAEMON_ID,
|
||
"daemon_name": DAEMON_NAME,
|
||
"status": "online",
|
||
"system": {
|
||
"cpu_usage": cpu_percent,
|
||
"memory_total": memory.total,
|
||
"memory_used": memory.used,
|
||
"memory_percent": memory.percent,
|
||
"disk_total": disk.total,
|
||
"disk_used": disk.used,
|
||
"disk_percent": disk.percent
|
||
},
|
||
"servers": {
|
||
"total": len(list(SERVERS_DIR.iterdir())),
|
||
"running": len(server_processes)
|
||
},
|
||
"timestamp": datetime.now().isoformat()
|
||
}
|
||
|
||
|
||
@app.get("/api/servers")
|
||
async def list_servers(authorization: str = Header(None)):
|
||
"""Список всех серверов на этом демоне"""
|
||
verify_key(authorization)
|
||
|
||
servers = []
|
||
for server_dir in SERVERS_DIR.iterdir():
|
||
if server_dir.is_dir():
|
||
config_file = server_dir / "config.json"
|
||
if config_file.exists():
|
||
with open(config_file, 'r', encoding='utf-8') as f:
|
||
config = json.load(f)
|
||
|
||
servers.append({
|
||
"name": server_dir.name,
|
||
"display_name": config.get("displayName", server_dir.name),
|
||
"status": "running" if server_dir.name in server_processes else "stopped",
|
||
"daemon_id": DAEMON_ID
|
||
})
|
||
|
||
return servers
|
||
|
||
|
||
@app.post("/api/servers/{server_name}/start")
|
||
async def start_server(server_name: str, authorization: str = Header(None)):
|
||
"""Запустить сервер"""
|
||
verify_key(authorization)
|
||
|
||
server_dir = SERVERS_DIR / server_name
|
||
if not server_dir.exists():
|
||
raise HTTPException(status_code=404, detail="Server not found")
|
||
|
||
if server_name in server_processes:
|
||
raise HTTPException(status_code=400, detail="Server already running")
|
||
|
||
config_file = server_dir / "config.json"
|
||
if not config_file.exists():
|
||
raise HTTPException(status_code=400, detail="Server config not found")
|
||
|
||
with open(config_file, 'r', encoding='utf-8') as f:
|
||
config = json.load(f)
|
||
|
||
start_command = config.get("startCommand", "")
|
||
if not start_command:
|
||
raise HTTPException(status_code=400, detail="Start command not configured")
|
||
|
||
try:
|
||
# Запускаем процесс
|
||
process = subprocess.Popen(
|
||
start_command,
|
||
shell=True,
|
||
cwd=str(server_dir),
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.PIPE,
|
||
stdin=subprocess.PIPE
|
||
)
|
||
|
||
server_processes[server_name] = process
|
||
|
||
return {
|
||
"success": True,
|
||
"message": f"Server {server_name} started",
|
||
"pid": process.pid
|
||
}
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"Failed to start server: {str(e)}")
|
||
|
||
|
||
@app.post("/api/servers/{server_name}/stop")
|
||
async def stop_server(server_name: str, authorization: str = Header(None)):
|
||
"""Остановить сервер"""
|
||
verify_key(authorization)
|
||
|
||
if server_name not in server_processes:
|
||
raise HTTPException(status_code=400, detail="Server not running")
|
||
|
||
try:
|
||
process = server_processes[server_name]
|
||
process.terminate()
|
||
|
||
# Ждем завершения процесса
|
||
try:
|
||
process.wait(timeout=10)
|
||
except subprocess.TimeoutExpired:
|
||
process.kill()
|
||
|
||
del server_processes[server_name]
|
||
|
||
return {
|
||
"success": True,
|
||
"message": f"Server {server_name} stopped"
|
||
}
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"Failed to stop server: {str(e)}")
|
||
|
||
|
||
@app.post("/api/servers/{server_name}/command")
|
||
async def send_command(server_name: str, command: dict, authorization: str = Header(None)):
|
||
"""Отправить команду в консоль сервера"""
|
||
verify_key(authorization)
|
||
|
||
if server_name not in server_processes:
|
||
raise HTTPException(status_code=400, detail="Server not running")
|
||
|
||
try:
|
||
process = server_processes[server_name]
|
||
cmd = command.get("command", "")
|
||
|
||
if process.stdin:
|
||
process.stdin.write(f"{cmd}\n".encode())
|
||
process.stdin.flush()
|
||
|
||
return {
|
||
"success": True,
|
||
"message": "Command sent"
|
||
}
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"Failed to send command: {str(e)}")
|
||
|
||
|
||
@app.get("/api/servers/{server_name}/stats")
|
||
async def get_server_stats(server_name: str, authorization: str = Header(None)):
|
||
"""Получить статистику сервера"""
|
||
verify_key(authorization)
|
||
|
||
server_dir = SERVERS_DIR / server_name
|
||
if not server_dir.exists():
|
||
raise HTTPException(status_code=404, detail="Server not found")
|
||
|
||
is_running = server_name in server_processes
|
||
cpu = 0
|
||
memory = 0
|
||
|
||
if is_running:
|
||
try:
|
||
process = server_processes[server_name]
|
||
p = psutil.Process(process.pid)
|
||
cpu = p.cpu_percent(interval=0.1)
|
||
memory = p.memory_info().rss / 1024 / 1024 # MB
|
||
except:
|
||
pass
|
||
|
||
# Размер директории
|
||
disk_usage = sum(f.stat().st_size for f in server_dir.rglob('*') if f.is_file()) / 1024 / 1024 # MB
|
||
|
||
return {
|
||
"status": "running" if is_running else "stopped",
|
||
"cpu": cpu,
|
||
"memory": memory,
|
||
"disk": disk_usage
|
||
}
|
||
|
||
|
||
@app.post("/api/servers/create")
|
||
async def create_server_on_daemon(data: dict, authorization: str = Header(None)):
|
||
"""Создать сервер на этом демоне"""
|
||
verify_key(authorization)
|
||
|
||
server_name = data.get("name", "").strip()
|
||
if not server_name:
|
||
raise HTTPException(status_code=400, detail="Server name is required")
|
||
|
||
server_path = SERVERS_DIR / server_name
|
||
|
||
if server_path.exists():
|
||
raise HTTPException(status_code=400, detail="Server already exists")
|
||
|
||
try:
|
||
server_path.mkdir(parents=True)
|
||
|
||
# Сохраняем конфигурацию
|
||
config = {
|
||
"name": server_name,
|
||
"displayName": data.get("displayName", server_name),
|
||
"startCommand": data.get("startCommand", "java -Xmx2G -jar server.jar nogui"),
|
||
"owner": data.get("owner", "unknown")
|
||
}
|
||
|
||
config_file = server_path / "config.json"
|
||
with open(config_file, 'w', encoding='utf-8') as f:
|
||
json.dump(config, f, indent=2, ensure_ascii=False)
|
||
|
||
return {"message": "Server created successfully", "name": server_name}
|
||
except Exception as e:
|
||
# Если что-то пошло не так, удаляем папку
|
||
if server_path.exists():
|
||
import shutil
|
||
shutil.rmtree(server_path)
|
||
raise HTTPException(status_code=500, detail=f"Failed to create server: {str(e)}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
print(f"Starting MC Panel Daemon: {DAEMON_NAME} ({DAEMON_ID})")
|
||
print(f"Port: {DAEMON_PORT}")
|
||
print(f"Servers directory: {SERVERS_DIR}")
|
||
|
||
uvicorn.run(
|
||
app,
|
||
host="0.0.0.0",
|
||
port=DAEMON_PORT,
|
||
log_level="info"
|
||
)
|