Initial commit: Pangolin Sentinel Docker & Unraid Hub
This commit is contained in:
1 parent
178b40ba37
commit
b4095b82ea
24 files changed
+4355
No files matched your search
@@ -0,0 +1,273 @@
|
||||
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
|
||||
Reference in new issue
Block a user