mirror of
https://github.com/PegaProx/project-pegaprox.git
synced 2026-08-12 15:27:47 +08:00
The in-app updater and update.sh are a git-tree + pip file update that only fits
the source/deploy.sh layout. On an apt/dpkg install the correct path is apt
upgrade (deps are system python3-* packages; a pip run diverges from dpkg and
can't lift the dpkg-owned crypto libs -> fail-closed TLS on restart); on Docker
a file update is discarded at the next image pull.
- settings.py: _detect_install_method() (docker via /.dockerenv + cgroup, apt via
dpkg -S, else source); perform_pegaprox_update refuses on apt/docker with the
right guidance + copy-paste command (409, allow_managed override); check-update
reports install_method / in_app_update_supported / managed_update_{hint,command}.
- update.sh: same detection + guard before touching anything (--force override).
- settings_modal.js: the Install button becomes a package-manager / image hint
with a copyable command on apt/docker; performUpdate handles the 409.
- tests: 5 guard/reporting tests.
518 lines
21 KiB
Bash
Executable File
518 lines
21 KiB
Bash
Executable File
#!/bin/bash
|
|
# ============================================================================
|
|
# PegaProx Update Script (Archive-based)
|
|
# ============================================================================
|
|
#
|
|
# Downloads the latest release as a tar.gz archive from GitHub.
|
|
# This scales to any number of files without needing to list them individually.
|
|
#
|
|
# Usage:
|
|
# ./update.sh # Normal update
|
|
# ./update.sh --force # Force update (skip version check)
|
|
#
|
|
# Note: Safe to run as root - automatically preserves original file ownership
|
|
#
|
|
# NS: Rewritten feb 2026 for archive-based updates (code split support)
|
|
# ============================================================================
|
|
|
|
set -e
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m'
|
|
|
|
# MK May 2026 (#417 follow-up, elektronen): allow updating from a specific
|
|
# branch via `PEGAPROX_BRANCH=Testing sudo ./update.sh`. Default still main.
|
|
GITHUB_BRANCH="${PEGAPROX_BRANCH:-main}"
|
|
|
|
# GitHub URLs
|
|
GITHUB_RAW="https://raw.githubusercontent.com/PegaProx/project-pegaprox/${GITHUB_BRANCH}"
|
|
# NS: auto-generated by GitHub, no manual release needed
|
|
GITHUB_ARCHIVE="https://github.com/PegaProx/project-pegaprox/archive/refs/heads/${GITHUB_BRANCH}.tar.gz"
|
|
|
|
# Mirror (fallback if GitHub is down) — note: mirror only serves main, so
|
|
# falling back to mirror when PEGAPROX_BRANCH≠main will land you on main content.
|
|
# Acceptable trade-off since mirror is the offline-network fallback path.
|
|
MIRROR_URL="https://updates.pegaprox.com"
|
|
MIRROR_ARCHIVE="https://updates.pegaprox.com/archive/main.tar.gz"
|
|
|
|
# Find script directory (where PegaProx is installed)
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
cd "$SCRIPT_DIR"
|
|
|
|
# Figure out who owns this installation
|
|
ORIGINAL_OWNER=""
|
|
if [ -d "config" ]; then
|
|
ORIGINAL_OWNER=$(stat -c '%U:%G' config 2>/dev/null || stat -f '%Su:%Sg' config 2>/dev/null)
|
|
elif [ -f "cert.pem" ]; then
|
|
ORIGINAL_OWNER=$(stat -c '%U:%G' cert.pem 2>/dev/null || stat -f '%Su:%Sg' cert.pem 2>/dev/null)
|
|
elif [ -d "ssl" ]; then
|
|
ORIGINAL_OWNER=$(stat -c '%U:%G' ssl 2>/dev/null || stat -f '%Su:%Sg' ssl 2>/dev/null)
|
|
fi
|
|
|
|
echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${BLUE}║ PegaProx Update Script ║${NC}"
|
|
echo -e "${BLUE}╚════════════════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
|
|
# Check if running as root
|
|
if [ "$EUID" -eq 0 ]; then
|
|
echo -e "${BLUE}Running as root${NC}"
|
|
if [ -n "$ORIGINAL_OWNER" ]; then
|
|
echo -e " Will restore ownership to: ${GREEN}$ORIGINAL_OWNER${NC}"
|
|
fi
|
|
echo ""
|
|
else
|
|
echo -e "${YELLOW}Tip: sudo ./update.sh for auto service restart${NC}"
|
|
echo ""
|
|
fi
|
|
|
|
# NS 2026-08-11 — install-method guard. This script does a git-tree download + pip and only
|
|
# fits a source/deploy.sh layout. On an apt/dpkg-managed install the right update is
|
|
# `apt upgrade` (a git+pip run diverges from dpkg and can't lift the dpkg-owned crypto libs, so
|
|
# the service fail-closes on TLS at restart). Inside a container a file update is discarded on
|
|
# the next image pull. Detect + refuse unless --force is passed.
|
|
_pgx_install_method() {
|
|
if [ -f /.dockerenv ] || grep -qE 'docker|containerd|kubepods' /proc/1/cgroup 2>/dev/null; then
|
|
echo docker; return
|
|
fi
|
|
if command -v dpkg >/dev/null 2>&1 \
|
|
&& dpkg -S "$SCRIPT_DIR/pegaprox_multi_cluster.py" 2>/dev/null | grep -q '^pegaprox:'; then
|
|
echo apt; return
|
|
fi
|
|
echo source
|
|
}
|
|
|
|
_PGX_FORCE=0
|
|
for _a in "$@"; do
|
|
case "$_a" in --force|--allow-managed) _PGX_FORCE=1 ;; esac
|
|
done
|
|
_PGX_METHOD="$(_pgx_install_method)"
|
|
if [ "$_PGX_METHOD" != "source" ] && [ "$_PGX_FORCE" -eq 0 ]; then
|
|
echo -e "${YELLOW}This PegaProx instance was installed via: ${_PGX_METHOD}.${NC}"
|
|
if [ "$_PGX_METHOD" = "apt" ]; then
|
|
echo -e "${YELLOW}Update it through the package manager, not this script:${NC}"
|
|
echo " sudo apt update && sudo apt upgrade pegaprox"
|
|
echo -e "${YELLOW}(A git+pip update would diverge from dpkg and can break dependency handling.)${NC}"
|
|
else
|
|
echo -e "${YELLOW}Update it by pulling a fresh image and recreating the container:${NC}"
|
|
echo " docker pull ghcr.io/pegaprox/pegaprox:latest # then recreate: compose up -d / docker run"
|
|
echo -e "${YELLOW}(An in-place update here is discarded on the next image pull.)${NC}"
|
|
fi
|
|
echo ""
|
|
echo -e " Override at your own risk with: ${BLUE}./update.sh --force${NC}"
|
|
exit 0
|
|
fi
|
|
|
|
# Check current version
|
|
CURRENT_VERSION="unknown"
|
|
if [ -f "version.json" ]; then
|
|
CURRENT_VERSION=$(grep -o '"version": *"[^"]*"' version.json | cut -d'"' -f4)
|
|
fi
|
|
echo -e "Current version: ${BLUE}$CURRENT_VERSION${NC}"
|
|
|
|
# Get latest version (try GitHub first, then mirror)
|
|
echo -n "Checking for updates... "
|
|
LATEST_VERSION=$(curl -s "$GITHUB_RAW/version.json" 2>/dev/null | grep -o '"version": *"[^"]*"' | cut -d'"' -f4)
|
|
|
|
if [ -z "$LATEST_VERSION" ]; then
|
|
# GitHub down? try mirror
|
|
LATEST_VERSION=$(curl -s "$MIRROR_URL/version.json" 2>/dev/null | grep -o '"version": *"[^"]*"' | cut -d'"' -f4)
|
|
fi
|
|
|
|
if [ -z "$LATEST_VERSION" ]; then
|
|
echo -e "${RED}Failed${NC}"
|
|
echo "Could not reach GitHub or mirror. Check your internet connection."
|
|
exit 1
|
|
fi
|
|
|
|
echo -e "${GREEN}OK${NC}"
|
|
echo -e "Latest version: ${GREEN}$LATEST_VERSION${NC}"
|
|
echo ""
|
|
|
|
# MK 2026-06-07: never skip on version-equality. A prior interrupted/partial
|
|
# update can leave version.json bumped while some code files stayed stale — and
|
|
# the old "already on latest → exit" path then meant `./update.sh` could NEVER
|
|
# heal it (you had to know about --force). We now ALWAYS download the archive and
|
|
# re-apply the FULL tree, so every run guarantees every file is actually in sync.
|
|
RESYNC=0
|
|
if [ "$CURRENT_VERSION" == "$LATEST_VERSION" ]; then
|
|
RESYNC=1
|
|
echo -e "${GREEN}✓ Already on $LATEST_VERSION${NC} — re-syncing all files anyway so nothing can be left stale."
|
|
echo ""
|
|
fi
|
|
|
|
# Confirm only for an actual version change. A same-version re-sync just proceeds
|
|
# (you explicitly ran the updater and re-applying the full tree is idempotent).
|
|
if [ "$RESYNC" -eq 0 ]; then
|
|
echo -e "${YELLOW}Ready to update from $CURRENT_VERSION to $LATEST_VERSION${NC}"
|
|
echo ""
|
|
read -p "Continue? [y/N] " -n 1 -r
|
|
echo ""
|
|
|
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
echo "Update cancelled."
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
echo -e "${YELLOW}Updating...${NC}"
|
|
|
|
# Create backup
|
|
BACKUP_DIR="${SCRIPT_DIR}/backups/backup_${CURRENT_VERSION}_$(date +%Y%m%d_%H%M%S)"
|
|
echo -n "Creating backup in $BACKUP_DIR... "
|
|
mkdir -p "$BACKUP_DIR"
|
|
|
|
# Backup important files (not config - that stays)
|
|
[ -f "pegaprox_multi_cluster.py" ] && cp pegaprox_multi_cluster.py "$BACKUP_DIR/"
|
|
[ -d "pegaprox" ] && cp -r pegaprox "$BACKUP_DIR/"
|
|
[ -f "web/index.html" ] && mkdir -p "$BACKUP_DIR/web" && cp web/index.html "$BACKUP_DIR/web/"
|
|
[ -f "web/index.html.original" ] && cp web/index.html.original "$BACKUP_DIR/web/"
|
|
[ -f "version.json" ] && cp version.json "$BACKUP_DIR/"
|
|
[ -f "requirements.txt" ] && cp requirements.txt "$BACKUP_DIR/"
|
|
|
|
echo -e "${GREEN}OK${NC}"
|
|
|
|
# Download release archive
|
|
echo ""
|
|
echo -n "Downloading release archive... "
|
|
TMPDIR=$(mktemp -d)
|
|
ARCHIVE="$TMPDIR/pegaprox.tar.gz"
|
|
|
|
if curl -sfL "$GITHUB_ARCHIVE" -o "$ARCHIVE" 2>/dev/null; then
|
|
echo -e "${GREEN}OK (GitHub)${NC}"
|
|
elif curl -sfL "$MIRROR_ARCHIVE" -o "$ARCHIVE" 2>/dev/null; then
|
|
echo -e "${GREEN}OK (Mirror)${NC}"
|
|
else
|
|
echo -e "${YELLOW}Archive not found, falling back to individual files...${NC}"
|
|
# Fallback: download individual files (for repos without releases)
|
|
# NS: try GitHub first, fall back to mirror
|
|
download_file() {
|
|
local file=$1
|
|
# never overwrite user data / secrets, even if they show up in the tree
|
|
case "$file" in
|
|
config/*|ssl/*|logs/*|backups/*|.git/*|*.db|*.pem|*.key|*.crt|*.enc) return 0 ;;
|
|
esac
|
|
local dir=$(dirname "$file")
|
|
[ "$dir" != "." ] && mkdir -p "$dir"
|
|
echo -n " $file... "
|
|
if curl -sfL "$GITHUB_RAW/$file" -o "$file.tmp" 2>/dev/null; then
|
|
mv "$file.tmp" "$file"
|
|
echo -e "${GREEN}OK (GitHub)${NC}"
|
|
return 0
|
|
elif curl -sfL "$MIRROR_URL/$file" -o "$file.tmp" 2>/dev/null; then
|
|
mv "$file.tmp" "$file"
|
|
echo -e "${GREEN}OK (mirror)${NC}"
|
|
return 0
|
|
else
|
|
rm -f "$file.tmp"
|
|
echo -e "${RED}FAILED${NC}"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# MK 2026-06-07: fetch the FULL repo tree (GitHub Trees API) so the fallback
|
|
# misses nothing — same completeness as the archive path. version.json's
|
|
# hand-maintained update_files list (no globs) used to drop any file not on
|
|
# it (e.g. a freshly-added sponsor logo). update_files is now only the
|
|
# degraded-degraded path when the Trees API itself is unreachable.
|
|
echo "Fetching file list (full tree)..."
|
|
PACKAGE_FILES=$(curl -s "https://api.github.com/repos/PegaProx/project-pegaprox/git/trees/${GITHUB_BRANCH}?recursive=1" 2>/dev/null | python3 -c "
|
|
import sys, json
|
|
try:
|
|
data = json.load(sys.stdin)
|
|
for it in data.get('tree', []):
|
|
if it.get('type') == 'blob':
|
|
print(it['path'])
|
|
except:
|
|
pass
|
|
" 2>/dev/null)
|
|
|
|
if [ -z "$PACKAGE_FILES" ]; then
|
|
# Trees API unreachable → fall back to version.json's update_files list
|
|
PACKAGE_FILES=$(curl -s "$GITHUB_RAW/version.json" 2>/dev/null | python3 -c "
|
|
import sys, json
|
|
try:
|
|
data = json.load(sys.stdin)
|
|
for f in data.get('update_files', []):
|
|
print(f)
|
|
except:
|
|
pass
|
|
" 2>/dev/null)
|
|
fi
|
|
|
|
if [ -z "$PACKAGE_FILES" ]; then
|
|
# Mirror fallback
|
|
PACKAGE_FILES=$(curl -s "$MIRROR_URL/version.json" 2>/dev/null | python3 -c "
|
|
import sys, json
|
|
try:
|
|
data = json.load(sys.stdin)
|
|
for f in data.get('update_files', []):
|
|
print(f)
|
|
except:
|
|
pass
|
|
" 2>/dev/null)
|
|
fi
|
|
|
|
# NS: track fallback-download failures so a partial/mixed update aborts and
|
|
# restores from the backup instead of silently leaving a half-written tree
|
|
# (#168, thanks @x86txt). Only the per-file fallback path — the rsync/tar
|
|
# archive path stays as-is (no --delete; it would wipe offline fonts + plugins).
|
|
DOWNLOAD_FAILURES=0
|
|
|
|
if [ -n "$PACKAGE_FILES" ]; then
|
|
echo "Downloading file list from manifest..."
|
|
while IFS= read -r pfile; do
|
|
[ -z "$pfile" ] && continue
|
|
if ! download_file "$pfile"; then
|
|
DOWNLOAD_FAILURES=$((DOWNLOAD_FAILURES + 1))
|
|
fi
|
|
done <<< "$PACKAGE_FILES"
|
|
else
|
|
# absolute fallback - at least get the essentials
|
|
echo "No file list found, downloading essentials..."
|
|
for _ess in pegaprox_multi_cluster.py version.json requirements.txt deploy.sh update.sh web/index.html web/index.html.original; do
|
|
if ! download_file "$_ess"; then
|
|
DOWNLOAD_FAILURES=$((DOWNLOAD_FAILURES + 1))
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [ "$DOWNLOAD_FAILURES" -gt 0 ]; then
|
|
echo -e "${RED}Update aborted: $DOWNLOAD_FAILURES file(s) failed to download.${NC}"
|
|
echo "Restoring from backup..."
|
|
[ -f "$BACKUP_DIR/pegaprox_multi_cluster.py" ] && cp "$BACKUP_DIR/pegaprox_multi_cluster.py" . 2>/dev/null || true
|
|
[ -d "$BACKUP_DIR/pegaprox" ] && { rm -rf pegaprox && cp -r "$BACKUP_DIR/pegaprox" . 2>/dev/null; } || true
|
|
[ -d "$BACKUP_DIR/web" ] && { mkdir -p web && cp "$BACKUP_DIR/web/"* web/ 2>/dev/null; } || true
|
|
[ -f "$BACKUP_DIR/version.json" ] && cp "$BACKUP_DIR/version.json" . 2>/dev/null || true
|
|
[ -f "$BACKUP_DIR/requirements.txt" ] && cp "$BACKUP_DIR/requirements.txt" . 2>/dev/null || true
|
|
rm -rf "$TMPDIR"
|
|
exit 1
|
|
fi
|
|
|
|
rm -rf "$TMPDIR"
|
|
|
|
# Skip to pip install
|
|
ARCHIVE=""
|
|
fi
|
|
|
|
# NS Feb 2026 - verify archive integrity via SHA256 checksum
|
|
if [ -n "$ARCHIVE" ] && [ -f "$ARCHIVE" ]; then
|
|
echo -n "Verifying archive integrity... "
|
|
SHA_FILE="$TMPDIR/SHA256SUMS"
|
|
SHA_VERIFIED=false
|
|
if curl -sfL "$GITHUB_RAW/SHA256SUMS" -o "$SHA_FILE" 2>/dev/null || \
|
|
curl -sfL "$MIRROR_URL/SHA256SUMS" -o "$SHA_FILE" 2>/dev/null; then
|
|
# SHA256SUMS contains lines like: <hash> <filename>
|
|
EXPECTED=$(grep -E "${GITHUB_BRANCH}\\.tar\\.gz\$" "$SHA_FILE" 2>/dev/null | awk '{print $1}')
|
|
if [ -n "$EXPECTED" ]; then
|
|
ACTUAL=$(sha256sum "$ARCHIVE" | awk '{print $1}')
|
|
if [ "$EXPECTED" = "$ACTUAL" ]; then
|
|
echo -e "${GREEN}OK (SHA256 verified)${NC}"
|
|
SHA_VERIFIED=true
|
|
else
|
|
echo -e "${RED}CHECKSUM MISMATCH${NC}"
|
|
echo -e "${RED}Expected: $EXPECTED${NC}"
|
|
echo -e "${RED}Got: $ACTUAL${NC}"
|
|
echo -e "${RED}Archive may be corrupted or tampered with. Aborting.${NC}"
|
|
rm -rf "$TMPDIR"
|
|
exit 1
|
|
fi
|
|
else
|
|
echo -e "${YELLOW}no matching entry in SHA256SUMS${NC}"
|
|
fi
|
|
else
|
|
echo -e "${YELLOW}SHA256SUMS not available (skipping verification)${NC}"
|
|
fi
|
|
fi
|
|
|
|
# Extract archive if we got one
|
|
if [ -n "$ARCHIVE" ] && [ -f "$ARCHIVE" ]; then
|
|
echo -n "Extracting archive... "
|
|
# Extract to temp dir first, then copy (safer)
|
|
EXTRACT_DIR="$TMPDIR/extracted"
|
|
mkdir -p "$EXTRACT_DIR"
|
|
tar xzf "$ARCHIVE" -C "$EXTRACT_DIR" 2>/dev/null
|
|
|
|
# Find the actual content (might be in a subdirectory)
|
|
CONTENT_DIR="$EXTRACT_DIR"
|
|
if [ ! -f "$CONTENT_DIR/pegaprox_multi_cluster.py" ]; then
|
|
# Check one level down (GitHub archives often have a subdirectory)
|
|
for subdir in "$EXTRACT_DIR"/*/; do
|
|
if [ -f "${subdir}pegaprox_multi_cluster.py" ]; then
|
|
CONTENT_DIR="$subdir"
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [ -f "$CONTENT_DIR/pegaprox_multi_cluster.py" ]; then
|
|
# Copy files, preserving directory structure
|
|
# Skip: config/, ssl/, logs/, backups/, cert.pem, key.pem, .git/
|
|
if command -v rsync &> /dev/null; then
|
|
rsync -a --exclude='config/' --exclude='ssl/' --exclude='logs/' \
|
|
--exclude='backups/' --exclude='cert.pem' --exclude='key.pem' \
|
|
--exclude='.git/' --exclude='.gitignore' \
|
|
"$CONTENT_DIR/" "$SCRIPT_DIR/"
|
|
else
|
|
# Fallback: cp + tar (works without rsync)
|
|
cd "$CONTENT_DIR"
|
|
tar cf - --exclude='config' --exclude='ssl' --exclude='logs' \
|
|
--exclude='backups' --exclude='cert.pem' --exclude='key.pem' \
|
|
--exclude='.git' --exclude='.gitignore' \
|
|
. | tar xf - -C "$SCRIPT_DIR"
|
|
cd "$SCRIPT_DIR"
|
|
fi
|
|
echo -e "${GREEN}OK${NC}"
|
|
else
|
|
echo -e "${RED}FAILED${NC}"
|
|
echo "Archive does not contain pegaprox_multi_cluster.py"
|
|
echo "Restoring from backup..."
|
|
cp "$BACKUP_DIR/pegaprox_multi_cluster.py" . 2>/dev/null || true
|
|
rm -rf "$TMPDIR"
|
|
exit 1
|
|
fi
|
|
|
|
rm -rf "$TMPDIR"
|
|
fi
|
|
|
|
# MK 2026-06-07: post-copy sanity check — confirm the new version.json actually
|
|
# landed on disk. Catches a half-applied copy AND a stale CDN tarball (GitHub can
|
|
# serve an old cached <branch>.tar.gz as a 200 right after a push).
|
|
APPLIED=$(grep -o '"version": *"[^"]*"' version.json 2>/dev/null | cut -d'"' -f4)
|
|
if [ -n "$LATEST_VERSION" ] && [ "$APPLIED" != "$LATEST_VERSION" ]; then
|
|
echo -e "${YELLOW}⚠ Post-update check: version.json says '$APPLIED' but expected '$LATEST_VERSION'.${NC}"
|
|
echo -e "${YELLOW} The download may be incomplete or a stale cache — re-run ./update.sh --force in a minute.${NC}"
|
|
fi
|
|
|
|
# Make scripts executable
|
|
chmod +x deploy.sh update.sh 2>/dev/null || true
|
|
chmod +x web/Dev/build.sh 2>/dev/null || true
|
|
|
|
# Fix ownership if running as root
|
|
if [ "$EUID" -eq 0 ] && [ -n "$ORIGINAL_OWNER" ] && [ "$ORIGINAL_OWNER" != "root:root" ]; then
|
|
echo -n "Fixing file ownership ($ORIGINAL_OWNER)... "
|
|
chown -R "$ORIGINAL_OWNER" pegaprox_multi_cluster.py version.json requirements.txt 2>/dev/null
|
|
chown -R "$ORIGINAL_OWNER" deploy.sh update.sh 2>/dev/null
|
|
chown -R "$ORIGINAL_OWNER" web/ 2>/dev/null
|
|
[ -d "pegaprox" ] && chown -R "$ORIGINAL_OWNER" pegaprox/ 2>/dev/null
|
|
chown -R "$ORIGINAL_OWNER" backups/ 2>/dev/null
|
|
# images/ was missing here - left root:root on a non-root install (#633)
|
|
[ -d "images" ] && chown -R "$ORIGINAL_OWNER" images/ 2>/dev/null
|
|
# config/ too: we chmod 700 it further down, so a single root-owned file in
|
|
# there (a root-run import can create config/ssl/cert.pem) locks the service
|
|
# user out of its own certs. ORIGINAL_OWNER is read from config/ itself, so
|
|
# this only ever repairs children (#633).
|
|
[ -d "config" ] && chown -R "$ORIGINAL_OWNER" config/ 2>/dev/null
|
|
echo -e "${GREEN}OK${NC}"
|
|
fi
|
|
|
|
# Restore restrictive permissions on config and ssl directories.
|
|
# These must be 0700 so that only the service user can read the encrypted
|
|
# database and SSL private keys. An update that runs as root via sudo can
|
|
# inadvertently leave them world-readable if umask is permissive.
|
|
if [ -d "config" ]; then
|
|
chmod 700 config 2>/dev/null || true
|
|
fi
|
|
if [ -d "config/ssl" ]; then
|
|
chmod 700 config/ssl 2>/dev/null || true
|
|
elif [ -d "ssl" ]; then
|
|
chmod 700 ssl 2>/dev/null || true
|
|
fi
|
|
|
|
# Install/update Python packages
|
|
echo ""
|
|
echo -n "Installing Python packages... "
|
|
|
|
PIP_SUCCESS=false
|
|
|
|
if [ -f "venv/bin/python" ] && [ "$PIP_SUCCESS" = false ]; then
|
|
./venv/bin/python -m pip install -q -r requirements.txt 2>/dev/null && PIP_SUCCESS=true
|
|
fi
|
|
|
|
if [ -f "venv/bin/pip" ] && [ "$PIP_SUCCESS" = false ]; then
|
|
./venv/bin/pip install -q -r requirements.txt 2>/dev/null && PIP_SUCCESS=true
|
|
fi
|
|
|
|
if [ "$EUID" -eq 0 ] && command -v pip3 &> /dev/null && [ "$PIP_SUCCESS" = false ]; then
|
|
pip3 install -q -r requirements.txt 2>/dev/null && PIP_SUCCESS=true
|
|
fi
|
|
|
|
if command -v pip3 &> /dev/null && [ "$PIP_SUCCESS" = false ]; then
|
|
pip3 install -q --user -r requirements.txt 2>/dev/null && PIP_SUCCESS=true
|
|
fi
|
|
|
|
if command -v python3 &> /dev/null && [ "$PIP_SUCCESS" = false ]; then
|
|
python3 -m pip install -q --user -r requirements.txt 2>/dev/null && PIP_SUCCESS=true
|
|
fi
|
|
|
|
if [ "$PIP_SUCCESS" = true ]; then
|
|
echo -e "${GREEN}OK${NC}"
|
|
else
|
|
echo -e "${YELLOW}Couldn't install - run: pip install -r requirements.txt${NC}"
|
|
fi
|
|
|
|
# MK 2026-08-11 — preflight the crypto/TLS stack BEFORE bouncing the service. Startup is
|
|
# fail-closed (#633): if the dependency step above didn't land a loadable cryptography/
|
|
# pyOpenSSL pair (offline host, a source build that timed out, or a venv that kept the old
|
|
# version), a restart takes the service down and it won't come back. Verify a self-signed
|
|
# cert can actually be generated first; if not, leave the running service untouched.
|
|
PY_BIN="python3"
|
|
[ -x "venv/bin/python" ] && PY_BIN="venv/bin/python"
|
|
CRYPTO_OK=true
|
|
"$PY_BIN" - <<'PYEOF' >/dev/null 2>&1 || CRYPTO_OK=false
|
|
from OpenSSL import crypto
|
|
k = crypto.PKey(); k.generate_key(crypto.TYPE_RSA, 2048)
|
|
c = crypto.X509(); c.set_pubkey(k); c.sign(k, 'sha256')
|
|
PYEOF
|
|
|
|
if [ "$CRYPTO_OK" = false ]; then
|
|
echo ""
|
|
echo -e "${YELLOW}⚠ Dependency/crypto preflight FAILED — NOT restarting the service.${NC}"
|
|
echo -e "${YELLOW} The new version needs an updated cryptography/pyOpenSSL that isn't installed yet.${NC}"
|
|
echo -e "${YELLOW} The service is still running the previous version. To finish the upgrade:${NC}"
|
|
echo " 1) $PY_BIN -m pip install -r requirements.txt (needs PyPI access; watch for build errors)"
|
|
echo " 2) sudo systemctl restart pegaprox"
|
|
echo ""
|
|
echo -e " Files are on disk at version ${GREEN}$LATEST_VERSION${NC}; it goes live once deps install and the service restarts."
|
|
exit 0
|
|
fi
|
|
|
|
# Restart service
|
|
echo ""
|
|
echo -n "Restarting PegaProx service... "
|
|
|
|
if systemctl is-active --quiet pegaprox 2>/dev/null; then
|
|
if systemctl restart pegaprox 2>/dev/null; then
|
|
echo -e "${GREEN}OK${NC}"
|
|
else
|
|
echo -e "${YELLOW}Failed - restart manually${NC}"
|
|
fi
|
|
elif systemctl is-active --quiet pegaprox.service 2>/dev/null; then
|
|
if systemctl restart pegaprox.service 2>/dev/null; then
|
|
echo -e "${GREEN}OK${NC}"
|
|
else
|
|
echo -e "${YELLOW}Failed - restart manually${NC}"
|
|
fi
|
|
else
|
|
echo -e "${YELLOW}No systemd service found${NC}"
|
|
echo " If running manually, restart with: python3 pegaprox_multi_cluster.py"
|
|
fi
|
|
|
|
# Done!
|
|
echo ""
|
|
echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${GREEN}║ Update Complete! ✓ ║${NC}"
|
|
echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
echo -e " Updated to version: ${GREEN}$LATEST_VERSION${NC}"
|
|
echo -e " Backup saved to: ${BLUE}$BACKUP_DIR${NC}"
|
|
echo ""
|
|
echo "If something went wrong, restore with:"
|
|
echo " cp -r $BACKUP_DIR/* ."
|
|
echo ""
|