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