diff --git a/backend/remote.py b/backend/remote.py index a8b5e37..0dffd7e 100644 --- a/backend/remote.py +++ b/backend/remote.py @@ -4,7 +4,7 @@ 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 +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(): @@ -40,9 +40,12 @@ def build_ssh_prefix(settings: Optional[Dict[str, Any]] = None) -> List[str]: 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 @@ -69,10 +72,12 @@ def build_scp_prefix(settings: Optional[Dict[str, Any]] = None) -> List[str]: 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" ] @@ -99,79 +104,53 @@ async def run_remote_quick(cmd: str, settings: Optional[Dict[str, Any]] = None) 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) + 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() +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, - "connected": False, - "error": "No VPS Host / IP configured. Please specify the VPS address in Settings." - } + return {"success": False, "error": "No VPS host configured. Please provide IP or hostname."} - # 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) + # 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 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 - } + 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 = [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 + 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 "" - # 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()] + # 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" - 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 + # 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, - "connected": True, - "hostname": hostname, - "os": os_name, + "os_name": 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!" + "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." } diff --git a/entrypoint.sh b/entrypoint.sh index 3c9d497..f482a72 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -5,7 +5,8 @@ set -e DATA_PATH="${DATA_DIR:-/data}" BACKUP_PATH="${BACKUP_DIR:-/backups}" -mkdir -p "$DATA_PATH" "$BACKUP_PATH" +mkdir -p "$DATA_PATH" "$BACKUP_PATH" "$DATA_PATH/.ssh" +export HOME="$DATA_PATH" # Set secure permissions on SSH private key if it exists if [ -f "$DATA_PATH/id_ed25519" ]; then @@ -22,9 +23,9 @@ if [ "$PUID" -ne 0 ] && [ "$PGID" -ne 0 ]; then groupadd -g "$PGID" pangolin 2>/dev/null || true fi - # Create or update user + # Create or update user with persistent home in DATA_PATH if ! getent passwd pangolin >/dev/null 2>&1; then - useradd -u "$PUID" -g "$PGID" -d /app -s /bin/bash pangolin 2>/dev/null || true + useradd -u "$PUID" -g "$PGID" -d "$DATA_PATH" -s /bin/bash pangolin 2>/dev/null || true fi # Ensure ownership of persistent data and backup volumes