Added Daemon system and fixed interface
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
307
daemon/main.py
Normal file
307
daemon/main.py
Normal file
@@ -0,0 +1,307 @@
|
||||
"""
|
||||
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"
|
||||
)
|
||||
Reference in New Issue
Block a user