Compare commits
8
Commits
| 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 = [
|
||||
f'echo "=== STEP 1: Creating Hot-Snapshot on Remote VPS ({vps_host}) ==="',
|
||||
f'mkdir -p "{remote_stage}/pangolin"',
|
||||
f'echo "--- Staging Pangolin files from {p_dir} ---"',
|
||||
f'if [ -d "{p_dir}" ]; then rsync -av --exclude="*.db" --exclude="*.db-wal" --exclude="*.db-shm" "{p_dir}/" "{remote_stage}/pangolin/"; fi',
|
||||
f'echo "--- Performing zero-lock hot-backup of SQLite databases on VPS ---"',
|
||||
f"""
|
||||
for db in $(find "{p_dir}" -name "*.db" -o -name "*.sqlite" 2>/dev/null); do
|
||||
rel=$(realpath --relative-to="{p_dir}" "$db")
|
||||
mkdir -p "{remote_stage}/pangolin/$(dirname "$rel")"
|
||||
echo "Hot-backing up $rel..."
|
||||
sqlite3 "$db" ".backup '{remote_stage}/pangolin/$rel'" 2>/dev/null || cp -v "$db" "{remote_stage}/pangolin/$rel"
|
||||
f'''
|
||||
eval P_DIR="{p_dir}"
|
||||
P_DIR="${{P_DIR/#~/$HOME}}"
|
||||
|
||||
# Auto-discovery if configured directory does not exist or is empty
|
||||
if [ ! -d "$P_DIR" ] || [ -z "$(ls -A "$P_DIR" 2>/dev/null)" ]; then
|
||||
echo "Notice: Configured path $P_DIR is empty or does not exist."
|
||||
echo "Searching VPS for Pangolin Docker compose stack..."
|
||||
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
|
||||
# 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"]:
|
||||
|
||||
+56
-9
@@ -31,11 +31,29 @@ async def get_pangolin_stack_status() -> Dict[str, Any]:
|
||||
|
||||
# Query Remote VPS for files, DB, and Docker status
|
||||
check_script = f"""
|
||||
[ -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"
|
||||
eval P_DIR="{p_dir}"
|
||||
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
|
||||
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
|
||||
echo "DB_PATH:$db"
|
||||
wc -c "$db" 2>/dev/null | awk '{{print $1}}'
|
||||
@@ -45,7 +63,7 @@ else
|
||||
fi
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -54,7 +72,7 @@ docker ps -a --format '{{json .}}' 2>/dev/null || true
|
||||
"vps_configured": True,
|
||||
"connected": False,
|
||||
"installed": False,
|
||||
"path": p_dir,
|
||||
"path": resolved_path,
|
||||
"compose_exists": False,
|
||||
"config_exists": False,
|
||||
"database": {"found": False, "path": None, "size_bytes": 0},
|
||||
@@ -66,6 +84,12 @@ docker ps -a --format '{{json .}}' 2>/dev/null || true
|
||||
}
|
||||
|
||||
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)
|
||||
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,
|
||||
"connected": True,
|
||||
"installed": compose_exists,
|
||||
"path": p_dir,
|
||||
"path": resolved_path,
|
||||
"compose_exists": compose_exists,
|
||||
"config_exists": config_exists,
|
||||
"database": {
|
||||
@@ -133,16 +157,39 @@ async def trigger_pangolin_update() -> Dict[str, Any]:
|
||||
|
||||
# Remote update script executed on the VPS
|
||||
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}) ==="
|
||||
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 "=== STEP 2: Pulling latest images on VPS ==="
|
||||
docker compose pull
|
||||
$DC pull
|
||||
|
||||
echo ""
|
||||
echo "=== STEP 3: Applying stack updates & recreating containers ==="
|
||||
docker compose up -d --remove-orphans
|
||||
$DC up -d --remove-orphans
|
||||
|
||||
echo ""
|
||||
echo "=== STEP 4: Verifying running container status ==="
|
||||
|
||||
+53
-66
@@ -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,61 @@ 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"
|
||||
# 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").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:
|
||||
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
|
||||
# 4. Check SQLite database status in Pangolin directory
|
||||
chk_db_cmd = f"""
|
||||
eval P_DIR="{pangolin_dir}"
|
||||
P_DIR="${{P_DIR/#~/$HOME}}"
|
||||
find "$P_DIR" -name "*.db" -o -name "*.sqlite" 2>/dev/null
|
||||
"""
|
||||
_, 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
|
||||
_, 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."
|
||||
}
|
||||
+4
-3
@@ -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
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<?xml version="1.0"?>
|
||||
<Container version="2">
|
||||
<Name>PangolinSentinel</Name>
|
||||
<Repository>pangolin-sentinel:latest</Repository>
|
||||
<Registry>https://hub.docker.com/</Registry>
|
||||
<Repository>fluffyranger/pangolin-sentinel:latest</Repository>
|
||||
<Registry>https://hub.docker.com/r/fluffyranger/pangolin-sentinel</Registry>
|
||||
<Network>bridge</Network>
|
||||
<MyIP/>
|
||||
<Shell>bash</Shell>
|
||||
<Privileged>false</Privileged>
|
||||
<Support/>
|
||||
<Project>https://github.com/fosrl/pangolin</Project>
|
||||
<Support>https://github.com/FluffyRanger/PangolinSentinel/issues</Support>
|
||||
<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>
|
||||
<Category>Backup: Tools: Network:Management</Category>
|
||||
<WebUI>http://[IP]:[PORT:8888]/</WebUI>
|
||||
<TemplateURL/>
|
||||
<Icon>http://[IP]:[PORT:8888]/assets/icon.png</Icon>
|
||||
<TemplateURL>https://raw.githubusercontent.com/FluffyRanger/PangolinSentinel/main/pangolin-sentinel-unraid.xml</TemplateURL>
|
||||
<Icon>https://raw.githubusercontent.com/FluffyRanger/PangolinSentinel/main/icon.png</Icon>
|
||||
<ExtraParams/>
|
||||
<PostArgs/>
|
||||
<CPUset/>
|
||||
|
||||
Reference in new issue
Block a user