Files
orion/scripts/seed/install.py
Samir Boulahtit 1b8a40f1ff
All checks were successful
CI / dependency-scanning (push) Successful in 27s
CI / docs (push) Successful in 35s
CI / ruff (push) Successful in 8s
CI / pytest (push) Successful in 34m22s
CI / validate (push) Successful in 19s
CI / deploy (push) Successful in 2m25s
feat(validators): add noqa suppression support to security and performance validators
- Add centralized _is_noqa_suppressed() to BaseValidator with normalization
  (accepts both SEC001 and SEC-001 formats for ruff compatibility)
- Wire noqa support into all 21 security and 18 performance check functions
- Add ruff external config for SEC/PERF/MOD/EXC codes in pyproject.toml
- Convert all 280 Python noqa comments to dashless format (ruff-compatible)
- Add site/ to IGNORE_PATTERNS (excludes mkdocs build output)
- Suppress 152 false positive findings (test passwords, seed data, validator
  self-references, Apple Wallet SHA1, etc.)
- Security: 79 errors → 0, 60 warnings → 0
- Performance: 80 warnings → 77 (3 test script suppressions)
- Add proposal doc with noqa inventory and remaining findings recommendations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 22:56:56 +01:00

591 lines
21 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Orion Platform Installation Script
This script handles first-time installation of the Orion platform:
1. Validates Python version and dependencies
2. Checks environment configuration (.env file)
3. Validates required settings for production
4. Runs database migrations
5. Initializes essential data (admin, settings, CMS, email templates)
6. Provides a configuration status report
Usage:
make platform-install
# or directly:
python scripts/install.py
This script is idempotent - safe to run multiple times.
"""
import subprocess
import sys
from pathlib import Path
# Add project root to path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
# =============================================================================
# CONSOLE OUTPUT HELPERS
# =============================================================================
class Colors:
"""ANSI color codes for terminal output."""
HEADER = "\033[95m"
BLUE = "\033[94m"
CYAN = "\033[96m"
GREEN = "\033[92m"
WARNING = "\033[93m"
FAIL = "\033[91m"
ENDC = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"
def print_header(text: str):
"""Print a bold header."""
print(f"\n{Colors.BOLD}{Colors.HEADER}{'=' * 70}{Colors.ENDC}")
print(f"{Colors.BOLD}{Colors.HEADER} {text}{Colors.ENDC}")
print(f"{Colors.BOLD}{Colors.HEADER}{'=' * 70}{Colors.ENDC}")
def print_section(text: str):
"""Print a section header."""
print(f"\n{Colors.BOLD}{Colors.CYAN}[*] {text}{Colors.ENDC}")
def print_step(step: int, total: int, text: str):
"""Print a step indicator."""
print(f"\n{Colors.BLUE}[{step}/{total}] {text}{Colors.ENDC}")
def print_success(text: str):
"""Print success message."""
print(f" {Colors.GREEN}{Colors.ENDC} {text}")
def print_warning(text: str):
"""Print warning message."""
print(f" {Colors.WARNING}{Colors.ENDC} {text}")
def print_error(text: str):
"""Print error message."""
print(f" {Colors.FAIL}{Colors.ENDC} {text}")
def print_info(text: str):
"""Print info message."""
print(f" {Colors.DIM}{Colors.ENDC} {text}")
# =============================================================================
# VALIDATION FUNCTIONS
# =============================================================================
def check_python_version() -> bool:
"""Check if Python version is supported."""
major, minor = sys.version_info[:2]
if major < 3 or (major == 3 and minor < 11):
print_error(f"Python 3.11+ required. Found: {major}.{minor}")
return False
print_success(f"Python version: {major}.{minor}")
return True
def check_env_file() -> tuple[bool, dict]:
"""Check if .env file exists and load it."""
env_path = project_root / ".env"
env_example_path = project_root / ".env.example"
if not env_path.exists():
if env_example_path.exists():
print_warning(".env file not found")
print_info("Copy .env.example to .env and configure it:")
print_info(" cp .env.example .env")
return False, {}
print_warning("Neither .env nor .env.example found")
return False, {}
print_success(".env file found")
# Load .env manually to check values
env_vars = {}
with open(env_path) as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and "=" in line:
key, _, value = line.partition("=")
# Remove quotes if present
value = value.strip().strip("'\"")
env_vars[key.strip()] = value
return True, env_vars
def validate_configuration(env_vars: dict) -> dict:
"""
Validate configuration and return status for each category.
Returns dict with categories and their status:
{
"category": {
"status": "ok" | "warning" | "missing",
"message": "...",
"items": [...]
}
}
"""
results = {}
# -------------------------------------------------------------------------
# Database
# -------------------------------------------------------------------------
db_url = env_vars.get("DATABASE_URL", "")
if db_url and "sqlite" not in db_url.lower():
results["database"] = {
"status": "ok",
"message": "Production database configured",
"items": [f"URL: {db_url[:50]}..."]
}
elif db_url and "sqlite" in db_url.lower():
results["database"] = {
"status": "warning",
"message": "SQLite database (OK for development)",
"items": ["Consider PostgreSQL for production"]
}
else:
results["database"] = {
"status": "ok",
"message": "Using default SQLite database",
"items": ["Configure DATABASE_URL for production"]
}
# -------------------------------------------------------------------------
# Security (JWT)
# -------------------------------------------------------------------------
jwt_key = env_vars.get("JWT_SECRET_KEY", "")
if jwt_key and jwt_key != "change-this-in-production" and len(jwt_key) >= 32:
results["security"] = {
"status": "ok",
"message": "JWT secret configured",
"items": ["Secret key is set and sufficiently long"]
}
elif jwt_key and jwt_key == "change-this-in-production":
results["security"] = {
"status": "warning",
"message": "Using default JWT secret",
"items": [
"CRITICAL: Change JWT_SECRET_KEY for production!",
"Use: openssl rand -hex 32"
]
}
else:
results["security"] = {
"status": "warning",
"message": "JWT secret not explicitly set",
"items": ["Set JWT_SECRET_KEY in .env"]
}
# -------------------------------------------------------------------------
# Admin Credentials
# -------------------------------------------------------------------------
admin_pass = env_vars.get("ADMIN_PASSWORD", "admin123")
admin_email = env_vars.get("ADMIN_EMAIL", "admin@orion.lu")
if admin_pass != "admin123":
results["admin"] = {
"status": "ok",
"message": "Admin credentials configured",
"items": [f"Email: {admin_email}"]
}
else:
results["admin"] = {
"status": "warning",
"message": "Using default admin password",
"items": [
"Set ADMIN_PASSWORD in .env",
"Change immediately after first login"
]
}
# -------------------------------------------------------------------------
# Stripe Billing
# -------------------------------------------------------------------------
stripe_secret = env_vars.get("STRIPE_SECRET_KEY", "")
stripe_pub = env_vars.get("STRIPE_PUBLISHABLE_KEY", "")
stripe_webhook = env_vars.get("STRIPE_WEBHOOK_SECRET", "")
if stripe_secret and stripe_pub:
if stripe_secret.startswith("sk_live_"):
results["stripe"] = {
"status": "ok",
"message": "Stripe LIVE mode configured",
"items": [
"Live secret key set",
f"Webhook secret: {'configured' if stripe_webhook else 'NOT SET'}"
]
}
elif stripe_secret.startswith("sk_test_"):
results["stripe"] = {
"status": "warning",
"message": "Stripe TEST mode configured",
"items": [
"Using test keys (OK for development)",
"Switch to live keys for production"
]
}
else:
results["stripe"] = {
"status": "warning",
"message": "Stripe keys set but format unclear",
"items": ["Verify STRIPE_SECRET_KEY format"]
}
else:
results["stripe"] = {
"status": "missing",
"message": "Stripe not configured",
"items": [
"Set STRIPE_SECRET_KEY",
"Set STRIPE_PUBLISHABLE_KEY",
"Set STRIPE_WEBHOOK_SECRET",
"Billing features will not work!"
]
}
# -------------------------------------------------------------------------
# Email Configuration
# -------------------------------------------------------------------------
email_provider = env_vars.get("EMAIL_PROVIDER", "smtp")
email_enabled = env_vars.get("EMAIL_ENABLED", "true").lower() == "true"
email_debug = env_vars.get("EMAIL_DEBUG", "false").lower() == "true"
if not email_enabled:
results["email"] = {
"status": "warning",
"message": "Email disabled",
"items": ["EMAIL_ENABLED=false - no emails will be sent"]
}
elif email_debug:
results["email"] = {
"status": "warning",
"message": "Email in debug mode",
"items": ["EMAIL_DEBUG=true - emails logged, not sent"]
}
elif email_provider == "smtp":
smtp_host = env_vars.get("SMTP_HOST", "localhost")
smtp_user = env_vars.get("SMTP_USER", "")
if smtp_host != "localhost" and smtp_user:
results["email"] = {
"status": "ok",
"message": f"SMTP configured ({smtp_host})",
"items": [f"User: {smtp_user}"]
}
else:
results["email"] = {
"status": "warning",
"message": "SMTP using defaults",
"items": [
"Configure SMTP_HOST, SMTP_USER, SMTP_PASSWORD",
"Or use EMAIL_DEBUG=true for development"
]
}
elif email_provider == "sendgrid":
api_key = env_vars.get("SENDGRID_API_KEY", "")
if api_key:
results["email"] = {
"status": "ok",
"message": "SendGrid configured",
"items": ["API key set"]
}
else:
results["email"] = {
"status": "missing",
"message": "SendGrid selected but not configured",
"items": ["Set SENDGRID_API_KEY"]
}
elif email_provider == "mailgun":
api_key = env_vars.get("MAILGUN_API_KEY", "")
domain = env_vars.get("MAILGUN_DOMAIN", "")
if api_key and domain:
results["email"] = {
"status": "ok",
"message": f"Mailgun configured ({domain})",
"items": ["API key and domain set"]
}
else:
results["email"] = {
"status": "missing",
"message": "Mailgun selected but not configured",
"items": ["Set MAILGUN_API_KEY and MAILGUN_DOMAIN"]
}
elif email_provider == "ses":
access_key = env_vars.get("AWS_ACCESS_KEY_ID", "")
secret_key = env_vars.get("AWS_SECRET_ACCESS_KEY", "")
if access_key and secret_key:
results["email"] = {
"status": "ok",
"message": "Amazon SES configured",
"items": [f"Region: {env_vars.get('AWS_REGION', 'eu-west-1')}"]
}
else:
results["email"] = {
"status": "missing",
"message": "SES selected but not configured",
"items": ["Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY"]
}
# -------------------------------------------------------------------------
# Platform Domain
# -------------------------------------------------------------------------
domain = env_vars.get("PLATFORM_DOMAIN", "orion.lu")
if domain != "orion.lu":
results["domain"] = {
"status": "ok",
"message": f"Custom domain: {domain}",
"items": []
}
else:
results["domain"] = {
"status": "warning",
"message": "Using default domain",
"items": ["Set PLATFORM_DOMAIN for your deployment"]
}
# -------------------------------------------------------------------------
# Celery / Redis Task Queue
# -------------------------------------------------------------------------
redis_url = env_vars.get("REDIS_URL", "")
use_celery = env_vars.get("USE_CELERY", "false").lower() == "true"
flower_url = env_vars.get("FLOWER_URL", "")
flower_password = env_vars.get("FLOWER_PASSWORD", "")
if use_celery:
if redis_url:
celery_items = [f"Redis: {redis_url.split('@')[-1] if '@' in redis_url else redis_url}"]
if flower_url:
celery_items.append(f"Flower: {flower_url}")
else:
celery_items.append("FLOWER_URL not set (monitoring disabled)")
if flower_password and flower_password != "changeme":
celery_items.append("Flower password configured")
elif flower_password == "changeme":
celery_items.append("WARNING: Change FLOWER_PASSWORD for production!")
results["celery"] = {
"status": "ok",
"message": "Celery enabled with Redis",
"items": celery_items
}
else:
results["celery"] = {
"status": "missing",
"message": "Celery enabled but Redis not configured",
"items": [
"Set REDIS_URL (e.g., redis://localhost:6379/0)",
"Or disable Celery: USE_CELERY=false"
]
}
else:
results["celery"] = {
"status": "warning",
"message": "Celery disabled (using FastAPI BackgroundTasks)",
"items": [
"Set USE_CELERY=true for production",
"Requires Redis: docker-compose up -d redis"
]
}
return results
def print_configuration_report(config_status: dict):
"""Print a formatted configuration status report."""
print_section("Configuration Status")
ok_count = 0
warning_count = 0
missing_count = 0
for category, status in config_status.items():
if status["status"] == "ok":
icon = f"{Colors.GREEN}{Colors.ENDC}"
ok_count += 1
elif status["status"] == "warning":
icon = f"{Colors.WARNING}{Colors.ENDC}"
warning_count += 1
else:
icon = f"{Colors.FAIL}{Colors.ENDC}"
missing_count += 1
print(f"\n {icon} {Colors.BOLD}{category.upper()}{Colors.ENDC}: {status['message']}")
for item in status.get("items", []):
print(f" {Colors.DIM}{item}{Colors.ENDC}")
print(f"\n {Colors.DIM}" * 35 + Colors.ENDC)
print(f" Summary: {Colors.GREEN}{ok_count} OK{Colors.ENDC}, "
f"{Colors.WARNING}{warning_count} warnings{Colors.ENDC}, "
f"{Colors.FAIL}{missing_count} missing{Colors.ENDC}")
return ok_count, warning_count, missing_count
# =============================================================================
# INSTALLATION STEPS
# =============================================================================
def run_migrations() -> bool:
"""Run database migrations."""
try:
result = subprocess.run(
[sys.executable, "-m", "alembic", "upgrade", "head"],
cwd=project_root,
capture_output=True,
text=True
)
if result.returncode == 0:
print_success("Migrations completed successfully")
return True
print_error("Migration failed")
if result.stderr:
print_info(result.stderr[:500])
return False
except Exception as e:
print_error(f"Failed to run migrations: {e}")
return False
def run_init_script(script_name: str, description: str) -> bool:
"""Run an initialization script."""
script_path = project_root / "scripts" / script_name
try:
result = subprocess.run(
[sys.executable, str(script_path)],
cwd=project_root,
capture_output=True,
text=True
)
if result.returncode == 0:
print_success(description)
return True
print_error(f"Failed: {description}")
if result.stderr:
print_info(result.stderr[:300])
return False
except Exception as e:
print_error(f"Error running {script_name}: {e}")
return False
# =============================================================================
# MAIN INSTALLATION FLOW
# =============================================================================
def main():
"""Main installation flow."""
print_header("ORION PLATFORM INSTALLATION")
# -------------------------------------------------------------------------
# Step 1: Pre-flight checks
# -------------------------------------------------------------------------
print_step(1, 4, "Pre-flight checks")
if not check_python_version():
sys.exit(1)
env_exists, env_vars = check_env_file()
if not env_exists:
print()
print_warning("Installation can continue with defaults,")
print_warning("but you should configure .env before going to production.")
print()
response = input("Continue with default configuration? [y/N]: ")
if response.lower() != "y":
print("\nInstallation cancelled. Please configure .env first.")
sys.exit(0)
env_vars = {}
# -------------------------------------------------------------------------
# Step 2: Validate configuration
# -------------------------------------------------------------------------
print_step(2, 4, "Validating configuration")
config_status = validate_configuration(env_vars)
ok_count, warning_count, missing_count = print_configuration_report(config_status)
if missing_count > 0:
print()
print_warning(f"{missing_count} critical configuration(s) missing!")
print_warning("The platform may not function correctly.")
response = input("\nContinue anyway? [y/N]: ")
if response.lower() != "y":
print("\nInstallation cancelled. Please fix configuration first.")
sys.exit(0)
# -------------------------------------------------------------------------
# Step 3: Database setup
# -------------------------------------------------------------------------
print_step(3, 4, "Database setup")
print_info("Running database migrations...")
if not run_migrations():
print_error("Failed to run migrations. Cannot continue.")
sys.exit(1)
# -------------------------------------------------------------------------
# Step 4: Initialize platform data
# -------------------------------------------------------------------------
print_step(4, 4, "Initializing platform data")
init_scripts = [
("init_production.py", "Admin user and platform settings"),
("init_log_settings.py", "Log settings"),
("create_default_content_pages.py", "Default CMS pages (all platforms)"),
("seed_email_templates.py", "Email templates"),
]
all_success = True
for script, description in init_scripts:
if not run_init_script(script, description):
all_success = False
# -------------------------------------------------------------------------
# Summary
# -------------------------------------------------------------------------
print_header("INSTALLATION COMPLETE")
if all_success:
print(f"\n {Colors.GREEN}✓ Platform installed successfully!{Colors.ENDC}")
else:
print(f"\n {Colors.WARNING}⚠ Installation completed with some errors{Colors.ENDC}")
print(f"\n {Colors.BOLD}Next Steps:{Colors.ENDC}")
print(" 1. Review any warnings above")
if warning_count > 0 or missing_count > 0:
print(" 2. Update .env with production values")
print(" 3. Run 'make platform-install' again to verify")
else:
print(" 2. Start the application: make dev")
print(f"\n {Colors.BOLD}Admin Login:{Colors.ENDC}")
admin_email = env_vars.get("ADMIN_EMAIL", "admin@orion.lu")
print(" URL: /admin/login")
print(f" Email: {admin_email}")
print(f" Password: {'(configured in .env)' if env_vars.get('ADMIN_PASSWORD') else 'admin123'}") # noqa: SEC021
if not env_vars.get("ADMIN_PASSWORD"):
print(f"\n {Colors.WARNING}⚠ CHANGE DEFAULT PASSWORD IMMEDIATELY!{Colors.ENDC}") # noqa: SEC021
print(f"\n {Colors.BOLD}For demo data (development only):{Colors.ENDC}")
print(" make seed-demo")
print()
if __name__ == "__main__":
main()