mirror of
https://github.com/andrey271192/vps_monitoring.git
synced 2026-09-20 11:55:34 +00:00
feat: keenetic integration, VPN alerts, docs
Wire keenetic monitor loop and router into main app. Add deduplicated Telegram alerts for router offline, internet, CPU/RAM, and VPN down. Update README with KeenDNS edit, import format, troubleshooting. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,16 +1,23 @@
|
||||
"""Synology NAS monitoring API endpoints."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
from fastapi import APIRouter, Request, Depends
|
||||
from fastapi.responses import PlainTextResponse
|
||||
|
||||
from server.auth import require_auth
|
||||
from server.config import DATA_DIR
|
||||
from server.config import DATA_DIR, BASE_DIR
|
||||
|
||||
router = APIRouter(prefix="/api/synology", tags=["synology"])
|
||||
|
||||
# Tunnel config
|
||||
TUNNEL_KEY_DIR = Path(BASE_DIR) / "tunnel_keys"
|
||||
TUNNEL_VPS_PORT = 15000 # VPS listens on this port, tunneled to Synology
|
||||
|
||||
# In-memory cache
|
||||
synology_metrics: Dict[str, dict] = {}
|
||||
|
||||
@@ -99,6 +106,8 @@ async def synology_refresh(name: str, request: Request, user: str = Depends(requ
|
||||
metrics["last_updated"] = datetime.now().isoformat()
|
||||
synology_metrics[name] = metrics
|
||||
await client.logout()
|
||||
if not metrics["online"] and metrics.get("error"):
|
||||
return {"status": "error", "detail": metrics["error"], "metrics": metrics}
|
||||
return {"status": "ok", "metrics": metrics}
|
||||
except Exception as e:
|
||||
return {"status": "error", "detail": str(e)}
|
||||
@@ -130,3 +139,88 @@ async def synology_refresh_all(request: Request, user: str = Depends(require_aut
|
||||
results.append({"name": dev["name"], "online": False, "error": str(e)})
|
||||
|
||||
return {"status": "ok", "results": results}
|
||||
|
||||
|
||||
@router.post("/tunnel/enable/{name}")
|
||||
async def synology_enable_tunnel(name: str, request: Request, user: str = Depends(require_auth)):
|
||||
"""Enable tunnel mode for a Synology device — VPS connects via localhost tunnel."""
|
||||
body = await request.json()
|
||||
local_ip = body.get("local_ip", "192.168.88.6")
|
||||
local_port = body.get("local_port", 5000)
|
||||
|
||||
devices = _load_synology()
|
||||
dev = next((d for d in devices if d["name"] == name), None)
|
||||
if not dev:
|
||||
return {"status": "error", "detail": "device not found"}
|
||||
|
||||
# Save original host and switch to tunnel
|
||||
dev["original_host"] = dev.get("original_host", dev["host"])
|
||||
dev["host"] = "127.0.0.1"
|
||||
dev["port"] = TUNNEL_VPS_PORT
|
||||
dev["https"] = False
|
||||
dev["tunnel"] = {
|
||||
"enabled": True,
|
||||
"local_ip": local_ip,
|
||||
"local_port": local_port,
|
||||
"vps_port": TUNNEL_VPS_PORT,
|
||||
}
|
||||
_save_synology(devices)
|
||||
|
||||
return {"status": "ok", "vps_port": TUNNEL_VPS_PORT}
|
||||
|
||||
|
||||
@router.post("/tunnel/disable/{name}")
|
||||
async def synology_disable_tunnel(name: str, request: Request, user: str = Depends(require_auth)):
|
||||
"""Disable tunnel mode, revert to direct host."""
|
||||
devices = _load_synology()
|
||||
dev = next((d for d in devices if d["name"] == name), None)
|
||||
if not dev:
|
||||
return {"status": "error", "detail": "device not found"}
|
||||
|
||||
if dev.get("original_host"):
|
||||
dev["host"] = dev["original_host"]
|
||||
dev["port"] = dev.get("tunnel", {}).get("local_port", 5000)
|
||||
dev.pop("tunnel", None)
|
||||
dev.pop("original_host", None)
|
||||
_save_synology(devices)
|
||||
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.get("/tunnel/key")
|
||||
async def get_tunnel_key(request: Request, user: str = Depends(require_auth)):
|
||||
"""Download SSH private key for tunnel setup."""
|
||||
key_file = TUNNEL_KEY_DIR / "synology_tunnel"
|
||||
if not key_file.exists():
|
||||
return {"status": "error", "detail": "tunnel key not generated"}
|
||||
|
||||
key_content = key_file.read_text()
|
||||
return PlainTextResponse(content=key_content, media_type="application/octet-stream",
|
||||
headers={"Content-Disposition": "attachment; filename=synology_tunnel"})
|
||||
|
||||
|
||||
@router.get("/tunnel/setup-command")
|
||||
async def get_tunnel_setup_command(name: str, request: Request, user: str = Depends(require_auth)):
|
||||
"""Generate PowerShell command to set up Synology tunnel on Windows PC."""
|
||||
devices = _load_synology()
|
||||
dev = next((d for d in devices if d["name"] == name), None)
|
||||
|
||||
tunnel = dev.get("tunnel", {}) if dev else {}
|
||||
local_ip = tunnel.get("local_ip", "192.168.88.6")
|
||||
local_port = tunnel.get("local_port", 5000)
|
||||
vps_port = tunnel.get("vps_port", TUNNEL_VPS_PORT)
|
||||
|
||||
host = request.headers.get("host", "77.239.126.123").split(":")[0]
|
||||
server_url = f"http://{host}"
|
||||
|
||||
ps_cmd = (
|
||||
f"Set-ExecutionPolicy Bypass -Scope Process -Force; "
|
||||
f"$ProgressPreference = 'SilentlyContinue'; "
|
||||
f"(New-Object Net.WebClient).DownloadFile('{server_url}/static/downloads/synology_tunnel.ps1', "
|
||||
f"\"$PWD\\synology_tunnel.ps1\"); "
|
||||
f"& \"$PWD\\synology_tunnel.ps1\" -ServerUrl '{server_url}' "
|
||||
f"-LocalTarget '{local_ip}:{local_port}' -VpsPort {vps_port}"
|
||||
)
|
||||
|
||||
return {"status": "ok", "command": ps_cmd, "local_ip": local_ip,
|
||||
"local_port": local_port, "vps_port": vps_port}
|
||||
|
||||
Reference in New Issue
Block a user