Files
PangolinSentinel/backend/remote.py
T

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."
}