| """ |
| Service for handling first-run setup operations. |
| """ |
|
|
| import logging |
| import os |
| import subprocess |
| import sys |
| from io import StringIO |
|
|
| from django.conf import settings |
| from django.utils import timezone |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| class SetupService: |
| """ |
| Handles initial setup operations including migrations, |
| default environment creation, and setup completion tracking. |
| """ |
|
|
| @classmethod |
| def is_setup_needed(cls) -> bool: |
| """ |
| Check if initial setup is required. |
| |
| Returns True if: |
| - GlobalSettings.setup_completed is False, OR |
| - No default Environment exists |
| """ |
| try: |
| from core.models import GlobalSettings, Environment |
|
|
| |
| settings_obj = GlobalSettings.get_settings() |
| if not settings_obj.setup_completed: |
| return True |
|
|
| |
| if not Environment.objects.filter(is_default=True).exists(): |
| return True |
|
|
| return False |
| except Exception as e: |
| |
| logger.debug(f"Setup check failed: {e}") |
| return True |
|
|
| @classmethod |
| def get_status(cls) -> dict: |
| """ |
| Get detailed status of setup components. |
| |
| Returns dict with status of each component. |
| """ |
| status = { |
| "database_ready": False, |
| "migrations_pending": True, |
| "default_env_exists": False, |
| "setup_completed": False, |
| "errors": [], |
| } |
|
|
| try: |
| |
| from django.db import connection |
| with connection.cursor() as cursor: |
| cursor.execute( |
| "SELECT name FROM sqlite_master WHERE type='table' AND name='global_settings'" |
| ) |
| status["database_ready"] = cursor.fetchone() is not None |
| except Exception as e: |
| status["errors"].append(f"Database check failed: {e}") |
|
|
| |
| try: |
| from django.db.migrations.executor import MigrationExecutor |
| from django.db import connection |
| executor = MigrationExecutor(connection) |
| plan = executor.migration_plan(executor.loader.graph.leaf_nodes()) |
| status["migrations_pending"] = len(plan) > 0 |
| except Exception as e: |
| status["errors"].append(f"Migration check failed: {e}") |
|
|
| |
| try: |
| from core.models import Environment |
| status["default_env_exists"] = Environment.objects.filter( |
| is_default=True |
| ).exists() |
| except Exception as e: |
| status["errors"].append(f"Environment check failed: {e}") |
|
|
| |
| try: |
| from core.models import GlobalSettings |
| settings_obj = GlobalSettings.get_settings() |
| status["setup_completed"] = settings_obj.setup_completed |
| except Exception as e: |
| status["errors"].append(f"Settings check failed: {e}") |
|
|
| return status |
|
|
| @classmethod |
| def run_migrations(cls) -> tuple[bool, str]: |
| """ |
| Run pending database migrations programmatically. |
| |
| Returns: |
| Tuple of (success: bool, output: str) |
| """ |
| try: |
| from django.core.management import call_command |
|
|
| output = StringIO() |
| call_command("migrate", verbosity=1, stdout=output, stderr=output) |
|
|
| result = output.getvalue() |
| logger.info("Migrations completed successfully") |
| return True, result |
| except Exception as e: |
| error_msg = f"Migration failed: {str(e)}" |
| logger.error(error_msg) |
| return False, error_msg |
|
|
| @classmethod |
| def create_default_environment(cls) -> tuple[bool, str]: |
| """ |
| Create the default Python virtual environment. |
| |
| Reuses logic from setup_default_env management command. |
| |
| Returns: |
| Tuple of (success: bool, message: str) |
| """ |
| from core.models import Environment |
|
|
| |
| existing = Environment.objects.filter(is_default=True).first() |
| needs_package_restore = False |
| if existing: |
| |
| if existing.exists(): |
| return True, f"Default environment already exists: {existing.name}" |
| else: |
| |
| logger.warning( |
| f"Default environment record exists but directory missing, recreating..." |
| ) |
| |
| if existing.requirements: |
| needs_package_restore = True |
|
|
| |
| env_path = "default" |
| full_path = os.path.join(settings.ENVIRONMENTS_ROOT, env_path) |
|
|
| |
| if os.path.exists(full_path): |
| logger.info(f"Found existing venv at {full_path}, will use it") |
| else: |
| |
| logger.info(f"Creating virtual environment at {full_path}...") |
| try: |
| creationflags = ( |
| subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0 |
| ) |
| result = subprocess.run( |
| [sys.executable, "-m", "venv", full_path], |
| check=True, |
| capture_output=True, |
| text=True, |
| timeout=120, |
| creationflags=creationflags, |
| ) |
| except subprocess.CalledProcessError as e: |
| error_msg = f"Failed to create virtual environment: {e.stderr}" |
| logger.error(error_msg) |
| return False, error_msg |
| except subprocess.TimeoutExpired: |
| return False, "Timeout creating virtual environment" |
| except Exception as e: |
| return False, f"Error creating virtual environment: {str(e)}" |
|
|
| |
| if os.name == "nt": |
| python_path = os.path.join(full_path, "Scripts", "python.exe") |
| else: |
| python_path = os.path.join(full_path, "bin", "python") |
|
|
| python_version = "unknown" |
| try: |
| creationflags = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0 |
| result = subprocess.run( |
| [python_path, "--version"], |
| check=True, |
| capture_output=True, |
| text=True, |
| timeout=10, |
| creationflags=creationflags, |
| ) |
| python_version = result.stdout.strip().replace("Python ", "") |
| except Exception as e: |
| logger.warning(f"Could not get Python version: {e}") |
|
|
| |
| if existing: |
| existing.path = env_path |
| existing.python_version = python_version |
| existing.save() |
| msg = f"Updated default environment (Python {python_version})" |
| else: |
| Environment.objects.create( |
| name="Default Environment", |
| description="Auto-created default Python environment", |
| path=env_path, |
| python_version=python_version, |
| is_default=True, |
| is_active=True, |
| ) |
| msg = f"Created default environment (Python {python_version})" |
|
|
| logger.info(msg) |
|
|
| |
| if needs_package_restore and existing: |
| from core.services.environment_service import EnvironmentService |
|
|
| logger.info(f"Restoring packages from database for {existing.name}...") |
| success, _, stderr = EnvironmentService.install_requirements( |
| existing, existing.requirements |
| ) |
| if success: |
| logger.info(f"Successfully restored packages for {existing.name}") |
| msg += " (packages restored from database)" |
| else: |
| logger.warning( |
| f"Some packages failed to restore: {stderr[:200] if stderr else 'unknown error'}" |
| ) |
| msg += " (package restoration had errors, check logs)" |
|
|
| return True, msg |
|
|
| @classmethod |
| def needs_admin_setup(cls) -> bool: |
| """Check if admin user needs to be created.""" |
| try: |
| from core.models import User |
| return not User.objects.filter(is_superuser=True).exists() |
| except Exception: |
| return False |
|
|
| @classmethod |
| def create_admin_user(cls, email: str, password: str) -> tuple[bool, str]: |
| """ |
| Create the initial admin user with password. |
| |
| Args: |
| email: Admin email address |
| password: Admin password |
| |
| Returns: |
| Tuple of (success: bool, message: str) |
| """ |
| from core.models import User, GlobalSettings |
|
|
| try: |
| |
| if User.objects.filter(is_superuser=True).exists(): |
| return False, "An admin user already exists." |
|
|
| |
| user = User.objects.create_user( |
| email=email, |
| username=email, |
| is_staff=True, |
| is_superuser=True, |
| is_verified=True, |
| ) |
| user.set_password(password) |
| user.save() |
|
|
| |
| settings_obj = GlobalSettings.get_settings() |
| settings_obj.allow_registration = False |
| settings_obj.save(update_fields=["allow_registration"]) |
|
|
| logger.info(f"Admin user created: {email}") |
| return True, f"Admin user {email} created successfully." |
|
|
| except Exception as e: |
| error_msg = f"Failed to create admin user: {str(e)}" |
| logger.error(error_msg) |
| return False, error_msg |
|
|
| @classmethod |
| def complete_setup(cls) -> None: |
| """Mark initial setup as completed.""" |
| from core.models import GlobalSettings |
|
|
| settings_obj = GlobalSettings.get_settings() |
| settings_obj.setup_completed = True |
| settings_obj.setup_completed_at = timezone.now() |
| settings_obj.save() |
|
|
| logger.info("Setup marked as completed") |
|
|
| @classmethod |
| def run_full_setup(cls) -> dict: |
| """ |
| Run the complete setup process. |
| |
| Returns dict with results of each step. |
| """ |
| results = { |
| "migrations": {"success": False, "message": ""}, |
| "default_env": {"success": False, "message": ""}, |
| "completed": False, |
| } |
|
|
| |
| success, message = cls.run_migrations() |
| results["migrations"] = {"success": success, "message": message} |
|
|
| if not success: |
| return results |
|
|
| |
| success, message = cls.create_default_environment() |
| results["default_env"] = {"success": success, "message": message} |
|
|
| if not success: |
| return results |
|
|
| |
| cls.complete_setup() |
| results["completed"] = True |
|
|
| return results |
|
|