178 lines
6.5 KiB
Python
178 lines
6.5 KiB
Python
import asyncio
|
|
import os
|
|
import shutil
|
|
import time
|
|
from pathlib import Path
|
|
from typing import Dict, Any, List, Optional, Tuple
|
|
from backend.config import load_settings, DEFAULT_KEY_FILE, DEFAULT_PUB_KEY_FILE
|
|
|
|
def get_public_key() -> str:
|
|
if DEFAULT_PUB_KEY_FILE.exists():
|
|
return DEFAULT_PUB_KEY_FILE.read_text(encoding="utf-8").strip()
|
|
return ""
|
|
|
|
def ensure_ssh_key() -> str:
|
|
if not DEFAULT_KEY_FILE.exists():
|
|
DEFAULT_KEY_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
os.system(f'ssh-keygen -t ed25519 -f "{DEFAULT_KEY_FILE}" -N "" -C "pangolin-sentinel@unraid" -q')
|
|
if DEFAULT_KEY_FILE.exists():
|
|
try:
|
|
os.chmod(DEFAULT_KEY_FILE, 0o600)
|
|
except Exception:
|
|
pass
|
|
return get_public_key()
|
|
|
|
def resolve_key_path(configured_path: Optional[str] = None) -> str:
|
|
if configured_path and os.path.exists(configured_path):
|
|
return configured_path
|
|
if DEFAULT_KEY_FILE.exists():
|
|
return str(DEFAULT_KEY_FILE)
|
|
return configured_path or str(DEFAULT_KEY_FILE)
|
|
|
|
def build_ssh_prefix(settings: Optional[Dict[str, Any]] = None) -> List[str]:
|
|
if not settings:
|
|
settings = load_settings()
|
|
|
|
host = settings.get("vps_host", "").strip()
|
|
port = str(settings.get("vps_port", 22))
|
|
user = settings.get("vps_user", "root").strip()
|
|
auth_method = settings.get("vps_auth_method", "key")
|
|
raw_key_path = settings.get("vps_ssh_key_path", str(DEFAULT_KEY_FILE))
|
|
key_path = resolve_key_path(raw_key_path)
|
|
password = settings.get("vps_ssh_password", "")
|
|
|
|
ssh_opts = [
|
|
"-o", "StrictHostKeyChecking=accept-new",
|
|
"-o", "ConnectTimeout=8",
|
|
"-o", "ServerAliveInterval=15",
|
|
"-p", port
|
|
]
|
|
|
|
if auth_method == "password" and password:
|
|
return ["sshpass", "-p", password, "ssh"] + ssh_opts + [f"{user}@{host}"]
|
|
else:
|
|
# Use Key Auth
|
|
if os.path.exists(key_path):
|
|
try:
|
|
os.chmod(key_path, 0o600)
|
|
except Exception:
|
|
pass
|
|
return ["ssh", "-i", key_path, "-o", "BatchMode=yes"] + ssh_opts + [f"{user}@{host}"]
|
|
return ["ssh", "-o", "BatchMode=yes"] + ssh_opts + [f"{user}@{host}"]
|
|
|
|
def build_scp_prefix(settings: Optional[Dict[str, Any]] = None) -> List[str]:
|
|
if not settings:
|
|
settings = load_settings()
|
|
|
|
port = str(settings.get("vps_port", 22))
|
|
auth_method = settings.get("vps_auth_method", "key")
|
|
raw_key_path = settings.get("vps_ssh_key_path", str(DEFAULT_KEY_FILE))
|
|
key_path = resolve_key_path(raw_key_path)
|
|
password = settings.get("vps_ssh_password", "")
|
|
|
|
scp_opts = [
|
|
"-P", port,
|
|
"-o", "StrictHostKeyChecking=accept-new",
|
|
"-o", "ConnectTimeout=10"
|
|
]
|
|
|
|
if auth_method == "password" and password:
|
|
return ["sshpass", "-p", password, "scp"] + scp_opts
|
|
else:
|
|
if os.path.exists(key_path):
|
|
try:
|
|
os.chmod(key_path, 0o600)
|
|
except Exception:
|
|
pass
|
|
return ["scp", "-i", key_path, "-o", "BatchMode=yes"] + scp_opts
|
|
return ["scp", "-o", "BatchMode=yes"] + scp_opts
|
|
|
|
async def run_remote_quick(cmd: str, settings: Optional[Dict[str, Any]] = None) -> Tuple[int, str, str]:
|
|
prefix = build_ssh_prefix(settings)
|
|
full_cmd = prefix + [cmd]
|
|
try:
|
|
proc = await asyncio.create_subprocess_exec(
|
|
*full_cmd,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE
|
|
)
|
|
stdout, stderr = await proc.communicate()
|
|
return proc.returncode, stdout.decode('utf-8', errors='replace'), stderr.decode('utf-8', errors='replace')
|
|
except Exception as e:
|
|
return -1, "", str(e)
|
|
|
|
async def test_vps_connection(custom_settings: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
s = custom_settings or load_settings()
|
|
host = s.get("vps_host", "").strip()
|
|
if not host:
|
|
return {
|
|
"success": False,
|
|
"connected": False,
|
|
"error": "No VPS Host / IP configured. Please specify the VPS address in Settings."
|
|
}
|
|
|
|
# Run comprehensive diagnostic script on remote VPS
|
|
diag_script = """
|
|
echo "===PING_OK==="
|
|
uname -n
|
|
cat /etc/os-release | grep PRETTY_NAME | cut -d= -f2 | tr -d '"'
|
|
uname -r
|
|
uptime -p 2>/dev/null || uptime
|
|
which docker >/dev/null 2>&1 && echo "DOCKER_FOUND" || echo "DOCKER_NOT_FOUND"
|
|
docker ps >/dev/null 2>&1 && echo "DOCKER_RUNNING" || echo "DOCKER_STOPPED"
|
|
"""
|
|
ret, stdout, stderr = await run_remote_quick(diag_script, s)
|
|
|
|
if ret != 0 or "===PING_OK===" not in stdout:
|
|
err = stderr.strip() or "Connection failed. Please check Host, Port, and SSH credentials."
|
|
if "Permission denied" in err:
|
|
err = "Permission denied (SSH authentication failed). Please ensure your SSH public key or password is correct on the VPS."
|
|
elif "Connection timed out" in err or "No route to host" in err:
|
|
err = f"Could not reach {host}:{s.get('vps_port', 22)}. Verify firewall, IP address, and SSH port."
|
|
return {
|
|
"success": False,
|
|
"connected": False,
|
|
"error": err
|
|
}
|
|
|
|
lines = [line.strip() for line in stdout.splitlines() if line.strip()]
|
|
hostname = lines[1] if len(lines) > 1 else host
|
|
os_name = lines[2] if len(lines) > 2 else "Linux"
|
|
kernel = lines[3] if len(lines) > 3 else ""
|
|
uptime_str = lines[4] if len(lines) > 4 else ""
|
|
docker_found = "DOCKER_FOUND" in stdout
|
|
docker_running = "DOCKER_RUNNING" in stdout
|
|
|
|
# Check Pangolin path on VPS
|
|
p_dir = s.get("vps_pangolin_dir", "/opt/pangolin")
|
|
p_script = f"""
|
|
[ -d "{p_dir}" ] && echo "DIR_OK" || echo "DIR_MISSING"
|
|
[ -f "{p_dir}/docker-compose.yml" ] || [ -f "{p_dir}/compose.yaml" ] && echo "COMPOSE_OK" || echo "COMPOSE_MISSING"
|
|
find "{p_dir}" -name "*.db" -o -name "*.sqlite" 2>/dev/null | head -n 1
|
|
"""
|
|
_, p_out, _ = await run_remote_quick(p_script, s)
|
|
p_lines = [l.strip() for l in p_out.splitlines() if l.strip()]
|
|
|
|
dir_ok = "DIR_OK" in p_lines
|
|
compose_ok = "COMPOSE_OK" in p_lines
|
|
db_file = None
|
|
for l in p_lines:
|
|
if l.endswith((".db", ".sqlite")):
|
|
db_file = l
|
|
break
|
|
|
|
return {
|
|
"success": True,
|
|
"connected": True,
|
|
"hostname": hostname,
|
|
"os": os_name,
|
|
"kernel": kernel,
|
|
"uptime": uptime_str,
|
|
"docker_installed": docker_found,
|
|
"docker_running": docker_running,
|
|
"pangolin_dir_exists": dir_ok,
|
|
"compose_file_exists": compose_ok,
|
|
"database_file": db_file,
|
|
"message": f"Successfully connected to {hostname} ({host}) via SSH!"
|
|
}
|