Files
ScriptVPS/vps-monitor/backend/main.py
jeanotx32 cf0b3f0acf
Some checks failed
Build and Push Docker Images / docker (push) Failing after 5s
feat: add VPS Monitor backend and frontend services
- Create systemd service for VPS Monitor agent.
- Add FastAPI backend with endpoints for managing VPS configurations and statuses.
- Implement Dockerfile for backend service with required dependencies.
- Create frontend using React with Vite and Tailwind CSS for styling.
- Add API client for communicating with the backend.
- Implement components for displaying VPS information and logs.
- Set up Docker Compose for orchestrating backend and frontend services.
- Add environment configuration files for backend and agent.
- Implement CORS support in the backend for frontend communication.
2026-05-18 22:31:36 -04:00

180 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""
VPS Monitor Backend — serveur central.
Agrège les données de tous les agents et expose une API REST pour le frontend.
"""
import asyncio
import json
import os
from pathlib import Path
import aiohttp
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
# ─── Config ───────────────────────────────────────────────────────────────────
CONFIG_FILE = Path(os.getenv("CONFIG_FILE", "data/vps.json"))
AGENT_TIMEOUT = int(os.getenv("AGENT_TIMEOUT", "5"))
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
if not CONFIG_FILE.exists():
CONFIG_FILE.write_text("[]")
# ─── Modèles ──────────────────────────────────────────────────────────────────
class VpsConfig(BaseModel):
id: str
name: str
host: str
port: int = 8001
api_key: str
description: str = ""
class ActionRequest(BaseModel):
action: str # start | stop | restart
# ─── Persistance ──────────────────────────────────────────────────────────────
def load_vps() -> list[dict]:
return json.loads(CONFIG_FILE.read_text())
def save_vps(data: list[dict]) -> None:
CONFIG_FILE.write_text(json.dumps(data, indent=2))
# ─── App ──────────────────────────────────────────────────────────────────────
app = FastAPI(title="VPS Monitor Backend", version="1.0.0")
app.add_middleware(
CORSMiddleware,
allow_origins=os.getenv("CORS_ORIGINS", "*").split(","),
allow_methods=["*"],
allow_headers=["*"],
)
# ─── Helpers HTTP ─────────────────────────────────────────────────────────────
async def agent_get(vps: dict, path: str):
url = f"http://{vps['host']}:{vps['port']}{path}"
headers = {"X-API-Key": vps["api_key"]}
timeout = aiohttp.ClientTimeout(total=AGENT_TIMEOUT)
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers, timeout=timeout) as r:
r.raise_for_status()
return await r.json()
async def agent_post(vps: dict, path: str, payload: dict | None = None):
url = f"http://{vps['host']}:{vps['port']}{path}"
headers = {"X-API-Key": vps["api_key"]}
timeout = aiohttp.ClientTimeout(total=AGENT_TIMEOUT)
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, json=payload, timeout=timeout) as r:
r.raise_for_status()
return await r.json()
async def fetch_vps_status(vps: dict) -> dict:
"""Interroge un agent et retourne son état complet."""
try:
containers = await agent_get(vps, "/containers")
return {
"id": vps["id"],
"name": vps["name"],
"host": vps["host"],
"description": vps.get("description", ""),
"online": True,
"containers": containers,
}
except Exception as e:
return {
"id": vps["id"],
"name": vps["name"],
"host": vps["host"],
"description": vps.get("description", ""),
"online": False,
"error": str(e),
"containers": [],
}
# ─── Routes VPS ───────────────────────────────────────────────────────────────
@app.get("/api/vps")
def list_vps():
"""Liste les VPS configurés (sans les clés API)."""
return [
{"id": v["id"], "name": v["name"], "host": v["host"], "description": v.get("description", "")}
for v in load_vps()
]
@app.post("/api/vps", status_code=201)
def add_vps(vps: VpsConfig):
"""Ajoute un nouveau VPS."""
data = load_vps()
if any(v["id"] == vps.id for v in data):
raise HTTPException(status_code=409, detail="Un VPS avec cet ID existe déjà")
data.append(vps.model_dump())
save_vps(data)
return {"status": "ok", "id": vps.id}
@app.delete("/api/vps/{vps_id}")
def delete_vps(vps_id: str):
"""Supprime un VPS de la configuration."""
data = load_vps()
filtered = [v for v in data if v["id"] != vps_id]
if len(filtered) == len(data):
raise HTTPException(status_code=404, detail="VPS introuvable")
save_vps(filtered)
return {"status": "ok"}
@app.get("/api/status")
async def all_status():
"""Retourne l'état de tous les VPS en parallèle."""
vps_list = load_vps()
results = await asyncio.gather(*[fetch_vps_status(v) for v in vps_list])
return list(results)
@app.get("/api/vps/{vps_id}/status")
async def vps_status(vps_id: str):
"""Retourne l'état d'un VPS spécifique."""
vps = next((v for v in load_vps() if v["id"] == vps_id), None)
if not vps:
raise HTTPException(status_code=404, detail="VPS introuvable")
return await fetch_vps_status(vps)
@app.get("/api/vps/{vps_id}/containers/{container_id}/logs")
async def container_logs(vps_id: str, container_id: str, lines: int = 100):
"""Récupère les logs d'un conteneur via l'agent."""
vps = next((v for v in load_vps() if v["id"] == vps_id), None)
if not vps:
raise HTTPException(status_code=404, detail="VPS introuvable")
try:
return await agent_get(vps, f"/containers/{container_id}/logs?lines={lines}")
except Exception as e:
raise HTTPException(status_code=502, detail=str(e))
@app.post("/api/vps/{vps_id}/containers/{container_id}/action")
async def container_action(vps_id: str, container_id: str, body: ActionRequest):
"""Effectue une action sur un conteneur via l'agent."""
vps = next((v for v in load_vps() if v["id"] == vps_id), None)
if not vps:
raise HTTPException(status_code=404, detail="VPS introuvable")
try:
return await agent_post(vps, f"/containers/{container_id}/action?action={body.action}")
except Exception as e:
raise HTTPException(status_code=502, detail=str(e))