Compare commits
8
Commits
7c665cac33
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4eb7a2fa9 | ||
|
|
2527045bc6 | ||
|
|
7875ef0bc2 | ||
|
|
877ff62105 | ||
|
|
5c44884c5c | ||
|
|
f74a3e09f5 | ||
|
|
6d03f5de43 | ||
|
|
6064002ac6 |
No files matched your search
+80
-10
@@ -87,17 +87,87 @@ async def run_backup_task(
|
|||||||
remote_commands = [
|
remote_commands = [
|
||||||
f'echo "=== STEP 1: Creating Hot-Snapshot on Remote VPS ({vps_host}) ==="',
|
f'echo "=== STEP 1: Creating Hot-Snapshot on Remote VPS ({vps_host}) ==="',
|
||||||
f'mkdir -p "{remote_stage}/pangolin"',
|
f'mkdir -p "{remote_stage}/pangolin"',
|
||||||
f'echo "--- Staging Pangolin files from {p_dir} ---"',
|
f'''
|
||||||
f'if [ -d "{p_dir}" ]; then rsync -av --exclude="*.db" --exclude="*.db-wal" --exclude="*.db-shm" "{p_dir}/" "{remote_stage}/pangolin/"; fi',
|
eval P_DIR="{p_dir}"
|
||||||
f'echo "--- Performing zero-lock hot-backup of SQLite databases on VPS ---"',
|
P_DIR="${{P_DIR/#~/$HOME}}"
|
||||||
f"""
|
|
||||||
for db in $(find "{p_dir}" -name "*.db" -o -name "*.sqlite" 2>/dev/null); do
|
# Auto-discovery if configured directory does not exist or is empty
|
||||||
rel=$(realpath --relative-to="{p_dir}" "$db")
|
if [ ! -d "$P_DIR" ] || [ -z "$(ls -A "$P_DIR" 2>/dev/null)" ]; then
|
||||||
mkdir -p "{remote_stage}/pangolin/$(dirname "$rel")"
|
echo "Notice: Configured path $P_DIR is empty or does not exist."
|
||||||
echo "Hot-backing up $rel..."
|
echo "Searching VPS for Pangolin Docker compose stack..."
|
||||||
sqlite3 "$db" ".backup '{remote_stage}/pangolin/$rel'" 2>/dev/null || cp -v "$db" "{remote_stage}/pangolin/$rel"
|
DETECTED=$(docker inspect --format '{{{{index .Config.Labels "com.docker.compose.project.working_dir"}}}}' $(docker ps -aq --filter "name=pangolin" 2>/dev/null) 2>/dev/null | head -n 1)
|
||||||
|
if [ -z "$DETECTED" ] || [ ! -d "$DETECTED" ]; then
|
||||||
|
for candidate in "/opt/pangolin" "$HOME/pangolin" "/root/pangolin" "$HOME/config" "/root/config" "$HOME" "/root"; do
|
||||||
|
if [ -d "$candidate" ] && [ -n "$(ls -A "$candidate" 2>/dev/null)" ]; then
|
||||||
|
DETECTED="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
if [ -n "$DETECTED" ] && [ -d "$DETECTED" ]; then
|
||||||
|
echo "--> Auto-discovered active Pangolin directory at: $DETECTED"
|
||||||
|
P_DIR="$DETECTED"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Standard HHF Pangolin stack items
|
||||||
|
BACKUP_ITEMS=("docker-compose.yml" "compose.yaml" "docker-compose.yaml" ".env" "config" "crowdsec" "traefik")
|
||||||
|
|
||||||
|
echo "--- Staging Pangolin files from $P_DIR ---"
|
||||||
|
if [ -d "$P_DIR" ]; then
|
||||||
|
if [ -f "$P_DIR/docker-compose.yml" ] || [ -d "$P_DIR/config" ]; then
|
||||||
|
echo "Detected Pangolin stack root at $P_DIR"
|
||||||
|
for item in "${{BACKUP_ITEMS[@]}}"; do
|
||||||
|
if [ -e "$P_DIR/$item" ]; then
|
||||||
|
echo "Staging $item..."
|
||||||
|
mkdir -p "{remote_stage}/pangolin/$(dirname "$item")"
|
||||||
|
cp -a "$P_DIR/$item" "{remote_stage}/pangolin/$item" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
echo "Staging contents of $P_DIR into {remote_stage}/pangolin/config/..."
|
||||||
|
mkdir -p "{remote_stage}/pangolin/config"
|
||||||
|
rsync -av --exclude="*.db" --exclude="*.db-wal" --exclude="*.db-shm" "$P_DIR/" "{remote_stage}/pangolin/config/"
|
||||||
|
|
||||||
|
# Check parent folder for companion compose and env files
|
||||||
|
PARENT_DIR="$(dirname "$P_DIR")"
|
||||||
|
for extra in docker-compose.yml compose.yaml docker-compose.yaml .env; do
|
||||||
|
if [ -f "$PARENT_DIR/$extra" ]; then
|
||||||
|
echo "--- Staging companion $extra from $PARENT_DIR ---"
|
||||||
|
cp "$PARENT_DIR/$extra" "{remote_stage}/pangolin/" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "--- Performing zero-lock hot-backup of SQLite databases on VPS ---"
|
||||||
|
if [ -d "$P_DIR/config" ]; then
|
||||||
|
TARGET_BASE="{remote_stage}/pangolin/config"
|
||||||
|
SEARCH_BASE="$P_DIR/config"
|
||||||
|
else
|
||||||
|
TARGET_BASE="{remote_stage}/pangolin"
|
||||||
|
SEARCH_BASE="$P_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for db in $(find "$SEARCH_BASE" -maxdepth 4 -name "*.db" -o -name "*.sqlite" 2>/dev/null); do
|
||||||
|
rel=$(realpath --relative-to="$SEARCH_BASE" "$db")
|
||||||
|
mkdir -p "$TARGET_BASE/$(dirname "$rel")"
|
||||||
|
echo "Hot-backing up SQLite database: $rel..."
|
||||||
|
sqlite3 "$db" ".backup '$TARGET_BASE/$rel'" 2>/dev/null || cp -v "$db" "$TARGET_BASE/$rel"
|
||||||
done
|
done
|
||||||
"""
|
# Clean dangling lock/WAL files in staging archive
|
||||||
|
find "" -name "*.db-wal" -delete -o -name "*.db-shm" -delete 2>/dev/null || true
|
||||||
|
|
||||||
|
# Backup any named Docker volumes attached to pangolin containers
|
||||||
|
for vol in $(docker inspect --format '{{{{range .Mounts}}}}{{{{if eq .Type "volume"}}}}{{{{.Source}}}}{{{{"\\n"}}}}{{{{end}}}}{{{{end}}}}' $(docker ps -aq --filter "name=pangolin" 2>/dev/null) 2>/dev/null); do
|
||||||
|
if [ -d "$vol" ] && [ -n "$(ls -A "$vol" 2>/dev/null)" ]; then
|
||||||
|
vname=$(basename "$vol")
|
||||||
|
echo "--- Staging Docker Volume: $vname from $vol ---"
|
||||||
|
mkdir -p "{remote_stage}/pangolin/volumes/$vname"
|
||||||
|
cp -a "$vol/." "{remote_stage}/pangolin/volumes/$vname/" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
'''
|
||||||
]
|
]
|
||||||
|
|
||||||
if scope in ["vps_config", "full"]:
|
if scope in ["vps_config", "full"]:
|
||||||
|
|||||||
+56
-9
@@ -31,11 +31,29 @@ async def get_pangolin_stack_status() -> Dict[str, Any]:
|
|||||||
|
|
||||||
# Query Remote VPS for files, DB, and Docker status
|
# Query Remote VPS for files, DB, and Docker status
|
||||||
check_script = f"""
|
check_script = f"""
|
||||||
[ -f "{p_dir}/docker-compose.yml" ] || [ -f "{p_dir}/compose.yaml" ] && echo "COMPOSE_EXISTS" || echo "COMPOSE_NO"
|
eval P_DIR="{p_dir}"
|
||||||
[ -f "{p_dir}/config/config.yml" ] || [ -f "{p_dir}/config.yml" ] && echo "CONFIG_EXISTS" || echo "CONFIG_NO"
|
P_DIR="${{P_DIR/#~/$HOME}}"
|
||||||
|
if [ ! -d "$P_DIR" ] || [ -z "$(ls -A "$P_DIR" 2>/dev/null)" ]; then
|
||||||
|
DETECTED=$(docker inspect --format '{{{{index .Config.Labels \"com.docker.compose.project.working_dir\"}}}}' $(docker ps -aq --filter "name=pangolin" 2>/dev/null) 2>/dev/null | head -n 1)
|
||||||
|
if [ -z "$DETECTED" ] || [ ! -d "$DETECTED" ]; then
|
||||||
|
for candidate in "$HOME/config" "/root/config" "$HOME/pangolin" "/root/pangolin" "$HOME" "/opt/pangolin" /home/*/pangolin /var/lib/pangolin /srv/pangolin /etc/pangolin; do
|
||||||
|
if [ -d "$candidate" ] && [ -n "$(ls -A "$candidate" 2>/dev/null)" ]; then
|
||||||
|
DETECTED="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
if [ -n "$DETECTED" ] && [ -d "$DETECTED" ]; then
|
||||||
|
P_DIR="$DETECTED"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "RESOLVED_DIR:$P_DIR"
|
||||||
|
|
||||||
|
[ -f "$P_DIR/docker-compose.yml" ] || [ -f "$P_DIR/compose.yaml" ] && echo "COMPOSE_EXISTS" || echo "COMPOSE_NO"
|
||||||
|
[ -f "$P_DIR/config/config.yml" ] || [ -f "$P_DIR/config.yml" ] && echo "CONFIG_EXISTS" || echo "CONFIG_NO"
|
||||||
|
|
||||||
# Check DB
|
# Check DB
|
||||||
db=$(find "{p_dir}" -name "*.db" -o -name "*.sqlite" 2>/dev/null | head -n 1)
|
db=$(find "$P_DIR" -name "*.db" -o -name "*.sqlite" 2>/dev/null | head -n 1)
|
||||||
if [ -n "$db" ]; then
|
if [ -n "$db" ]; then
|
||||||
echo "DB_PATH:$db"
|
echo "DB_PATH:$db"
|
||||||
wc -c "$db" 2>/dev/null | awk '{{print $1}}'
|
wc -c "$db" 2>/dev/null | awk '{{print $1}}'
|
||||||
@@ -45,7 +63,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Docker containers
|
# Docker containers
|
||||||
docker ps -a --format '{{json .}}' 2>/dev/null || true
|
docker ps -a --format '{{{{json .}}}}' 2>/dev/null || true
|
||||||
"""
|
"""
|
||||||
ret, stdout, stderr = await run_remote_quick(check_script, settings)
|
ret, stdout, stderr = await run_remote_quick(check_script, settings)
|
||||||
|
|
||||||
@@ -54,7 +72,7 @@ docker ps -a --format '{{json .}}' 2>/dev/null || true
|
|||||||
"vps_configured": True,
|
"vps_configured": True,
|
||||||
"connected": False,
|
"connected": False,
|
||||||
"installed": False,
|
"installed": False,
|
||||||
"path": p_dir,
|
"path": resolved_path,
|
||||||
"compose_exists": False,
|
"compose_exists": False,
|
||||||
"config_exists": False,
|
"config_exists": False,
|
||||||
"database": {"found": False, "path": None, "size_bytes": 0},
|
"database": {"found": False, "path": None, "size_bytes": 0},
|
||||||
@@ -66,6 +84,12 @@ docker ps -a --format '{{json .}}' 2>/dev/null || true
|
|||||||
}
|
}
|
||||||
|
|
||||||
lines = stdout.splitlines()
|
lines = stdout.splitlines()
|
||||||
|
resolved_path = p_dir
|
||||||
|
for l in lines:
|
||||||
|
if l.startswith("RESOLVED_DIR:"):
|
||||||
|
resolved_path = l.split(":", 1)[1].strip()
|
||||||
|
break
|
||||||
|
|
||||||
compose_exists = any("COMPOSE_EXISTS" in l for l in lines)
|
compose_exists = any("COMPOSE_EXISTS" in l for l in lines)
|
||||||
config_exists = any("CONFIG_EXISTS" in l for l in lines)
|
config_exists = any("CONFIG_EXISTS" in l for l in lines)
|
||||||
|
|
||||||
@@ -107,7 +131,7 @@ docker ps -a --format '{{json .}}' 2>/dev/null || true
|
|||||||
"vps_configured": True,
|
"vps_configured": True,
|
||||||
"connected": True,
|
"connected": True,
|
||||||
"installed": compose_exists,
|
"installed": compose_exists,
|
||||||
"path": p_dir,
|
"path": resolved_path,
|
||||||
"compose_exists": compose_exists,
|
"compose_exists": compose_exists,
|
||||||
"config_exists": config_exists,
|
"config_exists": config_exists,
|
||||||
"database": {
|
"database": {
|
||||||
@@ -133,16 +157,39 @@ async def trigger_pangolin_update() -> Dict[str, Any]:
|
|||||||
|
|
||||||
# Remote update script executed on the VPS
|
# Remote update script executed on the VPS
|
||||||
remote_script = f"""
|
remote_script = f"""
|
||||||
|
eval P_DIR="{p_dir}"
|
||||||
|
P_DIR="${{P_DIR/#~/$HOME}}"
|
||||||
|
|
||||||
|
# Locate compose directory
|
||||||
|
if [ -f "$P_DIR/docker-compose.yml" ] || [ -f "$P_DIR/compose.yaml" ]; then
|
||||||
|
COMPOSE_DIR="$P_DIR"
|
||||||
|
elif [ -f "$(dirname "$P_DIR")/docker-compose.yml" ] || [ -f "$(dirname "$P_DIR")/compose.yaml" ]; then
|
||||||
|
COMPOSE_DIR="$(dirname "$P_DIR")"
|
||||||
|
else
|
||||||
|
COMPOSE_DIR="$P_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "=== STEP 1: Connecting to Pangolin VPS ({vps_host}) ==="
|
echo "=== STEP 1: Connecting to Pangolin VPS ({vps_host}) ==="
|
||||||
cd "{p_dir}" || exit 1
|
echo "Operating Directory: $COMPOSE_DIR"
|
||||||
|
cd "$COMPOSE_DIR" || exit 1
|
||||||
|
|
||||||
|
# Detect docker compose version
|
||||||
|
if docker compose version >/dev/null 2>&1; then
|
||||||
|
DC="docker compose"
|
||||||
|
elif command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
DC="docker-compose"
|
||||||
|
else
|
||||||
|
echo "ERROR: Neither 'docker compose' nor 'docker-compose' found on VPS."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== STEP 2: Pulling latest images on VPS ==="
|
echo "=== STEP 2: Pulling latest images on VPS ==="
|
||||||
docker compose pull
|
$DC pull
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== STEP 3: Applying stack updates & recreating containers ==="
|
echo "=== STEP 3: Applying stack updates & recreating containers ==="
|
||||||
docker compose up -d --remove-orphans
|
$DC up -d --remove-orphans
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== STEP 4: Verifying running container status ==="
|
echo "=== STEP 4: Verifying running container status ==="
|
||||||
|
|||||||
+53
-66
@@ -4,7 +4,7 @@ import shutil
|
|||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Any, List, Optional, Tuple
|
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:
|
def get_public_key() -> str:
|
||||||
if DEFAULT_PUB_KEY_FILE.exists():
|
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))
|
raw_key_path = settings.get("vps_ssh_key_path", str(DEFAULT_KEY_FILE))
|
||||||
key_path = resolve_key_path(raw_key_path)
|
key_path = resolve_key_path(raw_key_path)
|
||||||
password = settings.get("vps_ssh_password", "")
|
password = settings.get("vps_ssh_password", "")
|
||||||
|
known_hosts = str(DATA_DIR / "known_hosts")
|
||||||
|
|
||||||
ssh_opts = [
|
ssh_opts = [
|
||||||
|
"-T",
|
||||||
"-o", "StrictHostKeyChecking=accept-new",
|
"-o", "StrictHostKeyChecking=accept-new",
|
||||||
|
"-o", f"UserKnownHostsFile={known_hosts}",
|
||||||
"-o", "ConnectTimeout=8",
|
"-o", "ConnectTimeout=8",
|
||||||
"-o", "ServerAliveInterval=15",
|
"-o", "ServerAliveInterval=15",
|
||||||
"-p", port
|
"-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))
|
raw_key_path = settings.get("vps_ssh_key_path", str(DEFAULT_KEY_FILE))
|
||||||
key_path = resolve_key_path(raw_key_path)
|
key_path = resolve_key_path(raw_key_path)
|
||||||
password = settings.get("vps_ssh_password", "")
|
password = settings.get("vps_ssh_password", "")
|
||||||
|
known_hosts = str(DATA_DIR / "known_hosts")
|
||||||
|
|
||||||
scp_opts = [
|
scp_opts = [
|
||||||
"-P", port,
|
"-P", port,
|
||||||
"-o", "StrictHostKeyChecking=accept-new",
|
"-o", "StrictHostKeyChecking=accept-new",
|
||||||
|
"-o", f"UserKnownHostsFile={known_hosts}",
|
||||||
"-o", "ConnectTimeout=10"
|
"-o", "ConnectTimeout=10"
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -99,79 +104,61 @@ async def run_remote_quick(cmd: str, settings: Optional[Dict[str, Any]] = None)
|
|||||||
stdout, stderr = await proc.communicate()
|
stdout, stderr = await proc.communicate()
|
||||||
return proc.returncode, stdout.decode('utf-8', errors='replace'), stderr.decode('utf-8', errors='replace')
|
return proc.returncode, stdout.decode('utf-8', errors='replace'), stderr.decode('utf-8', errors='replace')
|
||||||
except Exception as e:
|
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]:
|
async def test_vps_connection(settings: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
s = custom_settings or load_settings()
|
if not settings:
|
||||||
host = s.get("vps_host", "").strip()
|
settings = load_settings()
|
||||||
|
|
||||||
|
host = settings.get("vps_host", "").strip()
|
||||||
if not host:
|
if not host:
|
||||||
return {
|
return {"success": False, "error": "No VPS host configured. Please provide IP or hostname."}
|
||||||
"success": False,
|
|
||||||
"connected": False,
|
|
||||||
"error": "No VPS Host / IP configured. Please specify the VPS address in Settings."
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run comprehensive diagnostic script on remote VPS
|
# 1. Test basic SSH connectivity & gather OS summary
|
||||||
diag_script = """
|
cmd_ping = 'echo "PANGOLIN_PONG" && uname -srm && cat /etc/os-release 2>/dev/null | grep PRETTY_NAME | cut -d= -f2 | tr -d \'"\''
|
||||||
echo "===PING_OK==="
|
code, stdout, stderr = await run_remote_quick(cmd_ping, settings)
|
||||||
uname -n
|
|
||||||
cat /etc/os-release | grep PRETTY_NAME | cut -d= -f2 | tr -d '"'
|
if code != 0:
|
||||||
uname -r
|
err_msg = stderr.strip() or "Connection timed out or failed."
|
||||||
uptime -p 2>/dev/null || uptime
|
if "Permission denied" in err_msg:
|
||||||
which docker >/dev/null 2>&1 && echo "DOCKER_FOUND" || echo "DOCKER_NOT_FOUND"
|
err_msg += " (SSH Authentication failed. Check key in ~/.ssh/authorized_keys or password)"
|
||||||
docker ps >/dev/null 2>&1 && echo "DOCKER_RUNNING" || echo "DOCKER_STOPPED"
|
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").strip() or "/opt/pangolin"
|
||||||
|
chk_dir_cmd = f"""
|
||||||
|
eval P_DIR="{pangolin_dir}"
|
||||||
|
P_DIR="${{P_DIR/#~/$HOME}}"
|
||||||
|
[ -d "$P_DIR" ] && echo "DIR_EXISTS" || echo "DIR_MISSING"
|
||||||
"""
|
"""
|
||||||
ret, stdout, stderr = await run_remote_quick(diag_script, s)
|
_, stdout_dir, _ = await run_remote_quick(chk_dir_cmd, settings)
|
||||||
|
pangolin_dir_found = "DIR_EXISTS" in stdout_dir
|
||||||
|
|
||||||
if ret != 0 or "===PING_OK===" not in stdout:
|
# 4. Check SQLite database status in Pangolin directory
|
||||||
err = stderr.strip() or "Connection failed. Please check Host, Port, and SSH credentials."
|
chk_db_cmd = f"""
|
||||||
if "Permission denied" in err:
|
eval P_DIR="{pangolin_dir}"
|
||||||
err = "Permission denied (SSH authentication failed). Please ensure your SSH public key or password is correct on the VPS."
|
P_DIR="${{P_DIR/#~/$HOME}}"
|
||||||
elif "Connection timed out" in err or "No route to host" in err:
|
find "$P_DIR" -name "*.db" -o -name "*.sqlite" 2>/dev/null
|
||||||
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)
|
_, stdout_db, _ = await run_remote_quick(chk_db_cmd, settings)
|
||||||
p_lines = [l.strip() for l in p_out.splitlines() if l.strip()]
|
dbs_found = [p.strip() for p in stdout_db.strip().split("\n") if p.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 {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"connected": True,
|
"os_name": os_name,
|
||||||
"hostname": hostname,
|
|
||||||
"os": os_name,
|
|
||||||
"kernel": kernel,
|
"kernel": kernel,
|
||||||
"uptime": uptime_str,
|
"docker_installed": docker_installed,
|
||||||
"docker_installed": docker_found,
|
"docker_version": docker_version,
|
||||||
"docker_running": docker_running,
|
"pangolin_dir_found": pangolin_dir_found,
|
||||||
"pangolin_dir_exists": dir_ok,
|
"sqlite_databases": dbs_found,
|
||||||
"compose_file_exists": compose_ok,
|
"message": "SSH Connection successful! Remote host is ready for backups and updates."
|
||||||
"database_file": db_file,
|
|
||||||
"message": f"Successfully connected to {hostname} ({host}) via SSH!"
|
|
||||||
}
|
}
|
||||||
+4
-3
@@ -5,7 +5,8 @@ set -e
|
|||||||
DATA_PATH="${DATA_DIR:-/data}"
|
DATA_PATH="${DATA_DIR:-/data}"
|
||||||
BACKUP_PATH="${BACKUP_DIR:-/backups}"
|
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
|
# Set secure permissions on SSH private key if it exists
|
||||||
if [ -f "$DATA_PATH/id_ed25519" ]; then
|
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
|
groupadd -g "$PGID" pangolin 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create or update user
|
# Create or update user with persistent home in DATA_PATH
|
||||||
if ! getent passwd pangolin >/dev/null 2>&1; then
|
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
|
fi
|
||||||
|
|
||||||
# Ensure ownership of persistent data and backup volumes
|
# Ensure ownership of persistent data and backup volumes
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
<?xml version="1.0"?>
|
<?xml version="1.0"?>
|
||||||
<Container version="2">
|
<Container version="2">
|
||||||
<Name>PangolinSentinel</Name>
|
<Name>PangolinSentinel</Name>
|
||||||
<Repository>pangolin-sentinel:latest</Repository>
|
<Repository>fluffyranger/pangolin-sentinel:latest</Repository>
|
||||||
<Registry>https://hub.docker.com/</Registry>
|
<Registry>https://hub.docker.com/r/fluffyranger/pangolin-sentinel</Registry>
|
||||||
<Network>bridge</Network>
|
<Network>bridge</Network>
|
||||||
<MyIP/>
|
<MyIP/>
|
||||||
<Shell>bash</Shell>
|
<Shell>bash</Shell>
|
||||||
<Privileged>false</Privileged>
|
<Privileged>false</Privileged>
|
||||||
<Support/>
|
<Support>https://github.com/FluffyRanger/PangolinSentinel/issues</Support>
|
||||||
<Project>https://github.com/fosrl/pangolin</Project>
|
<Project>https://github.com/FluffyRanger/PangolinSentinel</Project>
|
||||||
<Overview>Pangolin Sentinel is a dedicated Unraid-to-VPS backup and update manager. Pull zero-lock hot backups of your remote Pangolin Reverse Proxy stack (SQLite databases, Traefik SSL certs, configurations) directly into your Unraid array, trigger remote stack updates, and monitor remote VPS OS packages and metrics over secure SSH.</Overview>
|
<Overview>Pangolin Sentinel is a dedicated Unraid-to-VPS backup and update manager. Pull zero-lock hot backups of your remote Pangolin Reverse Proxy stack (SQLite databases, Traefik SSL certs, configurations) directly into your Unraid array, trigger remote stack updates, and monitor remote VPS OS packages and metrics over secure SSH.</Overview>
|
||||||
<Category>Backup: Tools: Network:Management</Category>
|
<Category>Backup: Tools: Network:Management</Category>
|
||||||
<WebUI>http://[IP]:[PORT:8888]/</WebUI>
|
<WebUI>http://[IP]:[PORT:8888]/</WebUI>
|
||||||
<TemplateURL/>
|
<TemplateURL>https://raw.githubusercontent.com/FluffyRanger/PangolinSentinel/main/pangolin-sentinel-unraid.xml</TemplateURL>
|
||||||
<Icon>http://[IP]:[PORT:8888]/assets/icon.png</Icon>
|
<Icon>https://raw.githubusercontent.com/FluffyRanger/PangolinSentinel/main/icon.png</Icon>
|
||||||
<ExtraParams/>
|
<ExtraParams/>
|
||||||
<PostArgs/>
|
<PostArgs/>
|
||||||
<CPUset/>
|
<CPUset/>
|
||||||
|
|||||||
Reference in new issue
Block a user