/
HookDev-Arch
/
ServerMonitor
Обзор
Документация
Войти
/
HookDev-Arch
/
ServerMonitor
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
2
CI/CD
Аналитика
Безопасность
master
monitor_v3_base.py
399 строк
12 KB
HookDev-Arch
upload files
09 окт 2025, 02:25
09 окт 2025, 02:25
4f23f3e
Код
Авторство
О чём код?
"""FastAPI application entry point for the HookDev-Arch Dashboard.""" from __future__ import annotations import socket import time from datetime import datetime from pathlib import Path from typing import Optional from fastapi import Depends, FastAPI, HTTPException, Request, status from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from pydantic import BaseModel from api.routes_servers import router as servers_router from api.routes_stats import router as stats_router from core.auth_manager import auth_mgr from core.config import settings from core.logger import get_logger from core.metrics import alert_manager, health_checker, performance_monitor from core.security import auth_manager from core.utils import GPU_BACKEND, force_refresh_public_ip, load_servers, save_servers from core.version_check import check_version # === Application metadata === logger = get_logger("main") APP_NAME = settings.app_name __version__ = settings.version __build__ = "3.5.5" # === Paths & persisted data === BASE_DIR = Path(__file__).resolve().parent SERVERS_FILE = BASE_DIR / "data" / "servers.json" SERVERS_FILE.parent.mkdir(parents=True, exist_ok=True) (SERVERS_FILE.parent / "logs").mkdir(exist_ok=True) # === FastAPI application setup === app = FastAPI( title=APP_NAME, version=__version__, description="Modern server monitoring dashboard with SSH integration", docs_url="/docs" if settings.debug else None, redoc_url="/redoc" if settings.debug else None, ) # Cross-origin resource sharing app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # === Authentication helpers === security = HTTPBearer(auto_error=False) class LoginPayload(BaseModel): """Request body for the login endpoint.""" username: str password: str async def get_current_user( credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), ): """Return decoded JWT payload if a valid bearer token is supplied.""" if not credentials: return None try: return auth_manager.verify_token(credentials.credentials) except ValueError: return None def require_auth(user=Depends(get_current_user)): """Dependency that enforces authentication on protected endpoints.""" if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required", ) return user # === HTTP middleware === @app.middleware("http") async def auth_middleware(request: Request, call_next): """Protect application routes while keeping public assets accessible.""" # Static assets and auth routes stay public if request.url.path.startswith("/assets/") or request.url.path.startswith("/api/auth/"): return await call_next(request) if request.url.path in {"/login", "/favicon.ico"}: return await call_next(request) # Skip guard entirely if auth is disabled if not settings.require_auth: return await call_next(request) # Accept the request when a valid in-memory session exists session_id = request.cookies.get("session_id") if session_id and auth_mgr.get_session(session_id): return await call_next(request) # API endpoints should inform the caller when auth is missing if request.url.path.startswith("/api/"): return JSONResponse({"detail": "Authentication required"}, status_code=401) # Redirect anonymous users from the dashboard root to the login page if request.url.path == "/": return RedirectResponse(url="/login") return await call_next(request) # === Static content === app.mount("/assets", StaticFiles(directory="assets"), name="assets") templates = Jinja2Templates(directory="templates") # === Router registration === app.include_router(stats_router) app.include_router(servers_router) # === Server-rendered views === @app.get("/", response_class=HTMLResponse) def index(request: Request): """Render the main dashboard shell.""" return templates.TemplateResponse( "index.html", { "request": request, "app": APP_NAME, "ver": __version__, "build": __build__, "year": datetime.now().year, }, ) @app.get("/login", response_class=HTMLResponse) def login_page(request: Request): """Render the dedicated login page.""" return templates.TemplateResponse( "login.html", { "request": request, "app": APP_NAME, "ver": __version__, "build": __build__, "year": datetime.now().year, }, ) # === Public API endpoints === @app.get("/api/version") def get_version_info(): """Return basic version metadata without performing remote calls.""" return { "current_version": __version__, "build": __build__, "app_name": APP_NAME, "repo_url": "https://gitverse.ru/HookDev-Arch/ServerMonitor", "version_check": { "message": "Version status not evaluated yet", "is_up_to_date": None, "update_available": False, }, } @app.get("/api/version/check") def manual_version_check(): """Trigger an on-demand remote version check.""" try: version_info = check_version(__version__) return { "current_version": __version__, "build": __build__, "app_name": APP_NAME, "repo_url": "https://gitverse.ru/HookDev-Arch/ServerMonitor", "version_check": version_info, } except Exception as exc: # pragma: no cover - defensive log path return { "current_version": __version__, "build": __build__, "app_name": APP_NAME, "repo_url": "https://gitverse.ru/HookDev-Arch/ServerMonitor", "version_check": { "message": f"Version check failed: {exc}", "is_up_to_date": None, "update_available": False, }, } @app.get("/api/refresh-ip") def refresh_public_ip(): """Recalculate and cache the public IP address for the gateway host.""" try: new_ip = force_refresh_public_ip() except Exception as exc: # pragma: no cover - defensive log path logger.error("Error refreshing IP: %s", exc) return {"success": False, "public_ip": "N/A", "message": f"IP refresh failed: {exc}"} logger.info("IP refreshed: %s", new_ip) if new_ip == "N/A": message = "Public IP unavailable" else: message = f"Public IP updated: {new_ip}" return {"success": True, "public_ip": new_ip, "message": message} @app.get("/api/health") async def health_check(): """Execute registered health checks and return their results.""" try: return await health_checker.run_health_checks() except Exception as exc: # pragma: no cover - defensive log path logger.error("Health check failed: %s", exc) return {"overall": "error", "error": str(exc), "timestamp": time.time()} @app.get("/api/metrics") def get_metrics(): """Expose aggregated metrics for the dashboard widgets.""" try: summary = performance_monitor.get_performance_summary() return { "metrics": summary, "alerts": alert_manager.get_recent_alerts(hours=1), "timestamp": time.time(), } except Exception as exc: # pragma: no cover - defensive log path logger.error("Error getting metrics: %s", exc) return {"error": str(exc)} # === Authentication API === @app.post("/api/auth/login") def login(payload: LoginPayload): """Authenticate the user, create an in-memory session, and set a cookie.""" try: user_data = auth_mgr.authenticate(payload.username, payload.password) except Exception as exc: # pragma: no cover - defensive log path logger.error("Login error: %s", exc) return JSONResponse({"success": False, "message": "Server error"}, status_code=500) if not user_data: logger.warning("Failed login attempt for user: %s", payload.username) return JSONResponse( {"success": False, "message": "Invalid username or password"}, status_code=401, ) session_id = auth_mgr.create_session(user_data) response = JSONResponse({"success": True, "message": "Login successful", "user": user_data}) response.set_cookie( key="session_id", value=session_id, httponly=True, secure=False, # consider enabling in production behind HTTPS samesite="lax", max_age=86400, ) logger.info("User %s logged in successfully", payload.username) return response @app.get("/api/auth/check") def check_auth(request: Request): """Return the active session payload when a session cookie is present.""" session_id = request.cookies.get("session_id") if session_id: session = auth_mgr.get_session(session_id) if session: return {"authenticated": True, "user": session["user"]} return {"authenticated": False} @app.post("/api/auth/logout") def logout(request: Request): """Invalidate the user's session cookie and destroy the backend state.""" session_id = request.cookies.get("session_id") if session_id: auth_mgr.destroy_session(session_id) logger.info("User logged out") response = JSONResponse({"success": True, "message": "Logged out"}) response.delete_cookie("session_id") return response # === Application bootstrap when executed directly === if __name__ == "__main__": import uvicorn logger.info("Starting %s v%s", APP_NAME, __version__) print(f"[START] Starting {APP_NAME} v{__version__}") print(f"Build tag: {__build__}") print(f"Debug mode: {settings.debug}") print(f"Log level: {settings.log_level}") if not SERVERS_FILE.exists(): save_servers({"servers": []}) logger.info("Created servers.json") print("Created servers.json") loaded_servers = load_servers() if loaded_servers["servers"]: logger.info("Found %s configured servers", len(loaded_servers["servers"])) print("Configured servers:") for server in loaded_servers["servers"]: print(f" - {server['name']} ({server['host']}:{server.get('api_port', 3333)})") else: logger.info("No remote servers configured") print("No remote servers configured yet.") def find_free_port(start_port: int = settings.port) -> int: """Select the first available TCP port in the configured range.""" for port in range(start_port, start_port + settings.max_port_attempts): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: try: sock.bind(("0.0.0.0", port)) return port except OSError: continue return start_port port = find_free_port() if port != settings.port: warning = ( f"Port {settings.port} unavailable, falling back to {port}." ) logger.warning(warning) print(warning) print(f"Dashboard: http://localhost:{port}") print(f"GPU backend: {GPU_BACKEND or 'none'}") print("GitVerse: https://gitverse.ru/HookDev-Arch/ServerMonitor") if settings.debug: print(f"API docs: http://localhost:{port}/docs") # Register lightweight built-in checks async def check_database(): """Health check placeholder for database connectivity.""" return True async def check_disk_space(): """Ensure at least 1GB of free disk space is available.""" import shutil free_space = shutil.disk_usage("/").free return free_space > 1024 * 1024 * 1024 health_checker.register_check("database", check_database, critical=True) health_checker.register_check("disk_space", check_disk_space, critical=True) logger.info("Starting server on port %s", port) uvicorn.run( "monitor_v3_base:app", host=settings.host, port=port, log_level=settings.log_level.lower(), reload=settings.debug, access_log=settings.debug, )