Files
nearxos 808fbf5c7c Refactor golden image handling in backup upload process</message>
<message>Update the _set_golden_from_path function to improve the handling of existing golden image files. Replace the existing unlink logic with a more robust method that safely removes files or broken symlinks using the missing_ok parameter. This change enhances the reliability of the backup upload process by ensuring that stale references are properly cleared before setting a new golden image path.
2026-02-24 00:19:40 +02:00

1071 lines
36 KiB
Bash
Executable File

#!/bin/bash
# Deployment script for GNSS Guard Server to AWS EC2 (Debian)
# Uses Docker with Nginx + Certbot for SSL
set -e
# =============================================================================
# CONFIGURATION - Edit these values before deploying
# =============================================================================
# AWS EC2 Instance
SERVER_USER="admin"
SERVER_HOST="gnss.tototheo.com"
SERVER_PORT="22"
# SSH Key file (relative to project root or absolute path)
SSH_KEY="server/.cert/Cortex-01.pem"
# Environment file to deploy (relative to project root)
# Copy server/env.example to server/.env.prod and configure it
ENV_FILE="server/.env.prod"
# Domain for SSL (required for Let's Encrypt)
# Must match GNSS_SERVER_DOMAIN in your env file
SERVER_DOMAIN="gnss.tototheo.com"
# Email for Let's Encrypt notifications
LETSENCRYPT_EMAIL="alexander.s@tototheo.com"
# =============================================================================
# END OF CONFIGURATION
# =============================================================================
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REMOTE_PATH="/opt/gnss-guard-server"
# Helper for display: include port in ssh commands if non-standard
if [ "${SERVER_PORT}" = "22" ]; then
SSH_DISPLAY="ssh -i ${SSH_KEY} ${SERVER_USER}@${SERVER_HOST}"
else
SSH_DISPLAY="ssh -i ${SSH_KEY} -p ${SERVER_PORT} ${SERVER_USER}@${SERVER_HOST}"
fi
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Resolve SSH key path
get_ssh_key_path() {
if [[ "${SSH_KEY}" = /* ]]; then
echo "${SSH_KEY}"
else
echo "${PROJECT_DIR}/${SSH_KEY}"
fi
}
# SSH/SCP wrapper with key-based auth
ssh_cmd() {
local key_path=$(get_ssh_key_path)
ssh -i "${key_path}" -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p ${SERVER_PORT} "$@"
}
scp_cmd() {
local key_path=$(get_ssh_key_path)
scp -i "${key_path}" -P ${SERVER_PORT} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$@"
}
# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
test_connection() {
log_info "Testing SSH connection to ${SERVER_USER}@${SERVER_HOST}:${SERVER_PORT}..."
if ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "echo 'Connection successful'" 2>/dev/null; then
log_info "SSH connection successful"
return 0
else
log_error "SSH connection failed. Please ensure:"
log_error " 1. EC2 instance is running and accessible at ${SERVER_HOST}"
log_error " 2. Security group allows SSH from your IP"
log_error " 3. SSH key is configured at ${SSH_KEY}"
return 1
fi
}
install_docker() {
log_info "Installing Docker..."
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "sudo bash -s" << 'DOCKER_EOF'
set -e
# Check if Docker is already installed
if command -v docker &> /dev/null; then
echo "Docker is already installed"
docker --version
exit 0
fi
# Install Docker using official script
curl -fsSL https://get.docker.com | sh
# Add current user to docker group
usermod -aG docker $SUDO_USER || true
# Install Docker Compose plugin, rsync, and PostgreSQL client
apt-get update
apt-get install -y docker-compose-plugin rsync postgresql-client
# Start and enable Docker
systemctl enable docker
systemctl start docker
echo "Docker installed successfully"
docker --version
docker compose version
DOCKER_EOF
log_info "Docker installed"
}
ensure_rsync() {
# Ensure rsync is installed on the remote server
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "which rsync > /dev/null 2>&1 || sudo apt-get update && sudo apt-get install -y rsync" > /dev/null 2>&1
}
ensure_postgresql_client() {
# Ensure postgresql-client is installed on the remote server
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "which psql > /dev/null 2>&1 || sudo apt-get update && sudo apt-get install -y postgresql-client" > /dev/null 2>&1
}
install_fail2ban() {
log_info "Installing and configuring fail2ban..."
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "sudo bash -s" << 'FAIL2BAN_EOF'
set -e
# Install fail2ban
if ! command -v fail2ban-server &> /dev/null; then
apt-get update
apt-get install -y fail2ban
echo "fail2ban installed"
else
echo "fail2ban already installed"
fi
# Create local jail configuration
cat > /etc/fail2ban/jail.local << 'JAIL_EOF'
# GNSS Guard Server - fail2ban configuration
# Auto-generated by deploy_server.sh
[DEFAULT]
# Ban for 1 hour after 5 failures
bantime = 3600
findtime = 600
maxretry = 5
# Use iptables for banning
banaction = iptables-multiport
# Email notifications (uncomment and configure if needed)
# destemail = admin@example.com
# sendername = Fail2Ban
# action = %(action_mwl)s
# Whitelist internal networks (add your office IPs if needed)
ignoreip = 127.0.0.1/8 ::1
# =============================================================================
# SSH Protection
# =============================================================================
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 5
bantime = 3600
# =============================================================================
# Nginx Protection (HTTP/HTTPS)
# =============================================================================
# Block IPs with too many login failures (401)
[nginx-login]
enabled = true
port = http,https
filter = nginx-login
logpath = /var/log/nginx/access.log
backend = auto
maxretry = 10
findtime = 300
bantime = 3600
# Block IPs with excessive 4xx errors (scanners)
[nginx-badbots]
enabled = true
port = http,https
filter = nginx-badbots
logpath = /var/log/nginx/access.log
backend = auto
maxretry = 30
findtime = 60
bantime = 7200
# Block IPs hammering the validation endpoint
[nginx-ratelimit]
enabled = true
port = http,https
filter = nginx-ratelimit
logpath = /var/log/nginx/access.log
backend = auto
maxretry = 100
findtime = 60
bantime = 1800
JAIL_EOF
# Create nginx login filter (detects failed login attempts)
cat > /etc/fail2ban/filter.d/nginx-login.conf << 'FILTER_EOF'
# Fail2Ban filter for nginx login failures
# Matches 401 responses on login-related endpoints
[Definition]
failregex = ^<HOST> .* "(GET|POST) /login.*" 401
^<HOST> .* "(GET|POST) /api/v1/.*" 401
ignoreregex =
FILTER_EOF
# Create nginx badbots filter (detects scanners and bad bots)
cat > /etc/fail2ban/filter.d/nginx-badbots.conf << 'FILTER_EOF'
# Fail2Ban filter for nginx bad bots and scanners
# Matches excessive 403/404 errors
[Definition]
failregex = ^<HOST> .* "(GET|POST|HEAD) .*" (403|404)
ignoreregex = ^<HOST> .* "(GET|POST) /api/v1/validation"
^<HOST> .* "GET /health"
^<HOST> .* "GET /static/"
^<HOST> .* "GET /favicon.ico"
FILTER_EOF
# Create nginx rate limit filter (detects endpoint hammering)
cat > /etc/fail2ban/filter.d/nginx-ratelimit.conf << 'FILTER_EOF'
# Fail2Ban filter for nginx rate limiting
# Matches high-frequency requests to any endpoint
[Definition]
failregex = ^<HOST> .* "(GET|POST) /api/v1/validation" [0-9]{3}
ignoreregex =
FILTER_EOF
# Ensure nginx log directory and files exist before fail2ban starts
mkdir -p /var/log/nginx
touch /var/log/nginx/access.log
touch /var/log/nginx/error.log
chmod 644 /var/log/nginx/access.log /var/log/nginx/error.log
# Enable fail2ban
systemctl enable fail2ban
# Restart fail2ban and wait for it to start
systemctl restart fail2ban
sleep 2
# Check if fail2ban started successfully
if systemctl is-active --quiet fail2ban; then
echo "fail2ban configured and started successfully"
fail2ban-client status
else
echo "fail2ban service failed to start, checking logs..."
journalctl -u fail2ban --no-pager -n 20
# Try to start it again
systemctl start fail2ban
fi
FAIL2BAN_EOF
log_info "fail2ban installed and configured"
}
ensure_database() {
log_info "Ensuring database exists..."
# Ensure postgresql-client is available
ensure_postgresql_client
# Extract database connection info from env file on server
local db_url=$(ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "grep '^GNSS_SERVER_DATABASE_URL=' ${REMOTE_PATH}/.env.prod 2>/dev/null | cut -d'=' -f2- | tr -d '\"' | tr -d \"'\"")
if [ -z "${db_url}" ]; then
log_warn "GNSS_SERVER_DATABASE_URL not found in .env.prod, skipping database check"
return 0
fi
# Parse the database URL: postgresql://user:password@host:port/database
local db_user=$(echo "${db_url}" | sed -n 's|postgresql://\([^:]*\):.*|\1|p')
local db_pass=$(echo "${db_url}" | sed -n 's|postgresql://[^:]*:\([^@]*\)@.*|\1|p')
local db_host=$(echo "${db_url}" | sed -n 's|postgresql://[^@]*@\([^:/]*\).*|\1|p')
local db_port=$(echo "${db_url}" | sed -n 's|postgresql://[^@]*@[^:]*:\([0-9]*\)/.*|\1|p')
local db_name=$(echo "${db_url}" | sed -n 's|postgresql://[^@]*@[^/]*/\([^?]*\).*|\1|p')
[ -z "${db_port}" ] && db_port="5432"
if [ -z "${db_host}" ] || [ -z "${db_name}" ] || [ -z "${db_user}" ]; then
log_warn "Could not parse database URL, skipping database check"
return 0
fi
log_info "Checking if database '${db_name}' exists on ${db_host}..."
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "PGPASSWORD='${db_pass}' psql -h ${db_host} -p ${db_port} -U ${db_user} -d postgres -tc \"SELECT 1 FROM pg_database WHERE datname = '${db_name}'\" | grep -q 1 || PGPASSWORD='${db_pass}' psql -h ${db_host} -p ${db_port} -U ${db_user} -d postgres -c \"CREATE DATABASE ${db_name}\""
if [ $? -eq 0 ]; then
log_info "Database '${db_name}' ready"
else
log_warn "Could not verify/create database (may require manual creation)"
fi
}
deploy_server_files() {
log_info "Deploying server files to ${SERVER_USER}@${SERVER_HOST}:${REMOTE_PATH}..."
ensure_rsync
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "sudo mkdir -p ${REMOTE_PATH} && sudo chown ${SERVER_USER}:${SERVER_USER} ${REMOTE_PATH}"
local ssh_key_path=$(get_ssh_key_path)
local rsync_ssh="ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p ${SERVER_PORT}"
if [ -n "${SSH_KEY}" ] && [ -f "${ssh_key_path}" ]; then
rsync_ssh="${rsync_ssh} -i ${ssh_key_path}"
fi
# Backup SSL nginx config if it exists
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "test -f ${REMOTE_PATH}/nginx/conf.d/default.conf && \
grep -q 'ssl_certificate' ${REMOTE_PATH}/nginx/conf.d/default.conf && \
cp ${REMOTE_PATH}/nginx/conf.d/default.conf /tmp/nginx-ssl-backup.conf || true" 2>/dev/null
rsync -avz --delete \
-e "${rsync_ssh}" \
--exclude='.env*' \
--exclude='env.example' \
--exclude='.cert/' \
--exclude='*.pem' \
--exclude='.DS_Store' \
--exclude='__pycache__' \
--exclude='*.pyc' \
"${PROJECT_DIR}/server/" "${SERVER_USER}@${SERVER_HOST}:${REMOTE_PATH}/"
# Restore SSL nginx config if it was backed up
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "test -f /tmp/nginx-ssl-backup.conf && \
cp /tmp/nginx-ssl-backup.conf ${REMOTE_PATH}/nginx/conf.d/default.conf && \
rm /tmp/nginx-ssl-backup.conf && echo 'SSL nginx config restored' || true" 2>/dev/null
log_info "Server files deployed successfully"
}
deploy_env_file() {
log_info "Deploying server environment file..."
local env_file_path="${PROJECT_DIR}/${ENV_FILE}"
if [ ! -f "${env_file_path}" ]; then
log_error "Environment file not found: ${env_file_path}"
log_error ""
log_error "Please create it first:"
log_error " cp server/env.example server/.env.prod"
log_error " # Edit server/.env.prod with your configuration"
return 1
fi
if ! grep -q "^GNSS_SERVER_DATABASE_URL=" "${env_file_path}" || \
grep -q "your-password\|your-rds-endpoint" "${env_file_path}"; then
log_warn "Environment file may have placeholder values!"
log_warn "Please ensure GNSS_SERVER_DATABASE_URL is properly configured."
fi
scp_cmd "${env_file_path}" "${SERVER_USER}@${SERVER_HOST}:${REMOTE_PATH}/.env.prod"
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "chmod 600 ${REMOTE_PATH}/.env.prod"
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cat >> ${REMOTE_PATH}/.env.prod << EOF
# SSL settings (added by deploy script)
LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL}
EOF"
log_info "Environment file deployed"
}
clean_docker() {
log_info "Cleaning Docker build cache and unused images..."
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "docker system prune -f && docker builder prune -f 2>/dev/null || true"
log_info "Docker cache cleaned"
}
stop_server() {
log_info "Stopping Docker containers..."
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH} && docker compose down 2>/dev/null || true"
log_info "Containers stopped"
}
build_and_start() {
local CLEAN_BUILD=$1
ensure_database
if [ "$CLEAN_BUILD" = "true" ]; then
clean_docker
fi
log_info "Building and starting Docker containers..."
if ! ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH} && docker compose up -d --build" 2>&1; then
log_warn "Build failed, attempting clean rebuild..."
clean_docker
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH} && docker compose build --no-cache && docker compose up -d"
fi
sleep 5
log_info "Container status:"
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH} && docker compose ps"
}
setup_ssl() {
log_info "Setting up SSL certificate with Let's Encrypt..."
if [ -z "${SERVER_DOMAIN}" ] || [ "${SERVER_DOMAIN}" = "gnss.yourdomain.com" ]; then
log_error "SERVER_DOMAIN not configured properly"
log_error "Edit deploy_server.sh and set SERVER_DOMAIN"
return 1
fi
log_info "Requesting SSL certificate for ${SERVER_DOMAIN}..."
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH} && docker compose run --rm --entrypoint certbot certbot certonly --webroot --webroot-path=/var/www/certbot --email ${LETSENCRYPT_EMAIL} --agree-tos --no-eff-email --non-interactive -d ${SERVER_DOMAIN}"
log_info "Configuring nginx for SSL..."
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH}/nginx/conf.d && \
cp gnss-guard-ssl.conf.template gnss-guard-ssl.conf && \
sed -i 's/YOUR_DOMAIN_HERE/${SERVER_DOMAIN}/g' gnss-guard-ssl.conf && \
rm -f default.conf && \
mv gnss-guard-ssl.conf default.conf"
log_info "Reloading nginx..."
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH} && docker compose restart nginx"
log_info "SSL setup complete!"
log_info "Your server is now accessible at: https://${SERVER_DOMAIN}"
}
show_status() {
log_info "Server status:"
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH} && docker compose ps"
}
show_logs() {
local FOLLOW=$1
if [ "$FOLLOW" = "true" ]; then
log_info "Following server logs (Ctrl+C to stop)..."
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH} && docker compose logs -f gnss-server"
else
log_info "Recent server logs:"
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH} && docker compose logs --tail=100 gnss-server"
fi
}
show_rds_instructions() {
echo ""
echo "============================================================================="
echo "AWS RDS PostgreSQL Setup Instructions"
echo "============================================================================="
echo ""
echo "1. Create an RDS PostgreSQL instance:"
echo " - Engine: PostgreSQL 15+"
echo " - Instance class: db.t3.micro (free tier) or larger"
echo " - Storage: 20GB General Purpose SSD"
echo " - VPC: Same as your EC2 instance"
echo " - Public access: No (recommended)"
echo ""
echo "2. Configure Security Group:"
echo " - Allow inbound PostgreSQL (port 5432) from EC2 security group"
echo ""
echo "3. Create database and user (connect from EC2 instance):"
echo " psql -h YOUR_RDS_ENDPOINT -U postgres"
echo " CREATE DATABASE gnss_guard;"
echo " CREATE USER gnss_admin WITH PASSWORD 'YOUR_SECURE_PASSWORD';"
echo " GRANT ALL PRIVILEGES ON DATABASE gnss_guard TO gnss_admin;"
echo ""
echo "4. Create and configure your environment file:"
echo " cp server/env.example server/.env.prod"
echo " # Edit server/.env.prod and set GNSS_SERVER_DATABASE_URL"
echo ""
echo "============================================================================="
}
show_ec2_instructions() {
echo ""
echo "============================================================================="
echo "AWS EC2 Setup Instructions"
echo "============================================================================="
echo ""
echo "1. Launch EC2 instance:"
echo " - AMI: Debian 12 (or Ubuntu 22.04)"
echo " - Instance type: t3.micro (free tier) or t3.small"
echo " - Storage: 20GB"
echo ""
echo "2. Configure Security Group - Allow inbound:"
echo " - SSH (22) from your IP"
echo " - HTTP (80) from anywhere"
echo " - HTTPS (443) from anywhere"
echo " - PostgreSQL (5432) to/from your RDS security group"
echo ""
echo "3. Connect and test:"
echo " ssh -i your-key.pem admin@YOUR_EC2_IP"
echo ""
echo "4. Configure this script:"
echo " SERVER_HOST=\"YOUR_EC2_IP\""
echo " SERVER_USER=\"admin\" # or 'ubuntu' for Ubuntu AMI"
echo ""
echo "============================================================================="
}
# =============================================================================
# MAIN
# =============================================================================
show_fail2ban_status() {
log_info "fail2ban status:"
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "sudo fail2ban-client status 2>/dev/null || echo 'fail2ban not installed'"
echo ""
log_info "Jail details:"
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "sudo fail2ban-client status sshd 2>/dev/null || true"
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "sudo fail2ban-client status nginx-login 2>/dev/null || true"
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "sudo fail2ban-client status nginx-badbots 2>/dev/null || true"
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "sudo fail2ban-client status nginx-ratelimit 2>/dev/null || true"
}
# =============================================================================
# LOCAL MODE - Run server locally with converted client database
# =============================================================================
run_local() {
local asset_name="${1:-Local Asset}"
local client_db="${PROJECT_DIR}/data/gnss_guard.db"
local server_db="${PROJECT_DIR}/server/data/server_local.db"
local env_file="${PROJECT_DIR}/server/.env.local"
log_info "Running server in local mode..."
# Check if client database exists
if [ ! -f "${client_db}" ]; then
log_error "Client database not found: ${client_db}"
log_error "Please ensure data/gnss_guard.db exists (from client installation)"
return 1
fi
# Check for Python
if ! command -v python3 &> /dev/null; then
log_error "Python 3 is required but not found"
return 1
fi
# Create server data directory
mkdir -p "${PROJECT_DIR}/server/data"
# Convert client database to server format
log_info "Converting client database to server format..."
log_info " Asset name: ${asset_name}"
if ! python3 "${PROJECT_DIR}/server/tools/import_client_db_to_sqlite.py" \
--asset-name "${asset_name}" \
--input "${client_db}" \
--output "${server_db}"; then
log_error "Database conversion failed"
return 1
fi
echo ""
# Create local environment file
log_info "Creating local environment file..."
cat > "${env_file}" << EOF
# Local server configuration (auto-generated)
# SQLite database for local testing
GNSS_SERVER_DATABASE_URL=sqlite:///${server_db}
GNSS_SERVER_WEB_USERNAME=test
GNSS_SERVER_WEB_PASSWORD=Tototheo.25!
GNSS_SERVER_SECRET_KEY=local-dev-secret-key-change-in-production
GNSS_SERVER_DEBUG=true
GNSS_SERVER_HOST=127.0.0.1
GNSS_SERVER_PORT=8000
EOF
log_info "Environment file created: ${env_file}"
echo ""
# Install server dependencies if needed
if [ -f "${PROJECT_DIR}/server/requirements.txt" ]; then
log_info "Checking server dependencies..."
pip3 install -q -r "${PROJECT_DIR}/server/requirements.txt" 2>/dev/null || {
log_warn "Some dependencies may not be installed. Running anyway..."
}
fi
echo ""
log_info "=============================================="
log_info "Starting local GNSS Guard Server"
log_info "=============================================="
echo ""
log_info "Dashboard: http://localhost:8000"
log_info "Login: admin / localadmin123"
echo ""
log_info "Press Ctrl+C to stop the server"
echo ""
# Change to server directory and run
cd "${PROJECT_DIR}/server"
# Set environment file path
export ENV_FILE="${env_file}"
# Run the server using uvicorn
python3 -m uvicorn main:app --host 127.0.0.1 --port 8000 --reload --env-file "${env_file}"
}
unban_ip() {
local ip=$1
log_info "Unbanning IP ${ip} from all jails..."
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "sudo fail2ban-client unban ${ip} 2>/dev/null || echo 'IP not banned or fail2ban not running'"
log_info "Done"
}
# =============================================================================
# IMPORT CLIENT DATABASES - Import .db files from server/import/ to PostgreSQL
# =============================================================================
# Filename format: {asset_id}_{asset_name}.db
# Example: 2_msc_charlotte.db -> Asset ID: 2, Asset Name: "MSC Charlotte"
# =============================================================================
import_client_databases() {
local import_dir="${PROJECT_DIR}/server/import"
local import_script="${PROJECT_DIR}/server/tools/import_client_db_to_postgres.py"
# Check if import directory exists and has .db files
if [ ! -d "${import_dir}" ]; then
log_info "No import directory found at server/import/, skipping import"
return 0
fi
local db_files=$(find "${import_dir}" -maxdepth 1 -name "*.db" -type f 2>/dev/null)
if [ -z "${db_files}" ]; then
log_info "No .db files found in server/import/, skipping import"
return 0
fi
log_info "Found client databases to import:"
echo "${db_files}" | while read -r f; do
echo " - $(basename "$f")"
done
echo ""
# Get database URL from env file on server
local db_url=$(ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "grep '^GNSS_SERVER_DATABASE_URL=' ${REMOTE_PATH}/.env.prod 2>/dev/null | cut -d'=' -f2- | tr -d '\"' | tr -d \"'\"")
if [ -z "${db_url}" ]; then
log_error "GNSS_SERVER_DATABASE_URL not found in .env.prod, cannot import databases"
return 1
fi
# Create import directory on server
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "mkdir -p ${REMOTE_PATH}/import"
# Copy import script to server
log_info "Deploying import script to server..."
scp_cmd "${import_script}" "${SERVER_USER}@${SERVER_HOST}:${REMOTE_PATH}/import/"
# Install psycopg2 on server if not present
log_info "Ensuring psycopg2-binary is installed on server..."
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "sudo apt-get update -qq && sudo apt-get install -y -qq python3-pip libpq-dev > /dev/null 2>&1; sudo pip3 install --break-system-packages psycopg2-binary 2>/dev/null || sudo pip3 install psycopg2-binary 2>/dev/null || pip3 install psycopg2-binary"
# Process each .db file
echo "${db_files}" | while read -r db_file; do
local filename=$(basename "${db_file}")
# Parse filename: {asset_id}_{asset_name}.db
# Example: 2_msc_charlotte.db -> asset_id=2, asset_name="MSC Charlotte"
local asset_id=$(echo "${filename}" | sed -n 's/^\([0-9]*\)_.*/\1/p')
local asset_name_raw=$(echo "${filename}" | sed -n 's/^[0-9]*_\(.*\)\.db$/\1/p')
if [ -z "${asset_id}" ] || [ -z "${asset_name_raw}" ]; then
log_warn "Could not parse filename '${filename}' (expected format: {id}_{name}.db)"
log_warn "Skipping this file..."
continue
fi
# Convert asset name: msc_charlotte -> MSC Charlotte
# Replace underscores with spaces, capitalize each word
local asset_name=$(echo "${asset_name_raw}" | tr '_' ' ' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2))}1')
# Handle uppercase acronyms like MSC (use space/start boundaries for compatibility)
asset_name=$(echo "${asset_name}" | sed 's/^Msc /MSC /g; s/ Msc / MSC /g; s/ Msc$/ MSC/g; s/^Msc$/MSC/g')
log_info "Importing: ${filename}"
log_info " Asset ID: ${asset_id}"
log_info " Asset Name: ${asset_name}"
# Copy database file to server
scp_cmd "${db_file}" "${SERVER_USER}@${SERVER_HOST}:${REMOTE_PATH}/import/"
# Run import script on server
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH}/import && python3 import_client_db_to_postgres.py \
--input '${filename}' \
--asset-name '${asset_name}' \
--asset-id ${asset_id} \
--database-url '${db_url}'"
if [ $? -eq 0 ]; then
log_info "Successfully imported ${filename}"
# Optionally move processed file to .imported
# ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "mv ${REMOTE_PATH}/import/${filename} ${REMOTE_PATH}/import/${filename}.imported"
else
log_error "Failed to import ${filename}"
fi
echo ""
done
log_info "Database import complete"
}
show_help() {
echo "Usage: $0 [OPTIONS]"
echo ""
echo "GNSS Guard Server Deployment Script (Docker + Nginx)"
echo ""
echo "Options:"
echo " --start, -s Deploy and start server (default if no options)"
echo " --debug, -d Deploy and follow logs in terminal (foreground mode)"
echo " --ssl Setup SSL certificate with Let's Encrypt"
echo " --clean, -c Clean Docker cache before building"
echo " --stop Stop server containers"
echo " --restart Restart server containers"
echo " --status Show container status"
echo " --logs Show recent server logs"
echo " --logs-all Show all container logs"
echo " --fail2ban Show fail2ban status and banned IPs"
echo " --unban IP Unban a specific IP address"
echo " --import Import client databases from server/import/ to PostgreSQL"
echo " --local [NAME] Run server locally with converted client database"
echo " --rds Show RDS setup instructions"
echo " --ec2 Show EC2 setup instructions"
echo " --help, -h Show this help message"
echo ""
echo "Environment variables:"
echo " SERVER_USER SSH username (default: admin)"
echo " SERVER_HOST Server hostname/IP (default: from config)"
echo " SERVER_PORT SSH port (default: 22)"
echo " ENV_FILE Path to env file (default: server/.env.prod)"
echo ""
echo "Examples:"
echo " $0 Deploy and start server (production mode)"
echo " $0 --debug Deploy and follow logs in terminal"
echo " $0 --ssl Setup SSL certificate after deployment"
echo " $0 --clean Deploy with clean Docker cache"
echo " $0 --status Check server status"
echo " $0 --local Run locally with data/gnss_guard.db"
echo " $0 --local 'MSC Charlotte' Run locally with custom asset name"
echo " $0 --import Import databases from server/import/"
echo ""
echo "Import database format:"
echo " Place .db files in server/import/ with format: {id}_{name}.db"
echo " Example: 2_msc_charlotte.db -> Asset ID: 2, Name: 'MSC Charlotte'"
echo ""
echo "First-time deployment:"
echo " 1. Setup AWS: $0 --ec2 (follow instructions)"
echo " 2. Setup RDS: $0 --rds (follow instructions)"
echo " 3. Create env file: cp server/env.example server/.env.prod"
echo " 4. Edit server/.env.prod with your configuration"
echo " 5. Edit CONFIGURATION section in this script"
echo " 6. Run: $0"
echo " 7. Configure DNS to point ${SERVER_DOMAIN} to your server"
echo " 8. Run: $0 --ssl"
echo ""
echo "Access:"
echo " Before SSL: http://${SERVER_HOST}"
echo " After SSL: https://${SERVER_DOMAIN}"
echo ""
}
# Parse options
DEBUG_MODE=false
CLEAN_BUILD=false
SSL_SETUP=false
STOP_ONLY=false
RESTART_ONLY=false
STATUS_ONLY=false
LOGS_ONLY=false
LOGS_ALL=false
SHOW_RDS=false
SHOW_EC2=false
SHOW_FAIL2BAN=false
UNBAN_IP=""
START_MODE=true
LOCAL_MODE=false
LOCAL_ASSET_NAME=""
IMPORT_ONLY=false
while [[ $# -gt 0 ]]; do
case $1 in
--start|-s)
START_MODE=true
shift
;;
--debug|-d)
DEBUG_MODE=true
START_MODE=true
shift
;;
--ssl)
SSL_SETUP=true
START_MODE=false
shift
;;
--clean|-c)
CLEAN_BUILD=true
shift
;;
--stop)
STOP_ONLY=true
START_MODE=false
shift
;;
--restart)
RESTART_ONLY=true
START_MODE=false
shift
;;
--status)
STATUS_ONLY=true
START_MODE=false
shift
;;
--logs)
LOGS_ONLY=true
START_MODE=false
shift
;;
--logs-all)
LOGS_ALL=true
START_MODE=false
shift
;;
--fail2ban)
SHOW_FAIL2BAN=true
START_MODE=false
shift
;;
--import)
IMPORT_ONLY=true
START_MODE=false
shift
;;
--local)
LOCAL_MODE=true
START_MODE=false
# Check for optional asset name
if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
LOCAL_ASSET_NAME="$2"
shift
fi
shift
;;
--unban)
if [[ -n "$2" && ! "$2" =~ ^-- ]]; then
UNBAN_IP="$2"
START_MODE=false
shift 2
else
log_error "--unban requires an IP address"
exit 1
fi
;;
--rds)
SHOW_RDS=true
START_MODE=false
shift
;;
--ec2)
SHOW_EC2=true
START_MODE=false
shift
;;
--help|-h)
show_help
exit 0
;;
*)
log_error "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
done
# Handle info-only commands (no connection needed)
if [ "$SHOW_RDS" = "true" ]; then
show_rds_instructions
exit 0
fi
if [ "$SHOW_EC2" = "true" ]; then
show_ec2_instructions
exit 0
fi
# Test connection for all other operations
test_connection || exit 1
# Handle single operations
if [ "$STOP_ONLY" = "true" ]; then
stop_server
exit 0
fi
if [ "$RESTART_ONLY" = "true" ]; then
stop_server
build_and_start "$CLEAN_BUILD"
exit 0
fi
if [ "$STATUS_ONLY" = "true" ]; then
show_status
exit 0
fi
if [ "$LOGS_ONLY" = "true" ]; then
show_logs "true"
exit 0
fi
if [ "$LOGS_ALL" = "true" ]; then
log_info "Following all container logs (Ctrl+C to stop)..."
ssh_cmd "${SERVER_USER}@${SERVER_HOST}" "cd ${REMOTE_PATH} && docker compose logs -f"
exit 0
fi
if [ "$SHOW_FAIL2BAN" = "true" ]; then
show_fail2ban_status
exit 0
fi
if [ -n "$UNBAN_IP" ]; then
unban_ip "$UNBAN_IP"
exit 0
fi
if [ "$LOCAL_MODE" = "true" ]; then
run_local "$LOCAL_ASSET_NAME"
exit 0
fi
if [ "$IMPORT_ONLY" = "true" ]; then
import_client_databases
exit 0
fi
if [ "$SSL_SETUP" = "true" ]; then
setup_ssl
exit 0
fi
# Full deployment
log_info "Starting deployment to ${SERVER_USER}@${SERVER_HOST}"
log_info "Target: ${REMOTE_PATH}"
install_docker
stop_server
deploy_server_files
deploy_env_file || exit 1
build_and_start "$CLEAN_BUILD"
# Install fail2ban after containers are running (so nginx logs exist)
install_fail2ban
# Import client databases from server/import/ if any exist
import_client_databases
echo ""
log_info "Deployment completed successfully!"
echo ""
log_info "Server management commands:"
log_info " Status: $0 --status"
log_info " Stop: $0 --stop"
log_info " Restart: $0 --restart"
log_info " Logs: $0 --logs"
echo ""
if [ "$DEBUG_MODE" = "true" ]; then
log_info "Starting debug mode (following logs)..."
log_info "Press Ctrl+C to stop"
echo ""
show_logs "true"
else
log_info "GNSS Guard Server is now running!"
echo ""
log_info "Next steps:"
log_info " 1. Ensure DNS is configured: ${SERVER_DOMAIN} -> ${SERVER_HOST}"
log_info " 2. Run: $0 --ssl"
log_info " 3. Access dashboard: https://${SERVER_DOMAIN}"
echo ""
log_info "Import assets:"
log_info " curl -X POST https://${SERVER_DOMAIN}/api/v1/admin/assets/import/batch \\"
log_info " -H 'Content-Type: application/json' \\"
log_info " -d @.configs/assets.csv"
fi
# ============================================================================
# QUICK REFERENCE COMMANDS
# ============================================================================
#
# Deploy and manage server:
# ./deploy_server.sh # Deploy and start (production)
# ./deploy_server.sh --debug # Deploy and follow logs
# ./deploy_server.sh --clean # Deploy with clean Docker cache
# ./deploy_server.sh --ssl # Setup SSL certificate
#
# Server management:
# ./deploy_server.sh --status # Show container status
# ./deploy_server.sh --stop # Stop containers
# ./deploy_server.sh --restart # Restart containers
# ./deploy_server.sh --logs # Follow server logs
# ./deploy_server.sh --logs-all # Follow all container logs
#
# Security (fail2ban):
# ./deploy_server.sh --fail2ban # Show fail2ban status and banned IPs
# ./deploy_server.sh --unban IP # Unban a specific IP address
#
# Local development:
# ./deploy_server.sh --local # Run locally with data/gnss_guard.db
# ./deploy_server.sh --local 'My Asset' # Run locally with custom asset name
#
# Import client databases:
# ./deploy_server.sh --import # Import .db files from server/import/
# # Filename format: {id}_{name}.db (e.g., 2_msc_charlotte.db)
#
# Setup instructions:
# ./deploy_server.sh --ec2 # EC2 setup instructions
# ./deploy_server.sh --rds # RDS setup instructions
#
# Direct SSH access:
# ssh -i server/.cert/Cortex-01.pem admin@gnss.tototheo.com
#
# Docker commands on server:
# docker compose ps # Container status
# docker compose logs -f # Follow all logs
# docker compose exec gnss-server bash # Shell into server
#
# ============================================================================