mockapi/fix_vps_complete.sh
2026-03-16 18:05:02 +00:00

425 lines
No EOL
15 KiB
Bash

#!/bin/bash
# Complete fix script for MockAPI VPS deployment issues
# Run this on the VPS as the same user who installed MockAPI
set -e
echo "=== MockAPI VPS Complete Fix Script ==="
echo
APP_DIR="/opt/mockapi"
ENV_FILE="${APP_DIR}/.env"
CONFIG_FILE="${APP_DIR}/app/core/config.py"
APP_FILE="${APP_DIR}/app/core/app.py"
# Function to print colored output
print_status() {
echo -e "\033[0;32m[+]\033[0m $1"
}
print_warning() {
echo -e "\033[1;33m[!]\033[0m $1"
}
print_error() {
echo -e "\033[0;31m[!]\033[0m $1"
}
# Step 0: Check we're in the right place
print_status "Step 0: Checking environment"
if [[ ! -d "$APP_DIR" ]]; then
print_error "Application directory not found: $APP_DIR"
exit 1
fi
if [[ ! -f "$CONFIG_FILE" ]]; then
print_error "Config file not found: $CONFIG_FILE"
exit 1
fi
cd "$APP_DIR"
# Step 1: Backup original files
print_status "Step 1: Backing up original files"
BACKUP_DIR="${APP_DIR}/backup_$(date +%s)"
mkdir -p "$BACKUP_DIR"
cp "$CONFIG_FILE" "$BACKUP_DIR/config.py.backup"
cp "$APP_FILE" "$BACKUP_DIR/app.py.backup"
if [[ -f "$ENV_FILE" ]]; then
cp "$ENV_FILE" "$BACKUP_DIR/env.backup"
fi
print_status "Backups created in: $BACKUP_DIR"
# Step 2: Fix config.py - add missing fields and allow extra fields
print_status "Step 2: Fixing config.py"
# Check if log_level field already exists
if ! grep -q "log_level:" "$CONFIG_FILE"; then
print_status "Adding log_level, host, and port fields to config.py"
# Insert after the version field line
sed -i '/version: str = "1.0.0"/a\ log_level: str = "INFO"\n host: str = "0.0.0.0"\n port: int = 8000' "$CONFIG_FILE"
else
print_status "log_level field already exists"
fi
# Ensure model_config allows extra fields
if grep -q 'model_config = ConfigDict(env_file=".env")' "$CONFIG_FILE"; then
print_status "Updating model_config to allow extra fields"
sed -i 's/model_config = ConfigDict(env_file=".env")/model_config = ConfigDict(env_file=".env", extra="allow")/' "$CONFIG_FILE"
elif grep -q 'extra="allow"' "$CONFIG_FILE"; then
print_status "model_config already allows extra fields"
else
print_warning "Could not find model_config line to update"
fi
# Step 3: Fix app.py - update logging configuration
print_status "Step 3: Fixing app.py logging configuration"
# Check if log_level_map already exists
if ! grep -q "log_level_map" "$APP_FILE"; then
print_status "Updating logging configuration in app.py"
# Create a temporary file with the new logging config
cat > /tmp/new_logging_config.py << 'EOF'
# Convert log level string to logging constant
log_level_map = {
"DEBUG": logging.DEBUG,
"INFO": logging.INFO,
"WARNING": logging.WARNING,
"ERROR": logging.ERROR,
"CRITICAL": logging.CRITICAL,
}
log_level = log_level_map.get(settings.log_level.upper(), logging.INFO)
logging.basicConfig(
level=logging.DEBUG if settings.debug else log_level,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
logger = logging.getLogger(__name__)
EOF
# Replace the old logging.basicConfig block
# Find the line numbers of the old block
START_LINE=$(grep -n "logging.basicConfig" "$APP_FILE" | head -1 | cut -d: -f1)
if [[ -n "$START_LINE" ]]; then
# Find the next line after the closing parenthesis
END_LINE=$((START_LINE + 3))
# Check if we have the right pattern
if sed -n "${START_LINE},${END_LINE}p" "$APP_FILE" | grep -q "logger = logging.getLogger"; then
# Replace lines START_LINE to END_LINE with new config
sed -i "${START_LINE},${END_LINE}d" "$APP_FILE"
sed -i "${START_LINE}r /tmp/new_logging_config.py" "$APP_FILE"
print_status "Updated logging configuration"
else
print_warning "Could not find exact logging block pattern, attempting manual replacement"
# Fallback: replace the whole block we can identify
sed -i '/logging.basicConfig/,/^logger = logging.getLogger/d' "$APP_FILE"
sed -i "/import logging/a\\
# Convert log level string to logging constant\\
log_level_map = {\\
\"DEBUG\": logging.DEBUG,\\
\"INFO\": logging.INFO,\\
\"WARNING\": logging.WARNING,\\
\"ERROR\": logging.ERROR,\\
\"CRITICAL\": logging.CRITICAL,\\
}\\
log_level = log_level_map.get(settings.log_level.upper(), logging.INFO)\\
\\
logging.basicConfig(\\
level=logging.DEBUG if settings.debug else log_level,\\
format=\"%(asctime)s - %(name)s - %(levelname)s - %(message)s\",\\
)\\
logger = logging.getLogger(__name__)" "$APP_FILE"
fi
else
print_error "Could not find logging.basicConfig in app.py"
fi
rm /tmp/new_logging_config.py
else
print_status "Logging configuration already updated"
fi
# Step 4: Fix .env file format issues
print_status "Step 4: Fixing .env file format issues"
if [[ -f "$ENV_FILE" ]]; then
# Fix OAUTH2_SUPPORTED_SCOPES to proper JSON format
SCOPES_LINE=$(grep -i "^OAUTH2_SUPPORTED_SCOPES=" "$ENV_FILE" | head -1)
if [[ -n "$SCOPES_LINE" ]]; then
SCOPES_VALUE=$(echo "$SCOPES_LINE" | cut -d'=' -f2-)
# Check if value is valid JSON array
if ! python3 -c "import json; json.loads('$SCOPES_VALUE')" 2>/dev/null; then
print_warning "OAUTH2_SUPPORTED_SCOPES value is not valid JSON: $SCOPES_VALUE"
print_status "Converting to JSON array format"
# Convert space-separated or comma-separated to JSON array
CLEAN_VALUE=$(echo "$SCOPES_VALUE" | sed 's/"//g' | sed "s/'//g")
JSON_ARRAY=$(echo "$CLEAN_VALUE" | python3 -c "
import sys, json
value = sys.stdin.read().strip()
if ',' in value:
items = [item.strip() for item in value.split(',')]
else:
items = [item.strip() for item in value.split()]
print(json.dumps([item for item in items if item]))
")
# Update .env file
sed -i "s/^OAUTH2_SUPPORTED_SCOPES=.*$/OAUTH2_SUPPORTED_SCOPES=$JSON_ARRAY/" "$ENV_FILE"
print_status "Updated OAUTH2_SUPPORTED_SCOPES to: $JSON_ARRAY"
else
print_status "OAUTH2_SUPPORTED_SCOPES already in valid JSON format"
fi
fi
# Fix OAUTH2_SUPPORTED_GRANT_TYPES to proper JSON format
GRANT_LINE=$(grep -i "^OAUTH2_SUPPORTED_GRANT_TYPES=" "$ENV_FILE" | head -1)
if [[ -n "$GRANT_LINE" ]]; then
GRANT_VALUE=$(echo "$GRANT_LINE" | cut -d'=' -f2-)
# Check if value is valid JSON array
if ! python3 -c "import json; json.loads('$GRANT_VALUE')" 2>/dev/null; then
print_warning "OAUTH2_SUPPORTED_GRANT_TYPES value is not valid JSON: $GRANT_VALUE"
print_status "Converting to JSON array format"
# Convert space-separated or comma-separated to JSON array
CLEAN_VALUE=$(echo "$GRANT_VALUE" | sed 's/"//g' | sed "s/'//g")
JSON_ARRAY=$(echo "$CLEAN_VALUE" | python3 -c "
import sys, json
value = sys.stdin.read().strip()
if ',' in value:
items = [item.strip() for item in value.split(',')]
else:
items = [item.strip() for item in value.split()]
print(json.dumps([item for item in items if item]))
")
# Update .env file
sed -i "s/^OAUTH2_SUPPORTED_GRANT_TYPES=.*$/OAUTH2_SUPPORTED_GRANT_TYPES=$JSON_ARRAY/" "$ENV_FILE"
print_status "Updated OAUTH2_SUPPORTED_GRANT_TYPES to: $JSON_ARRAY"
else
print_status "OAUTH2_SUPPORTED_GRANT_TYPES already in valid JSON format"
fi
fi
# Remove duplicate entries
print_status "Checking for duplicate entries in .env"
for FIELD in OAUTH2_SUPPORTED_SCOPES OAUTH2_SUPPORTED_GRANT_TYPES; do
COUNT=$(grep -i "^${FIELD}=" "$ENV_FILE" | wc -l)
if [[ $COUNT -gt 1 ]]; then
print_warning "Found $COUNT entries for $FIELD, keeping only the first"
FIRST_LINE=$(grep -i -n "^${FIELD}=" "$ENV_FILE" | head -1 | cut -d: -f1)
# Create temp file without any of this field
grep -v -i "^${FIELD}=" "$ENV_FILE" > "${ENV_FILE}.tmp"
# Add back the first occurrence
grep -i "^${FIELD}=" "$ENV_FILE" | head -1 >> "${ENV_FILE}.tmp"
mv "${ENV_FILE}.tmp" "$ENV_FILE"
fi
done
# Ensure required fields exist
print_status "Ensuring all required fields exist in .env"
if ! grep -q "^LOG_LEVEL=" "$ENV_FILE"; then
echo "LOG_LEVEL=INFO" >> "$ENV_FILE"
print_status "Added LOG_LEVEL=INFO"
fi
if ! grep -q "^HOST=" "$ENV_FILE"; then
echo "HOST=0.0.0.0" >> "$ENV_FILE"
print_status "Added HOST=0.0.0.0"
fi
if ! grep -q "^PORT=" "$ENV_FILE"; then
echo "PORT=8000" >> "$ENV_FILE"
print_status "Added PORT=8000"
fi
if ! grep -q "^OAUTH2_AUTHORIZATION_CODE_EXPIRE_MINUTES=" "$ENV_FILE"; then
echo "OAUTH2_AUTHORIZATION_CODE_EXPIRE_MINUTES=10" >> "$ENV_FILE"
print_status "Added OAUTH2_AUTHORIZATION_CODE_EXPIRE_MINUTES=10"
fi
if ! grep -q "^OAUTH2_PKCE_REQUIRED=" "$ENV_FILE"; then
echo "OAUTH2_PKCE_REQUIRED=false" >> "$ENV_FILE"
print_status "Added OAUTH2_PKCE_REQUIRED=false"
fi
# Update OAUTH2_ISSUER if port has changed
CURRENT_PORT=$(grep "^PORT=" "$ENV_FILE" | cut -d'=' -f2)
CURRENT_ISSUER=$(grep "^OAUTH2_ISSUER=" "$ENV_FILE" | cut -d'=' -f2)
EXPECTED_ISSUER="http://localhost:${CURRENT_PORT}"
if [[ "$CURRENT_ISSUER" != "$EXPECTED_ISSUER" ]]; then
print_status "Updating OAUTH2_ISSUER to match port"
sed -i "s|^OAUTH2_ISSUER=.*|OAUTH2_ISSUER=$EXPECTED_ISSUER|" "$ENV_FILE"
fi
else
print_warning ".env file not found, skipping .env fixes"
fi
# Step 5: Test the configuration
print_status "Step 5: Testing configuration import"
cat > test_config.py << 'EOF'
import os
import sys
import json
# Add app directory to path
sys.path.insert(0, os.getcwd())
try:
print("Attempting to import config...")
from app.core.config import settings
print("✓ Config import successful!")
print(f" DEBUG: {settings.debug}")
print(f" LOG_LEVEL: {settings.log_level}")
print(f" HOST: {settings.host}")
print(f" PORT: {settings.port}")
print(f" OAUTH2_SUPPORTED_SCOPES: {settings.oauth2_supported_scopes}")
print(f" OAUTH2_SUPPORTED_GRANT_TYPES: {settings.oauth2_supported_grant_types}")
print(f" Type of scopes: {type(settings.oauth2_supported_scopes)}")
print(f" Type of grant types: {type(settings.oauth2_supported_grant_types)}")
# Test JSON serialization
print(f" Scopes as JSON: {json.dumps(settings.oauth2_supported_scopes)}")
print("✓ All configuration tests passed!")
except Exception as e:
print(f"✗ Config import failed: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
EOF
if python3 test_config.py; then
print_status "Configuration test passed!"
rm test_config.py
else
print_error "Configuration test failed"
print_warning "Check the error above. The .env file may still have issues."
exit 1
fi
# Step 5.5: Fix service file port issue
print_status "Step 5.5: Fixing service file port configuration"
SERVICE_FILE="/etc/systemd/system/mockapi.service"
if [[ -f "$SERVICE_FILE" ]]; then
print_status "Checking service file for port configuration issues..."
# Check if service file has the escaped \$PORT issue
if grep -q "\\\\\$PORT" "$SERVICE_FILE" || grep -q "--port=\\\$PORT" "$SERVICE_FILE"; then
print_warning "Found escaped \$PORT in service file. Fixing..."
# Read port from .env
SERVICE_PORT=$(grep "^PORT=" "$ENV_FILE" | cut -d'=' -f2 || echo "8000")
SERVICE_HOST=$(grep "^HOST=" "$ENV_FILE" | cut -d'=' -f2 || echo "0.0.0.0")
# Validate port
if [[ ! "$SERVICE_PORT" =~ ^[0-9]+$ ]]; then
print_warning "PORT '$SERVICE_PORT' is not numeric. Using 8000."
SERVICE_PORT="8000"
sed -i "s/^PORT=.*/PORT=8000/" "$ENV_FILE"
fi
print_status "Updating service file with hardcoded port: $SERVICE_PORT"
# Backup service file
SERVICE_BACKUP="${SERVICE_FILE}.backup.$(date +%s)"
sudo cp "$SERVICE_FILE" "$SERVICE_BACKUP"
print_status "Service file backed up to: $SERVICE_BACKUP"
# Get VENV_DIR from current service file
VENV_DIR=$(grep 'Environment="PATH=' "$SERVICE_FILE" | cut -d'=' -f2 | cut -d':' -f1 | sed 's/"//g' || echo "/opt/mockapi/venv")
# Create fixed service file
sudo tee "$SERVICE_FILE" > /dev/null << EOF
[Unit]
Description=Mock API Service
After=network.target
Wants=network.target
[Service]
Type=simple
User=$(grep "^User=" "$SERVICE_FILE" | cut -d'=' -f2)
Group=$(grep "^Group=" "$SERVICE_FILE" | cut -d'=' -f2)
WorkingDirectory=$APP_DIR
Environment="PATH=$VENV_DIR/bin"
Environment="PYTHONPATH=$APP_DIR"
EnvironmentFile=$APP_DIR/.env
# Fixed: Hardcoded port from .env
ExecStart=$VENV_DIR/bin/waitress-serve --host=$SERVICE_HOST --port=$SERVICE_PORT wsgi:wsgi_app
Restart=always
RestartSec=10
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=mockapi
# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=$APP_DIR
[Install]
WantedBy=multi-user.target
EOF
print_status "Service file updated with hardcoded port $SERVICE_PORT."
sudo systemctl daemon-reload
print_status "Systemd daemon reloaded."
else
print_status "Service file port configuration looks correct."
fi
else
print_warning "Service file not found: $SERVICE_FILE"
print_warning "Service won't be fixed. Install service first with install.sh."
fi
# Step 6: Restart the service
print_status "Step 6: Restarting MockAPI service"
sudo systemctl restart mockapi
sleep 5
if sudo systemctl is-active --quiet mockapi; then
print_status "✓ Service is running successfully!"
echo
echo "Service status:"
sudo systemctl status mockapi --no-pager -l
echo
print_status "Testing API endpoints..."
# Read port from .env for testing
TEST_PORT=$(grep "^PORT=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2 || echo "8000")
if [[ ! "$TEST_PORT" =~ ^[0-9]+$ ]]; then
TEST_PORT="8000"
fi
if curl -f -s http://localhost:$TEST_PORT/health > /dev/null 2>&1; then
print_status "✓ Health check passed: http://localhost:$TEST_PORT/health"
echo
echo "Access your MockAPI:"
echo " Admin UI: http://localhost:$TEST_PORT/admin/login"
echo " API Docs: http://localhost:$TEST_PORT/docs"
echo " Health check: http://localhost:$TEST_PORT/health"
else
print_warning "Health check failed. Checking logs..."
sudo journalctl -u mockapi -n 10 --no-pager
fi
else
print_error "Service failed to start"
echo
print_warning "Checking service logs..."
sudo journalctl -u mockapi -n 30 --no-pager
echo
print_warning "If the service still fails, check:"
print_warning "1. The .env file format: ${ENV_FILE}"
print_warning "2. File permissions in ${APP_DIR}"
print_warning "3. Database file permissions"
exit 1
fi
echo
print_status "=== Fix completed successfully! ==="
echo
echo "Backup files are in: $BACKUP_DIR"
echo "If you encounter issues, you can restore from backup."
echo