pyrunner / core /services /setup_service.py
Perspicacious's picture
Deploy PyRunner on Hugging Face
2529305
Raw
History Blame Contribute Delete
11.7 kB
"""
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
# Check setup_completed flag
settings_obj = GlobalSettings.get_settings()
if not settings_obj.setup_completed:
return True
# Check if default environment exists
if not Environment.objects.filter(is_default=True).exists():
return True
return False
except Exception as e:
# Tables might not exist yet (before migrations)
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:
# Check if database tables exist
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}")
# Check pending migrations
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}")
# Check default environment
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}")
# Check setup_completed flag
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
# Check if default environment already exists
existing = Environment.objects.filter(is_default=True).first()
needs_package_restore = False
if existing:
# Check if directory exists
if existing.exists():
return True, f"Default environment already exists: {existing.name}"
else:
# Directory missing, need to recreate
logger.warning(
f"Default environment record exists but directory missing, recreating..."
)
# Mark for package restoration if we have saved requirements
if existing.requirements:
needs_package_restore = True
# Define paths
env_path = "default"
full_path = os.path.join(settings.ENVIRONMENTS_ROOT, env_path)
# Check if directory already exists (orphaned)
if os.path.exists(full_path):
logger.info(f"Found existing venv at {full_path}, will use it")
else:
# Create the virtual environment
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)}"
# Get Python version from the new venv
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}")
# Create or update the Environment record
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)
# Restore packages from database if venv was recreated and had saved requirements
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:
# Check if admin already exists
if User.objects.filter(is_superuser=True).exists():
return False, "An admin user already exists."
# Create the admin user with password
user = User.objects.create_user(
email=email,
username=email,
is_staff=True,
is_superuser=True,
is_verified=True,
)
user.set_password(password)
user.save()
# Disable open registration after first user
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,
}
# Run migrations
success, message = cls.run_migrations()
results["migrations"] = {"success": success, "message": message}
if not success:
return results
# Create default environment
success, message = cls.create_default_environment()
results["default_env"] = {"success": success, "message": message}
if not success:
return results
# Mark setup as completed
cls.complete_setup()
results["completed"] = True
return results