274 lines
10 KiB
Python
274 lines
10 KiB
Python
import asyncio
|
|
import json
|
|
import os
|
|
import platform
|
|
import re
|
|
import shutil
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Dict, Any, List, Optional
|
|
import psutil
|
|
from backend.runner import runner
|
|
from backend.config import get_setting, load_settings, get_default_backup_dir
|
|
from backend.remote import run_remote_quick, build_ssh_prefix
|
|
|
|
def format_bytes(size: int | float) -> str:
|
|
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
|
|
if size < 1024.0:
|
|
return f"{size:.2f} {unit}"
|
|
size /= 1024.0
|
|
return f"{size:.2f} PB"
|
|
|
|
def format_uptime(seconds: float) -> str:
|
|
days = int(seconds // (24 * 3600))
|
|
seconds %= (24 * 3600)
|
|
hours = int(seconds // 3600)
|
|
seconds %= 3600
|
|
minutes = int(seconds // 60)
|
|
parts = []
|
|
if days > 0:
|
|
parts.append(f"{days}d")
|
|
if hours > 0 or days > 0:
|
|
parts.append(f"{hours}h")
|
|
parts.append(f"{minutes}m")
|
|
return " ".join(parts)
|
|
|
|
async def get_system_stats() -> Dict[str, Any]:
|
|
settings = load_settings()
|
|
vps_host = settings.get("vps_host", "").strip()
|
|
|
|
# Local Storage Info (Unraid Server or Docker Backup Volume)
|
|
configured_dir = settings.get("backup_dir", "").strip() or get_default_backup_dir()
|
|
backup_path = configured_dir
|
|
unraid_disk = None
|
|
try:
|
|
p = Path(backup_path)
|
|
if not p.exists():
|
|
p.mkdir(parents=True, exist_ok=True)
|
|
bd = psutil.disk_usage(str(p))
|
|
unraid_disk = {
|
|
"path": str(p),
|
|
"total": bd.total,
|
|
"used": bd.used,
|
|
"free": bd.free,
|
|
"percent": bd.percent,
|
|
"formatted_total": format_bytes(bd.total),
|
|
"formatted_used": format_bytes(bd.used),
|
|
"formatted_free": format_bytes(bd.free)
|
|
}
|
|
except Exception:
|
|
pass
|
|
|
|
# If no VPS configured yet, return local info and indicator
|
|
if not vps_host:
|
|
local_cpu = psutil.cpu_percent(interval=0.05)
|
|
local_mem = psutil.virtual_memory()
|
|
local_uptime = time.time() - psutil.boot_time()
|
|
return {
|
|
"vps_configured": False,
|
|
"hostname": "No VPS Configured (Local Host)",
|
|
"os": "Unraid / Docker Host",
|
|
"kernel": platform.release(),
|
|
"uptime_formatted": format_uptime(local_uptime),
|
|
"cpu": {"percent": local_cpu, "cores": psutil.cpu_count(logical=True), "load_avg": [0, 0, 0]},
|
|
"memory": {
|
|
"percent": local_mem.percent,
|
|
"formatted_total": format_bytes(local_mem.total),
|
|
"formatted_used": format_bytes(local_mem.used),
|
|
"formatted_available": format_bytes(local_mem.available)
|
|
},
|
|
"disk": unraid_disk or {"percent": 0, "formatted_used": "0 B", "formatted_total": "0 B", "formatted_free": "0 B"},
|
|
"unraid_disk": unraid_disk,
|
|
"reboot_required": False,
|
|
"reboot_packages": []
|
|
}
|
|
|
|
# Gather metrics from Remote VPS via SSH
|
|
remote_script = """
|
|
uname -n
|
|
cat /etc/os-release | grep PRETTY_NAME | cut -d= -f2 | tr -d '"'
|
|
uname -r
|
|
cat /proc/uptime | awk '{print $1}'
|
|
grep -c ^processor /proc/cpuinfo
|
|
cat /proc/loadavg | awk '{print $1,$2,$3}'
|
|
free -b | grep Mem: | awk '{print $2,$3,$7}'
|
|
df -B1 / | tail -n 1 | awk '{print $2,$3,$4}'
|
|
[ -f /var/run/reboot-required ] && echo "REBOOT_YES" || echo "REBOOT_NO"
|
|
[ -f /var/run/reboot-required.pkgs ] && cat /var/run/reboot-required.pkgs || true
|
|
"""
|
|
ret, stdout, stderr = await run_remote_quick(remote_script, settings)
|
|
|
|
if ret != 0:
|
|
return {
|
|
"vps_configured": True,
|
|
"vps_connected": False,
|
|
"hostname": vps_host,
|
|
"os": "Remote VPS (Unreachable)",
|
|
"kernel": "SSH Error",
|
|
"uptime_formatted": "Offline",
|
|
"error": stderr.strip() or "Failed to connect over SSH",
|
|
"cpu": {"percent": 0, "cores": 1, "load_avg": [0, 0, 0]},
|
|
"memory": {"percent": 0, "formatted_total": "0 B", "formatted_used": "0 B", "formatted_available": "0 B"},
|
|
"disk": {"percent": 0, "formatted_total": "0 B", "formatted_used": "0 B", "formatted_free": "0 B"},
|
|
"unraid_disk": unraid_disk,
|
|
"reboot_required": False,
|
|
"reboot_packages": []
|
|
}
|
|
|
|
lines = [l.strip() for l in stdout.splitlines() if l.strip()]
|
|
try:
|
|
r_host = lines[0]
|
|
r_os = lines[1]
|
|
r_kernel = lines[2]
|
|
r_uptime_sec = float(lines[3])
|
|
r_cores = int(lines[4])
|
|
r_load = [float(x) for x in lines[5].split()[:3]]
|
|
|
|
mem_parts = [int(x) for x in lines[6].split()]
|
|
mem_total, mem_used, mem_avail = mem_parts[0], mem_parts[1], mem_parts[2]
|
|
mem_pct = round((mem_used / mem_total) * 100, 1) if mem_total else 0
|
|
|
|
disk_parts = [int(x) for x in lines[7].split()]
|
|
disk_total, disk_used, disk_avail = disk_parts[0], disk_parts[1], disk_parts[2]
|
|
disk_pct = round((disk_used / disk_total) * 100, 1) if disk_total else 0
|
|
|
|
reboot_req = "REBOOT_YES" in stdout
|
|
reboot_pkgs = []
|
|
capture_pkgs = False
|
|
for l in lines:
|
|
if capture_pkgs and not l.startswith("REBOOT_"):
|
|
reboot_pkgs.append(l)
|
|
if l == "REBOOT_YES":
|
|
capture_pkgs = True
|
|
|
|
return {
|
|
"vps_configured": True,
|
|
"vps_connected": True,
|
|
"hostname": r_host,
|
|
"os": r_os,
|
|
"kernel": r_kernel,
|
|
"uptime_formatted": format_uptime(r_uptime_sec),
|
|
"cpu": {
|
|
"percent": min(100, round((r_load[0] / max(1, r_cores)) * 100, 1)),
|
|
"cores": r_cores,
|
|
"load_avg": r_load
|
|
},
|
|
"memory": {
|
|
"total": mem_total,
|
|
"used": mem_used,
|
|
"available": mem_avail,
|
|
"percent": mem_pct,
|
|
"formatted_total": format_bytes(mem_total),
|
|
"formatted_used": format_bytes(mem_used),
|
|
"formatted_available": format_bytes(mem_avail)
|
|
},
|
|
"disk": {
|
|
"total": disk_total,
|
|
"used": disk_used,
|
|
"free": disk_avail,
|
|
"percent": disk_pct,
|
|
"formatted_total": format_bytes(disk_total),
|
|
"formatted_used": format_bytes(disk_used),
|
|
"formatted_free": format_bytes(disk_avail)
|
|
},
|
|
"unraid_disk": unraid_disk,
|
|
"reboot_required": reboot_req,
|
|
"reboot_packages": reboot_pkgs
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"vps_configured": True,
|
|
"vps_connected": True,
|
|
"hostname": vps_host,
|
|
"os": "Linux VPS",
|
|
"kernel": "Connected",
|
|
"uptime_formatted": "Connected",
|
|
"cpu": {"percent": 0, "cores": 1, "load_avg": [0, 0, 0]},
|
|
"memory": {"percent": 0, "formatted_total": "0 B", "formatted_used": "0 B", "formatted_available": "0 B"},
|
|
"disk": {"percent": 0, "formatted_total": "0 B", "formatted_used": "0 B", "formatted_free": "0 B"},
|
|
"unraid_disk": unraid_disk,
|
|
"reboot_required": False,
|
|
"reboot_packages": []
|
|
}
|
|
|
|
async def check_upgradable_packages() -> Dict[str, Any]:
|
|
settings = load_settings()
|
|
vps_host = settings.get("vps_host", "").strip()
|
|
if not vps_host:
|
|
return {"total": 0, "security_count": 0, "packages": [], "message": "No VPS configured."}
|
|
|
|
# Run apt update silently on the VPS
|
|
await run_remote_quick("apt-get update -qq", settings)
|
|
|
|
# Run apt list --upgradable on the VPS
|
|
ret, stdout, _ = await run_remote_quick("apt list --upgradable 2>/dev/null", settings)
|
|
packages: List[Dict[str, str]] = []
|
|
security_count = 0
|
|
|
|
if ret == 0:
|
|
lines = stdout.splitlines()
|
|
pattern = re.compile(r"^([^/]+)/([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+\[upgradable from:\s+([^\]]+)\]")
|
|
for line in lines:
|
|
line = line.strip()
|
|
if not line or line.startswith("Listing..."):
|
|
continue
|
|
match = pattern.match(line)
|
|
if match:
|
|
pkg_name, repo, new_ver, arch, cur_ver = match.groups()
|
|
is_sec = "security" in repo.lower()
|
|
if is_sec:
|
|
security_count += 1
|
|
packages.append({
|
|
"name": pkg_name,
|
|
"repository": repo,
|
|
"current_version": cur_ver,
|
|
"new_version": new_ver,
|
|
"arch": arch,
|
|
"is_security": is_sec
|
|
})
|
|
|
|
return {
|
|
"total": len(packages),
|
|
"security_count": security_count,
|
|
"packages": packages
|
|
}
|
|
|
|
async def trigger_system_upgrade(dist_upgrade: bool = False) -> Dict[str, Any]:
|
|
settings = load_settings()
|
|
vps_host = settings.get("vps_host", "").strip()
|
|
if not vps_host:
|
|
return {"success": False, "error": "No VPS configured. Set VPS Host in Settings."}
|
|
|
|
cmd_type = "dist-upgrade" if dist_upgrade else "upgrade"
|
|
ssh_prefix = build_ssh_prefix(settings)
|
|
|
|
# Remote upgrade command with proper noninteractive env
|
|
remote_cmd = f"DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold {cmd_type} -y"
|
|
full_cmd = ssh_prefix + [remote_cmd]
|
|
|
|
res = await runner.run_command_job(
|
|
name=f"Remote VPS Packages {cmd_type.capitalize()}",
|
|
cmd=full_cmd,
|
|
job_type="update_system",
|
|
description=f"Automated package upgrade on remote VPS ({vps_host}) via apt-get {cmd_type} -y"
|
|
)
|
|
return res
|
|
|
|
async def trigger_system_reboot() -> Dict[str, Any]:
|
|
settings = load_settings()
|
|
vps_host = settings.get("vps_host", "").strip()
|
|
if not vps_host:
|
|
return {"success": False, "error": "No VPS configured."}
|
|
|
|
ssh_prefix = build_ssh_prefix(settings)
|
|
remote_cmd = "shutdown -r +1 'Reboot initiated from Unraid Pangolin Sentinel'"
|
|
full_cmd = ssh_prefix + [remote_cmd]
|
|
|
|
res = await runner.run_command_job(
|
|
name=f"Remote VPS Reboot ({vps_host})",
|
|
cmd=full_cmd,
|
|
job_type="general",
|
|
description="Initiate safe remote VPS reboot scheduled in 1 minute"
|
|
)
|
|
return res
|