mockapi/install.sh
cclohmar 80e8d93a27 fix: ensure required env variables are present in .env
- Add ensure_env_variables function to install missing HOST, PORT, LOG_LEVEL, OAUTH2_ISSUER
- Call function when using existing .env file
- Clean up temporary fix scripts and test files
- Update install.sh with better validation and error handling
2026-03-16 18:58:00 +00:00

593 lines
No EOL
19 KiB
Bash

#!/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"
}
ensure_env_variables() {
print_info "Ensuring required environment variables exist..."
local env_file="${APP_DIR}/.env"
if [[ ! -f "$env_file" ]]; then
return
fi
# Ensure HOST exists
if ! grep -q "^HOST=" "$env_file"; then
echo "HOST=0.0.0.0" >> "$env_file"
print_info "Added missing HOST to .env"
fi
# Ensure PORT exists
if ! grep -q "^PORT=" "$env_file"; then
echo "PORT=8000" >> "$env_file"
print_info "Added missing PORT to .env"
fi
# Ensure LOG_LEVEL exists
if ! grep -q "^LOG_LEVEL=" "$env_file"; then
echo "LOG_LEVEL=INFO" >> "$env_file"
print_info "Added missing LOG_LEVEL to .env"
fi
# Ensure OAUTH2_ISSUER exists
if ! grep -q "^OAUTH2_ISSUER=" "$env_file"; then
# Try to get PORT from .env or default
local port_value="8000"
if grep -q "^PORT=" "$env_file"; then
port_value=$(grep "^PORT=" "$env_file" | cut -d'=' -f2)
fi
echo "OAUTH2_ISSUER=http://localhost:$port_value" >> "$env_file"
print_info "Added missing OAUTH2_ISSUER to .env"
fi
}
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."
ensure_env_variables
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
ensure_env_variables
# 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