#!/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 = ^ .* "(GET|POST) /login.*" 401 ^ .* "(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 = ^ .* "(GET|POST|HEAD) .*" (403|404) ignoreregex = ^ .* "(GET|POST) /api/v1/validation" ^ .* "GET /health" ^ .* "GET /static/" ^ .* "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 = ^ .* "(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 # # ============================================================================