mirror of
https://github.com/andrey271192/vps_monitoring.git
synced 2026-09-20 11:55:34 +00:00
feat: initial VPS monitoring platform
- FastAPI backend with auth, server CRUD, metrics collection - Web dashboard with dark theme, server cards, SSH terminal (xterm.js) - Telegram bot with inline keyboards, server management, reboot - Telegram Mini App for mobile monitoring - Background monitoring via asyncssh (CPU, RAM, Disk, Network, Uptime) - Alert system with configurable thresholds - WebSocket SSH terminal in browser - One-command install script - Systemd service configuration Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
0
server/api/__init__.py
Normal file
0
server/api/__init__.py
Normal file
51
server/api/auth_routes.py
Normal file
51
server/api/auth_routes.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi.responses import RedirectResponse, JSONResponse
|
||||
|
||||
from server.auth import verify_password, create_token, require_auth
|
||||
from server.config import load_settings, save_settings
|
||||
from server.models import LoginForm, SettingsUpdate
|
||||
|
||||
router = APIRouter(tags=["auth"])
|
||||
|
||||
|
||||
@router.post("/api/login")
|
||||
async def login(form: LoginForm):
|
||||
settings = load_settings()
|
||||
if (
|
||||
form.username == settings["admin_login"]
|
||||
and form.password == settings["admin_password"]
|
||||
):
|
||||
token = create_token(form.username)
|
||||
response = JSONResponse({"status": "ok"})
|
||||
response.set_cookie("access_token", token, httponly=True, max_age=86400)
|
||||
return response
|
||||
raise HTTPException(401, "Invalid credentials")
|
||||
|
||||
|
||||
@router.post("/api/logout")
|
||||
async def logout():
|
||||
response = RedirectResponse("/login", status_code=302)
|
||||
response.delete_cookie("access_token")
|
||||
return response
|
||||
|
||||
|
||||
@router.get("/api/settings")
|
||||
async def get_settings(request: Request):
|
||||
require_auth(request)
|
||||
settings = load_settings()
|
||||
# Mask sensitive data
|
||||
safe = {**settings}
|
||||
if safe.get("telegram_bot_token"):
|
||||
safe["telegram_bot_token"] = safe["telegram_bot_token"][:10] + "..."
|
||||
return safe
|
||||
|
||||
|
||||
@router.put("/api/settings")
|
||||
async def update_settings(update: SettingsUpdate, request: Request):
|
||||
require_auth(request)
|
||||
settings = load_settings()
|
||||
for key, val in update.model_dump(exclude_unset=True).items():
|
||||
if val is not None:
|
||||
settings[key] = val
|
||||
save_settings(settings)
|
||||
return {"status": "ok"}
|
||||
105
server/api/servers.py
Normal file
105
server/api/servers.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import uuid
|
||||
from fastapi import APIRouter, HTTPException, Depends, Request
|
||||
|
||||
from server.auth import require_auth
|
||||
from server.config import load_servers, save_servers
|
||||
from server.models import ServerCreate, ServerUpdate
|
||||
from server.services.monitor import get_metrics, get_all_metrics
|
||||
|
||||
router = APIRouter(prefix="/api/servers", tags=["servers"])
|
||||
|
||||
|
||||
@router.get("")
|
||||
async def list_servers(request: Request, user: str = Depends(require_auth)):
|
||||
servers = load_servers()
|
||||
metrics = get_all_metrics()
|
||||
result = []
|
||||
for srv in servers:
|
||||
srv_data = {**srv}
|
||||
srv_data["metrics"] = metrics.get(srv["host"], {})
|
||||
if "password" in srv_data:
|
||||
srv_data["password"] = "***"
|
||||
result.append(srv_data)
|
||||
return result
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def add_server(server: ServerCreate, user: str = Depends(require_auth)):
|
||||
servers = load_servers()
|
||||
|
||||
# Check duplicate
|
||||
for s in servers:
|
||||
if s["host"] == server.host:
|
||||
raise HTTPException(400, f"Server {server.host} already exists")
|
||||
|
||||
new_server = {
|
||||
"id": str(uuid.uuid4())[:8],
|
||||
"name": server.name,
|
||||
"host": server.host,
|
||||
"port": server.port,
|
||||
"username": server.username,
|
||||
"password": server.password,
|
||||
"ssh_key": server.ssh_key,
|
||||
"description": server.description,
|
||||
}
|
||||
servers.append(new_server)
|
||||
save_servers(servers)
|
||||
return {"status": "ok", "server": {**new_server, "password": "***"}}
|
||||
|
||||
|
||||
@router.put("/{server_id}")
|
||||
async def update_server(server_id: str, update: ServerUpdate, user: str = Depends(require_auth)):
|
||||
servers = load_servers()
|
||||
for i, srv in enumerate(servers):
|
||||
if srv.get("id") == server_id or srv["host"] == server_id:
|
||||
for key, val in update.model_dump(exclude_unset=True).items():
|
||||
servers[i][key] = val
|
||||
save_servers(servers)
|
||||
return {"status": "ok"}
|
||||
raise HTTPException(404, "Server not found")
|
||||
|
||||
|
||||
@router.delete("/{server_id}")
|
||||
async def delete_server(server_id: str, user: str = Depends(require_auth)):
|
||||
servers = load_servers()
|
||||
servers = [s for s in servers if s.get("id") != server_id and s["host"] != server_id]
|
||||
save_servers(servers)
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.get("/{server_id}/metrics")
|
||||
async def server_metrics(server_id: str, user: str = Depends(require_auth)):
|
||||
servers = load_servers()
|
||||
for srv in servers:
|
||||
if srv.get("id") == server_id or srv["host"] == server_id:
|
||||
return get_metrics(srv["host"])
|
||||
raise HTTPException(404, "Server not found")
|
||||
|
||||
|
||||
@router.post("/{server_id}/reboot")
|
||||
async def reboot_server(server_id: str, user: str = Depends(require_auth)):
|
||||
from server.services.ssh_manager import execute_command
|
||||
|
||||
servers = load_servers()
|
||||
for srv in servers:
|
||||
if srv.get("id") == server_id or srv["host"] == server_id:
|
||||
result = await execute_command(srv, "reboot")
|
||||
return {"status": "ok", "result": result}
|
||||
raise HTTPException(404, "Server not found")
|
||||
|
||||
|
||||
@router.post("/{server_id}/exec")
|
||||
async def exec_command(server_id: str, request: Request, user: str = Depends(require_auth)):
|
||||
from server.services.ssh_manager import execute_command
|
||||
|
||||
body = await request.json()
|
||||
command = body.get("command", "")
|
||||
if not command:
|
||||
raise HTTPException(400, "Command required")
|
||||
|
||||
servers = load_servers()
|
||||
for srv in servers:
|
||||
if srv.get("id") == server_id or srv["host"] == server_id:
|
||||
result = await execute_command(srv, command)
|
||||
return result
|
||||
raise HTTPException(404, "Server not found")
|
||||
83
server/api/ssh_ws.py
Normal file
83
server/api/ssh_ws.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||
|
||||
import asyncssh
|
||||
|
||||
from server.auth import verify_token
|
||||
from server.config import load_servers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.websocket("/ws/ssh/{server_id}")
|
||||
async def ssh_websocket(websocket: WebSocket, server_id: str):
|
||||
"""WebSocket SSH terminal."""
|
||||
await websocket.accept()
|
||||
|
||||
# Auth check
|
||||
token = websocket.cookies.get("access_token")
|
||||
if not verify_token(token):
|
||||
await websocket.send_text("Authentication required")
|
||||
await websocket.close()
|
||||
return
|
||||
|
||||
servers = load_servers()
|
||||
server = None
|
||||
for srv in servers:
|
||||
if srv.get("id") == server_id or srv["host"] == server_id:
|
||||
server = srv
|
||||
break
|
||||
|
||||
if not server:
|
||||
await websocket.send_text("Server not found")
|
||||
await websocket.close()
|
||||
return
|
||||
|
||||
try:
|
||||
conn_kwargs = {
|
||||
"host": server["host"],
|
||||
"port": server.get("port", 22),
|
||||
"username": server.get("username", "root"),
|
||||
"known_hosts": None,
|
||||
"connect_timeout": 15,
|
||||
}
|
||||
|
||||
if server.get("password"):
|
||||
conn_kwargs["password"] = server["password"]
|
||||
elif server.get("ssh_key"):
|
||||
conn_kwargs["client_keys"] = [server["ssh_key"]]
|
||||
|
||||
async with asyncssh.connect(**conn_kwargs) as conn:
|
||||
process = await conn.create_process(
|
||||
term_type="xterm-256color",
|
||||
term_size=(120, 40),
|
||||
)
|
||||
|
||||
async def read_output():
|
||||
try:
|
||||
while True:
|
||||
data = await process.stdout.read(4096)
|
||||
if not data:
|
||||
break
|
||||
await websocket.send_text(data)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
read_task = asyncio.create_task(read_output())
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
process.stdin.write(data)
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
finally:
|
||||
read_task.cancel()
|
||||
process.close()
|
||||
|
||||
except Exception as e:
|
||||
await websocket.send_text(f"\r\nConnection error: {e}\r\n")
|
||||
await websocket.close()
|
||||
Reference in New Issue
Block a user