157 lines
6.1 KiB
Python
157 lines
6.1 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, DATA_DIR
|
|
|
|
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", "")
|
|
known_hosts = str(DATA_DIR / "known_hosts")
|
|
|
|
ssh_opts = [
|
|
"-T",
|
|
"-o", "StrictHostKeyChecking=accept-new",
|
|
"-o", f"UserKnownHostsFile={known_hosts}",
|
|
"-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", "")
|
|
known_hosts = str(DATA_DIR / "known_hosts")
|
|
|
|
scp_opts = [
|
|
"-P", port,
|
|
"-o", "StrictHostKeyChecking=accept-new",
|
|
"-o", f"UserKnownHostsFile={known_hosts}",
|
|
"-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(settings: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
if not settings:
|
|
settings = load_settings()
|
|
|
|
host = settings.get("vps_host", "").strip()
|
|
if not host:
|
|
return {"success": False, "error": "No VPS host configured. Please provide IP or hostname."}
|
|
|
|
# 1. Test basic SSH connectivity & gather OS summary
|
|
cmd_ping = 'echo "PANGOLIN_PONG" && uname -srm && cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d= -f2 | tr -d \'"\''
|
|
code, stdout, stderr = await run_remote_quick(cmd_ping, settings)
|
|
|
|
if code != 0:
|
|
err_msg = stderr.strip() or "Connection timed out or failed."
|
|
if "Permission denied" in err_msg:
|
|
err_msg += " (SSH Authentication failed. Check key in ~/.ssh/authorized_keys or password)"
|
|
return {"success": False, "error": err_msg}
|
|
|
|
lines = stdout.strip().split("\n")
|
|
os_name = lines[2] if len(lines) > 2 else (lines[1] if len(lines) > 1 else "Linux")
|
|
kernel = lines[1] if len(lines) > 1 else ""
|
|
|
|
# 2. Check Docker availability on VPS
|
|
code_docker, stdout_docker, _ = await run_remote_quick('docker --version', settings)
|
|
docker_installed = (code_docker == 0)
|
|
docker_version = stdout_docker.strip() if docker_installed else "Not detected"
|
|
|
|
# 3. Check Pangolin Directory
|
|
pangolin_dir = settings.get("vps_pangolin_dir", "/opt/pangolin")
|
|
chk_dir_cmd = f'[ -d "{pangolin_dir}" ] && echo "DIR_EXISTS" || echo "DIR_MISSING"'
|
|
_, stdout_dir, _ = await run_remote_quick(chk_dir_cmd, settings)
|
|
pangolin_dir_found = "DIR_EXISTS" in stdout_dir
|
|
|
|
# 4. Check SQLite database status in Pangolin directory
|
|
chk_db_cmd = f'find "{pangolin_dir}" -name "*.db" -o -name "*.sqlite" 2>/dev/null'
|
|
_, stdout_db, _ = await run_remote_quick(chk_db_cmd, settings)
|
|
dbs_found = [p.strip() for p in stdout_db.strip().split("\n") if p.strip()]
|
|
|
|
return {
|
|
"success": True,
|
|
"os_name": os_name,
|
|
"kernel": kernel,
|
|
"docker_installed": docker_installed,
|
|
"docker_version": docker_version,
|
|
"pangolin_dir_found": pangolin_dir_found,
|
|
"sqlite_databases": dbs_found,
|
|
"message": "SSH Connection successful! Remote host is ready for backups and updates."
|
|
}
|