""" 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" )