2026-02-12 15:46:57 -08:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# Automated NPMplus Backup Script
|
|
|
|
|
# Backs up database, proxy hosts, certificates, and configuration.
|
|
|
|
|
# Usage: bash scripts/verify/backup-npmplus.sh [--dry-run]
|
|
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
|
|
|
|
|
|
|
|
# Colors
|
|
|
|
|
RED='\033[0;31m'
|
|
|
|
|
GREEN='\033[0;32m'
|
|
|
|
|
YELLOW='\033[1;33m'
|
|
|
|
|
BLUE='\033[0;34m'
|
|
|
|
|
NC='\033[0m'
|
|
|
|
|
|
|
|
|
|
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
|
|
|
|
log_success() { echo -e "${GREEN}[✓]${NC} $1"; }
|
|
|
|
|
log_warn() { echo -e "${YELLOW}[⚠]${NC} $1"; }
|
|
|
|
|
log_error() { echo -e "${RED}[✗]${NC} $1"; }
|
|
|
|
|
|
|
|
|
|
cd "$PROJECT_ROOT"
|
|
|
|
|
|
2026-03-04 02:03:08 -08:00
|
|
|
# Source dotenv (operator creds): repo root .env then smom-dbis-138/.env
|
2026-02-12 15:46:57 -08:00
|
|
|
if [ -f .env ]; then
|
|
|
|
|
set +euo pipefail
|
|
|
|
|
source .env 2>/dev/null || true
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
fi
|
2026-03-04 02:03:08 -08:00
|
|
|
if [ -f smom-dbis-138/.env ]; then
|
|
|
|
|
set +euo pipefail
|
|
|
|
|
source smom-dbis-138/.env 2>/dev/null || true
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
fi
|
2026-02-12 15:46:57 -08:00
|
|
|
|
|
|
|
|
# Load ip-addresses.conf for fallbacks (before cd)
|
|
|
|
|
[ -f "${PROJECT_ROOT}/config/ip-addresses.conf" ] && source "${PROJECT_ROOT}/config/ip-addresses.conf" 2>/dev/null || true
|
|
|
|
|
# Configuration (from .env; NPMPLUS_* fall back to NPM_* / PROXMOX_HOST per .env.example)
|
|
|
|
|
NPMPLUS_VMID="${NPMPLUS_VMID:-${NPM_VMID:-10233}}"
|
|
|
|
|
NPMPLUS_HOST="${NPMPLUS_HOST:-${NPM_PROXMOX_HOST:-${PROXMOX_HOST:-${PROXMOX_HOST_R630_01:-192.168.11.11}}}}"
|
|
|
|
|
NPM_URL="${NPM_URL:-https://${IP_NPMPLUS:-${IP_NPMPLUS:-192.168.11.167}}:81}"
|
|
|
|
|
NPM_EMAIL="${NPM_EMAIL:-nsatoshi2007@hotmail.com}"
|
|
|
|
|
NPM_PASSWORD="${NPM_PASSWORD:-}"
|
|
|
|
|
|
|
|
|
|
DRY_RUN=false
|
|
|
|
|
[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=true
|
|
|
|
|
|
|
|
|
|
# Backup destination
|
|
|
|
|
BACKUP_BASE_DIR="${BACKUP_DIR:-$PROJECT_ROOT/backups/npmplus}"
|
|
|
|
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
|
|
|
|
BACKUP_DIR="$BACKUP_BASE_DIR/backup-$TIMESTAMP"
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
|
|
|
echo "💾 NPMplus Backup Script"
|
|
|
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
|
|
|
echo ""
|
|
|
|
|
|
|
|
|
|
# Validate NPM password (skip for dry-run)
|
|
|
|
|
if [ -z "$NPM_PASSWORD" ] && [[ "$DRY_RUN" != true ]]; then
|
|
|
|
|
log_error "NPM_PASSWORD environment variable is required"
|
|
|
|
|
log_info "Set it in .env file or export it before running this script"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
|
|
|
log_info "DRY-RUN: would backup NPMplus (database, API exports, certs) to $BACKUP_DIR"
|
|
|
|
|
log_info "Run without --dry-run to perform backup."
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
mkdir -p "$BACKUP_DIR"
|
|
|
|
|
log_info "Backup destination: $BACKUP_DIR"
|
|
|
|
|
echo ""
|
|
|
|
|
|
|
|
|
|
# Step 1: Backup SQLite Database
|
|
|
|
|
log_info "Step 1: Backing up NPMplus database..."
|
|
|
|
|
DB_BACKUP_DIR="$BACKUP_DIR/database"
|
|
|
|
|
mkdir -p "$DB_BACKUP_DIR"
|
|
|
|
|
|
|
|
|
|
# Method 1: SQL dump
|
|
|
|
|
log_info " Creating SQL dump..."
|
|
|
|
|
ssh root@"$NPMPLUS_HOST" "pct exec $NPMPLUS_VMID -- bash -c '
|
|
|
|
|
if [ -f /data/database.sqlite ]; then
|
|
|
|
|
sqlite3 /data/database.sqlite \".dump\" > /tmp/npm-database.sql 2>/dev/null || echo \"Database export may have issues\"
|
|
|
|
|
cat /tmp/npm-database.sql
|
|
|
|
|
else
|
|
|
|
|
echo \"Database file not found\"
|
|
|
|
|
fi
|
|
|
|
|
'" > "$DB_BACKUP_DIR/database.sql" || {
|
|
|
|
|
log_warn " SQL dump failed, trying direct copy..."
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Method 2: Direct file copy
|
|
|
|
|
log_info " Copying database file..."
|
|
|
|
|
ssh root@"$NPMPLUS_HOST" "pct exec $NPMPLUS_VMID -- cat /data/database.sqlite" > "$DB_BACKUP_DIR/database.sqlite" 2>/dev/null || {
|
|
|
|
|
log_warn " Direct copy failed - database may not exist or container may be down"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if [ -s "$DB_BACKUP_DIR/database.sql" ] || [ -s "$DB_BACKUP_DIR/database.sqlite" ]; then
|
|
|
|
|
log_success " Database backup completed"
|
|
|
|
|
else
|
|
|
|
|
log_warn " Database backup may be empty - check container status"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Step 2: Export Proxy Hosts via API
|
|
|
|
|
log_info "Step 2: Exporting proxy hosts configuration..."
|
|
|
|
|
API_BACKUP_DIR="$BACKUP_DIR/api"
|
|
|
|
|
mkdir -p "$API_BACKUP_DIR"
|
|
|
|
|
|
|
|
|
|
# Authenticate
|
|
|
|
|
log_info " Authenticating to NPMplus API..."
|
|
|
|
|
TOKEN_RESPONSE=$(curl -s -k -X POST "$NPM_URL/api/tokens" \
|
|
|
|
|
-H "Content-Type: application/json" \
|
|
|
|
|
-d "{\"identity\":\"$NPM_EMAIL\",\"secret\":\"$NPM_PASSWORD\"}")
|
|
|
|
|
|
|
|
|
|
TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.token // empty' 2>/dev/null || echo "")
|
|
|
|
|
|
|
|
|
|
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
|
|
|
|
|
log_error " Failed to authenticate to NPMplus API"
|
|
|
|
|
log_warn " Skipping API-based exports"
|
|
|
|
|
else
|
|
|
|
|
log_success " Authenticated successfully"
|
|
|
|
|
|
|
|
|
|
# Export proxy hosts
|
|
|
|
|
log_info " Exporting proxy hosts..."
|
|
|
|
|
curl -s -k -X GET "$NPM_URL/api/nginx/proxy-hosts" \
|
|
|
|
|
-H "Authorization: Bearer $TOKEN" | jq '.' > "$API_BACKUP_DIR/proxy_hosts.json" || {
|
|
|
|
|
log_warn " Failed to export proxy hosts"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Export certificates
|
|
|
|
|
log_info " Exporting certificates..."
|
|
|
|
|
curl -s -k -X GET "$NPM_URL/api/nginx/certificates" \
|
|
|
|
|
-H "Authorization: Bearer $TOKEN" | jq '.' > "$API_BACKUP_DIR/certificates.json" || {
|
|
|
|
|
log_warn " Failed to export certificates"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Export access lists
|
|
|
|
|
log_info " Exporting access lists..."
|
|
|
|
|
curl -s -k -X GET "$NPM_URL/api/nginx/access-lists" \
|
|
|
|
|
-H "Authorization: Bearer $TOKEN" | jq '.' > "$API_BACKUP_DIR/access_lists.json" 2>/dev/null || {
|
|
|
|
|
log_warn " Failed to export access lists (may not be supported)"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log_success " API exports completed"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Step 3: Backup Certificate Files
|
|
|
|
|
log_info "Step 3: Backing up certificate files..."
|
|
|
|
|
CERT_BACKUP_DIR="$BACKUP_DIR/certificates"
|
|
|
|
|
mkdir -p "$CERT_BACKUP_DIR"
|
|
|
|
|
|
|
|
|
|
# List all certificates
|
|
|
|
|
log_info " Listing certificates..."
|
|
|
|
|
ssh root@"$NPMPLUS_HOST" "pct exec $NPMPLUS_VMID -- ls -1 /data/tls/certbot/live/ 2>/dev/null" > "$CERT_BACKUP_DIR/cert_list.txt" 2>/dev/null || {
|
|
|
|
|
log_warn " Could not list certificates - path may differ"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Copy certificate files
|
|
|
|
|
if [ -s "$CERT_BACKUP_DIR/cert_list.txt" ]; then
|
|
|
|
|
log_info " Copying certificate files..."
|
|
|
|
|
while IFS= read -r cert_dir; do
|
|
|
|
|
if [ -n "$cert_dir" ] && [ "$cert_dir" != "lost+found" ]; then
|
|
|
|
|
mkdir -p "$CERT_BACKUP_DIR/$cert_dir"
|
|
|
|
|
|
|
|
|
|
# Copy fullchain.pem
|
|
|
|
|
ssh root@"$NPMPLUS_HOST" "pct exec $NPMPLUS_VMID -- cat /data/tls/certbot/live/$cert_dir/fullchain.pem" > "$CERT_BACKUP_DIR/$cert_dir/fullchain.pem" 2>/dev/null || {
|
|
|
|
|
log_warn " Failed to copy fullchain.pem for $cert_dir"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Copy privkey.pem
|
|
|
|
|
ssh root@"$NPMPLUS_HOST" "pct exec $NPMPLUS_VMID -- cat /data/tls/certbot/live/$cert_dir/privkey.pem" > "$CERT_BACKUP_DIR/$cert_dir/privkey.pem" 2>/dev/null || {
|
|
|
|
|
log_warn " Failed to copy privkey.pem for $cert_dir"
|
|
|
|
|
}
|
|
|
|
|
fi
|
|
|
|
|
done < "$CERT_BACKUP_DIR/cert_list.txt"
|
|
|
|
|
|
|
|
|
|
log_success " Certificate files backed up"
|
|
|
|
|
else
|
|
|
|
|
log_warn " No certificates found to backup"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Step 4: Backup Docker Volume (if accessible)
|
|
|
|
|
log_info "Step 4: Attempting Docker volume backup..."
|
|
|
|
|
VOLUME_BACKUP_DIR="$BACKUP_DIR/volumes"
|
|
|
|
|
mkdir -p "$VOLUME_BACKUP_DIR"
|
|
|
|
|
|
|
|
|
|
# Try to export Docker volume
|
|
|
|
|
ssh root@"$NPMPLUS_HOST" "pct exec $NPMPLUS_VMID -- docker volume ls" > "$VOLUME_BACKUP_DIR/volume_list.txt" 2>/dev/null || {
|
|
|
|
|
log_warn " Could not list Docker volumes"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Step 5: Create backup manifest
|
|
|
|
|
log_info "Step 5: Creating backup manifest..."
|
|
|
|
|
cat > "$BACKUP_DIR/manifest.json" <<EOF
|
|
|
|
|
{
|
|
|
|
|
"timestamp": "$TIMESTAMP",
|
|
|
|
|
"backup_date": "$(date -Iseconds)",
|
|
|
|
|
"npmplus_vmid": "$NPMPLUS_VMID",
|
|
|
|
|
"npmplus_host": "$NPMPLUS_HOST",
|
|
|
|
|
"npm_url": "$NPM_URL",
|
|
|
|
|
"backup_contents": {
|
|
|
|
|
"database": {
|
|
|
|
|
"sql_dump": "$([ -s "$DB_BACKUP_DIR/database.sql" ] && echo "present" || echo "missing")",
|
|
|
|
|
"sqlite_file": "$([ -s "$DB_BACKUP_DIR/database.sqlite" ] && echo "present" || echo "missing")"
|
|
|
|
|
},
|
|
|
|
|
"api_exports": {
|
|
|
|
|
"proxy_hosts": "$([ -s "$API_BACKUP_DIR/proxy_hosts.json" ] && echo "present" || echo "missing")",
|
|
|
|
|
"certificates": "$([ -s "$API_BACKUP_DIR/certificates.json" ] && echo "present" || echo "missing")",
|
|
|
|
|
"access_lists": "$([ -s "$API_BACKUP_DIR/access_lists.json" ] && echo "present" || echo "missing")"
|
|
|
|
|
},
|
|
|
|
|
"certificate_files": "$([ -s "$CERT_BACKUP_DIR/cert_list.txt" ] && echo "present" || echo "missing")"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
EOF
|
|
|
|
|
|
|
|
|
|
# Step 6: Compress backup
|
|
|
|
|
log_info "Step 6: Compressing backup..."
|
|
|
|
|
cd "$BACKUP_BASE_DIR"
|
|
|
|
|
tar -czf "backup-$TIMESTAMP.tar.gz" "backup-$TIMESTAMP" 2>/dev/null || {
|
|
|
|
|
log_warn " Compression failed - backup directory remains uncompressed"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if [ -f "backup-$TIMESTAMP.tar.gz" ]; then
|
|
|
|
|
BACKUP_SIZE=$(du -h "backup-$TIMESTAMP.tar.gz" | cut -f1)
|
|
|
|
|
log_success " Backup compressed: backup-$TIMESTAMP.tar.gz ($BACKUP_SIZE)"
|
|
|
|
|
# Optionally remove uncompressed directory
|
|
|
|
|
# rm -rf "backup-$TIMESTAMP"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
echo ""
|
|
|
|
|
log_success "Backup completed successfully!"
|
|
|
|
|
log_info "Backup location: $BACKUP_DIR"
|
|
|
|
|
if [ -f "$BACKUP_BASE_DIR/backup-$TIMESTAMP.tar.gz" ]; then
|
|
|
|
|
log_info "Compressed backup: $BACKUP_BASE_DIR/backup-$TIMESTAMP.tar.gz"
|
|
|
|
|
fi
|
|
|
|
|
echo ""
|