557 lines
No EOL
18 KiB
Bash
557 lines
No EOL
18 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"
|
|
}
|
|
|
|
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 |