#!/bin/bash # Mock API Installation Script # This script installs the Mock API application as a systemd service set -e # Exit on error # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Configuration APP_NAME="mockapi" APP_DIR="/opt/mockapi" SERVICE_NAME="mockapi" SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service" VENV_DIR="${APP_DIR}/venv" PORT="8000" RUN_USER="www-data" # Default user, can be changed below print_status() { echo -e "${GREEN}[+]${NC} $1" } print_warning() { echo -e "${YELLOW}[!]${NC} $1" } print_error() { echo -e "${RED}[!]${NC} $1" } print_info() { echo -e "${BLUE}[i]${NC} $1" } check_root() { if [[ $EUID -eq 0 ]]; then print_warning "Running as root. It's recommended to run this script as a regular user." read -p "Continue anyway? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1 fi fi } check_prerequisites() { print_status "Checking prerequisites..." # Check Python if ! command -v python3 &> /dev/null; then print_error "Python3 is not installed. Please install it first." print_info "On Ubuntu/Debian: sudo apt-get install python3" exit 1 fi # Check Python version for compatibility PYTHON_VERSION=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')") PYTHON_MAJOR=$(python3 -c "import sys; print(sys.version_info.major)") PYTHON_MINOR=$(python3 -c "import sys; print(sys.version_info.minor)") print_info "Python version: $PYTHON_VERSION" # Warn about Python 3.13+ compatibility issues if [[ $PYTHON_MAJOR -eq 3 ]] && [[ $PYTHON_MINOR -ge 13 ]]; then print_warning "Python $PYTHON_VERSION detected. Some packages may have compatibility issues." print_warning "For production, Python 3.11 or 3.12 is recommended for better package compatibility." echo print_info "Checking for alternative Python versions..." # Check if python3.12 or python3.11 are available if command -v python3.12 &> /dev/null; then print_info "Found python3.12. You can switch to it by modifying this script." elif command -v python3.11 &> /dev/null; then print_info "Found python3.11. You can switch to it by modifying this script." else print_info "No alternative Python versions found. Continuing with Python $PYTHON_VERSION." fi echo print_warning "Continuing with Python $PYTHON_VERSION. Compatibility issues may occur." read -p "Continue anyway? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then print_info "Installation aborted. Please install Python 3.11 or 3.12 and try again." print_info "On Ubuntu/Debian: sudo apt-get install python3.12" exit 1 fi fi # Check pip if ! command -v pip3 &> /dev/null; then print_warning "pip3 not found. Installing..." sudo apt-get update sudo apt-get install -y python3-pip fi # Check virtualenv if ! python3 -m venv --help &> /dev/null; then print_warning "venv module not available. Installing..." sudo apt-get install -y python3-venv fi print_status "Prerequisites check passed." } generate_random_string() { local length=$1 openssl rand -base64 $((length * 3 / 4)) | tr -d '/+=' | head -c "$length" } setup_environment() { print_status "Setting up Python virtual environment..." # Get Python version for compatibility handling PYTHON_MAJOR=$(python3 -c "import sys; print(sys.version_info.major)") PYTHON_MINOR=$(python3 -c "import sys; print(sys.version_info.minor)") # For Python 3.13+, always recreate virtual environment for fresh install if [[ $PYTHON_MAJOR -eq 3 ]] && [[ $PYTHON_MINOR -ge 13 ]]; then print_warning "Python 3.13+ detected. Forcing fresh virtual environment for compatibility." if [[ -d "$VENV_DIR" ]]; then rm -rf "$VENV_DIR" print_status "Removed existing virtual environment." fi python3 -m venv "$VENV_DIR" print_status "Created fresh virtual environment for Python 3.13+ compatibility." else # Normal flow for Python < 3.13 if [[ ! -d "$VENV_DIR" ]]; then python3 -m venv "$VENV_DIR" print_status "Virtual environment created at $VENV_DIR" else print_warning "Virtual environment already exists at $VENV_DIR" read -p "Recreate virtual environment? (y/N): " -n 1 -r echo if [[ $REPLY =~ ^[Yy]$ ]]; then rm -rf "$VENV_DIR" python3 -m venv "$VENV_DIR" print_status "Virtual environment recreated." fi fi fi # Activate virtual environment and install dependencies source "${VENV_DIR}/bin/activate" # Upgrade pip to latest version (important for Python 3.13+ compatibility) print_status "Upgrading pip to latest version..." pip install --upgrade pip # For Python 3.13+, install packages with --upgrade flag and better error handling print_status "Installing Python dependencies..." if [[ $PYTHON_MAJOR -eq 3 ]] && [[ $PYTHON_MINOR -ge 13 ]]; then print_info "Python 3.13+ detected: Using --upgrade flag for all packages." pip install --upgrade -r "${APP_DIR}/requirements.txt" else pip install -r "${APP_DIR}/requirements.txt" fi deactivate print_status "Dependencies installed." } configure_application() { print_status "Configuring application..." # Check if .env exists if [[ -f "${APP_DIR}/.env" ]]; then print_warning ".env file already exists." read -p "Overwrite with new configuration? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then print_info "Using existing .env file." return fi fi # Generate secure credentials print_info "Generating secure credentials..." ADMIN_PASSWORD=$(generate_random_string 16) SECRET_KEY=$(generate_random_string 64) # Ask for admin username read -p "Enter admin username [admin]: " admin_username ADMIN_USERNAME=${admin_username:-admin} # Ask for database path read -p "Enter database path [${APP_DIR}/mockapi.db]: " db_path DB_PATH=${db_path:-${APP_DIR}/mockapi.db} # Create .env file with generated credentials cat > "${APP_DIR}/.env" << EOF # Mock API Configuration # Generated on $(date) # Database Configuration DATABASE_URL=sqlite+aiosqlite:///${DB_PATH} # Admin Authentication ADMIN_USERNAME=${ADMIN_USERNAME} ADMIN_PASSWORD=${ADMIN_PASSWORD} # Security SECRET_KEY=${SECRET_KEY} # Application Settings DEBUG=False LOG_LEVEL=INFO # OAuth2 Settings OAUTH2_ISSUER=http://localhost:${PORT} OAUTH2_ACCESS_TOKEN_EXPIRE_MINUTES=30 OAUTH2_REFRESH_TOKEN_EXPIRE_DAYS=7 OAUTH2_AUTHORIZATION_CODE_EXPIRE_MINUTES=10 OAUTH2_SUPPORTED_GRANT_TYPES=["authorization_code", "client_credentials", "refresh_token"] OAUTH2_SUPPORTED_SCOPES=["openid", "profile", "email", "api:read", "api:write"] OAUTH2_PKCE_REQUIRED=false # Server Settings HOST=0.0.0.0 PORT=${PORT} EOF # Set secure permissions chmod 600 "${APP_DIR}/.env" print_status "Created .env file with secure credentials." print_info "Admin username: ${ADMIN_USERNAME}" print_info "Admin password: ${ADMIN_PASSWORD}" print_info "Secret key: ${SECRET_KEY:0:16}..." echo print_warning "IMPORTANT: Save these credentials in a secure location!" print_warning "You will need the admin password to log in." echo # Create database directory if needed DB_DIR=$(dirname "$DB_PATH") if [[ ! -d "$DB_DIR" ]]; then mkdir -p "$DB_DIR" fi } setup_systemd_service() { print_status "Setting up systemd service..." # Ask for run user echo print_warning "Choose a user to run the service:" echo "1) www-data (recommended for web applications)" echo "2) Current user ($USER)" echo "3) Create new user 'mockapi'" echo "4) Custom user" read -p "Enter choice [1-4]: " user_choice case $user_choice in 1) RUN_USER="www-data" ;; 2) RUN_USER="$USER" ;; 3) RUN_USER="mockapi" if ! id -u "$RUN_USER" &> /dev/null; then sudo adduser --system --no-create-home --group "$RUN_USER" print_status "Created user '$RUN_USER'" fi ;; 4) read -p "Enter username: " custom_user if id -u "$custom_user" &> /dev/null; then RUN_USER="$custom_user" else print_error "User '$custom_user' does not exist." exit 1 fi ;; *) RUN_USER="www-data" print_warning "Using default user: www-data" ;; esac # Ask for port read -p "Enter port number [8000]: " custom_port if [[ -n "$custom_port" ]]; then PORT="$custom_port" # Update PORT in .env if it exists if [[ -f "${APP_DIR}/.env" ]]; then sed -i "s/^PORT=.*/PORT=${PORT}/" "${APP_DIR}/.env" sed -i "s|^OAUTH2_ISSUER=.*|OAUTH2_ISSUER=http://localhost:${PORT}|" "${APP_DIR}/.env" fi fi # Create systemd service file print_status "Creating systemd service file at $SERVICE_FILE..." # Create service file with proper variable expansion # We use a HEREDOC with single quotes to prevent expansion of $HOST and $PORT # but we need to expand RUN_USER, APP_DIR, VENV_DIR. We'll use a template approach. sudo tee "$SERVICE_FILE" > /dev/null << 'SERVICE_TEMPLATE' [Unit] Description=Mock API Service After=network.target Wants=network.target [Service] Type=simple User=RUN_USER_PLACEHOLDER Group=RUN_USER_PLACEHOLDER WorkingDirectory=APP_DIR_PLACEHOLDER Environment="PATH=VENV_DIR_PLACEHOLDER/bin" Environment="PYTHONPATH=APP_DIR_PLACEHOLDER" EnvironmentFile=APP_DIR_PLACEHOLDER/.env # HOST and PORT are read from .env file at runtime ExecStart=VENV_DIR_PLACEHOLDER/bin/waitress-serve --host=$HOST --port=$PORT wsgi:wsgi_app Restart=always RestartSec=10 StandardOutput=syslog StandardError=syslog SyslogIdentifier=SERVICE_NAME_PLACEHOLDER # Security hardening NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ReadWritePaths=APP_DIR_PLACEHOLDER [Install] WantedBy=multi-user.target SERVICE_TEMPLATE # Replace placeholders with actual values sudo sed -i "s|RUN_USER_PLACEHOLDER|$RUN_USER|g" "$SERVICE_FILE" sudo sed -i "s|APP_DIR_PLACEHOLDER|$APP_DIR|g" "$SERVICE_FILE" sudo sed -i "s|VENV_DIR_PLACEHOLDER|$VENV_DIR|g" "$SERVICE_FILE" sudo sed -i "s|SERVICE_NAME_PLACEHOLDER|$SERVICE_NAME|g" "$SERVICE_FILE" print_status "Service file created with dynamic PORT from .env" print_status "Systemd service file created." # Set ownership and permissions print_status "Setting ownership and permissions..." # Set ownership of app directory sudo chown -R "$RUN_USER:$RUN_USER" "$APP_DIR" # Make sure install script is executable sudo chmod +x "$APP_DIR/install.sh" # Reload systemd sudo systemctl daemon-reload print_status "Systemd service configured." } initialize_database() { print_status "Initializing database..." # Activate virtual environment source "${VENV_DIR}/bin/activate" # Get Python version for logging PYTHON_VERSION=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')") # First, ensure database directory exists DB_PATH=$(grep "^DATABASE_URL=" "${APP_DIR}/.env" 2>/dev/null | cut -d'=' -f2- | sed "s|sqlite+aiosqlite:///||" || echo "${APP_DIR}/mockapi.db") DB_DIR=$(dirname "$DB_PATH") if [[ ! -d "$DB_DIR" ]]; then mkdir -p "$DB_DIR" print_status "Created database directory: $DB_DIR" fi # Try to initialize database with Python script (with error handling) print_info "Attempting to create database tables..." python3 -c " import asyncio import sys import traceback sys.path.insert(0, '${APP_DIR}') try: # Try to import SQLAlchemy and app modules from app.core.database import engine, init_db from app.core.config import settings async def create_tables(): try: # Call the app's init_db function which handles table creation await init_db() print('Database tables created successfully via init_db()') except Exception as e: print(f'Error in init_db(): {e}') # Fallback: try direct table creation try: from app.core.database import Base async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) print('Database tables created successfully (fallback method)') except Exception as e2: print(f'Fallback also failed: {e2}') print('Tables will be created when application starts') asyncio.run(create_tables()) except ImportError as e: print(f'Import error (package compatibility issue): {e}') print('This may be due to Python ${PYTHON_VERSION} compatibility issues.') print('Database tables will be created when the application starts.') print('If the application fails to start, try:') print('1. Installing Python 3.11 or 3.12') print('2. Checking pip package versions') except Exception as e: print(f'Unexpected error during database initialization: {e}') print(traceback.format_exc()) print('Database tables will be created when the application starts.') " # Create empty database file if it doesn't exist (for SQLite) if [[ ! -f "$DB_PATH" ]]; then touch "$DB_PATH" print_status "Created empty database file: $DB_PATH" print_info "Tables will be created when application starts." fi deactivate print_status "Database initialization attempted. Tables will be created on application startup if not already present." } start_service() { print_status "Starting and enabling service..." # Enable and start the service sudo systemctl enable "$SERVICE_NAME" sudo systemctl start "$SERVICE_NAME" # Wait a moment for service to start sleep 5 # Check service status if sudo systemctl is-active --quiet "$SERVICE_NAME"; then print_status "Service is running successfully!" else print_error "Service failed to start. Checking logs..." sudo journalctl -u "$SERVICE_NAME" -n 20 --no-pager exit 1 fi } show_summary() { # Read credentials from .env for summary if [[ -f "${APP_DIR}/.env" ]]; then ADMIN_USERNAME=$(grep "^ADMIN_USERNAME=" "${APP_DIR}/.env" | cut -d'=' -f2) ADMIN_PASSWORD=$(grep "^ADMIN_PASSWORD=" "${APP_DIR}/.env" | cut -d'=' -f2) fi echo echo -e "${GREEN}========================================${NC}" echo -e "${GREEN} Installation Complete! ${NC}" echo -e "${GREEN}========================================${NC}" echo echo "Application Details:" echo " Directory: $APP_DIR" echo " Service: $SERVICE_NAME" echo " Run User: $RUN_USER" echo " Port: $PORT" echo " Virtual Env: $VENV_DIR" echo echo "Credentials (saved in ${APP_DIR}/.env):" echo " Admin Username: $ADMIN_USERNAME" echo " Admin Password: $ADMIN_PASSWORD" echo echo "Service Management Commands:" echo " sudo systemctl start $SERVICE_NAME" echo " sudo systemctl stop $SERVICE_NAME" echo " sudo systemctl restart $SERVICE_NAME" echo " sudo systemctl status $SERVICE_NAME" echo " sudo journalctl -u $SERVICE_NAME -f" echo echo "Access the application:" echo " Admin UI: http://localhost:$PORT/admin/login" echo " API Docs: http://localhost:$PORT/docs" echo " Health Check: http://localhost:$PORT/health" echo echo "Configuration Files:" echo " Environment: $APP_DIR/.env" echo " Service: $SERVICE_FILE" echo " Database: $(grep "^DATABASE_URL=" "${APP_DIR}/.env" | cut -d'/' -f4-)" echo print_warning "IMPORTANT: Change admin password after first login!" print_warning "Keep the .env file secure - it contains sensitive credentials." echo print_info "To update configuration, edit $APP_DIR/.env and restart:" print_info " sudo systemctl restart $SERVICE_NAME" echo print_info "To change port (e.g., from $PORT to 8080):" print_info " 1. Edit $APP_DIR/.env (update PORT, OAUTH2_ISSUER)" print_info " 2. Run: sudo systemctl restart $SERVICE_NAME" print_info " The service automatically uses PORT from .env file" echo } main() { echo -e "${GREEN}========================================${NC}" echo -e "${GREEN} Mock API Installation Script ${NC}" echo -e "${GREEN}========================================${NC}" echo # Check if running as root (warn but allow) check_root # Check if app directory exists if [[ ! -d "$APP_DIR" ]]; then print_error "Application directory not found: $APP_DIR" print_info "Please clone the repository first:" print_info " cd /opt/" print_info " sudo git clone https://git.sechpoint.app/customer-engineering/mockapi.git" print_info " sudo chown -R \$USER:\$USER /opt/mockapi" exit 1 fi # Check prerequisites check_prerequisites # Setup Python environment setup_environment # Configure application configure_application # Initialize database initialize_database # Setup systemd service setup_systemd_service # Start the service start_service # Show summary show_summary } # Run main function main