Files

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