Akoda35 commited on
Commit
592d2ef
·
verified ·
1 Parent(s): 922e5e8

Upload 4 files

Browse files
core/management/commands/__init__.py ADDED
File without changes
core/management/commands/restart_workers.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Management command to restart qcluster workers.
3
+
4
+ This command signals the entrypoint script to restart the worker process.
5
+ It's designed to be called from the web UI or manually.
6
+ """
7
+
8
+ import os
9
+ import signal
10
+ import time
11
+ import logging
12
+
13
+ from django.core.management.base import BaseCommand, CommandError
14
+ from django.utils import timezone
15
+
16
+ from core.models import GlobalSettings
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ PID_FILE = "/tmp/qcluster.pid"
21
+
22
+
23
+ class Command(BaseCommand):
24
+ help = "Restart the django-q2 worker process"
25
+
26
+ def add_arguments(self, parser):
27
+ parser.add_argument(
28
+ "--force",
29
+ action="store_true",
30
+ help="Force restart even if workers appear healthy",
31
+ )
32
+ parser.add_argument(
33
+ "--timeout",
34
+ type=int,
35
+ default=30,
36
+ help="Seconds to wait for workers to restart (default: 30)",
37
+ )
38
+
39
+ def handle(self, *args, **options):
40
+ timeout = options["timeout"]
41
+
42
+ # Check if PID file exists
43
+ if not os.path.exists(PID_FILE):
44
+ raise CommandError(
45
+ f"Worker PID file not found at {PID_FILE}. "
46
+ "Workers may not be running or not started via entrypoint.sh"
47
+ )
48
+
49
+ # Read current worker PID
50
+ try:
51
+ with open(PID_FILE, "r") as f:
52
+ old_pid = int(f.read().strip())
53
+ except (ValueError, IOError) as e:
54
+ raise CommandError(f"Failed to read PID file: {e}")
55
+
56
+ # Verify process exists
57
+ try:
58
+ os.kill(old_pid, 0)
59
+ self.stdout.write(f"Current worker PID: {old_pid}")
60
+ except OSError:
61
+ self.stdout.write(
62
+ self.style.WARNING(
63
+ f"Worker process {old_pid} not found. May already be restarting."
64
+ )
65
+ )
66
+
67
+ # Record the restart request time
68
+ restart_requested_at = timezone.now()
69
+
70
+ # Try to send SIGUSR1 to PID 1 (entrypoint process in Docker)
71
+ restart_sent = False
72
+ try:
73
+ os.kill(1, signal.SIGUSR1)
74
+ self.stdout.write(self.style.SUCCESS("Restart signal sent to entrypoint"))
75
+ restart_sent = True
76
+ except OSError as e:
77
+ # Fallback: try to kill the worker directly (non-Docker case)
78
+ self.stdout.write(
79
+ self.style.WARNING(f"Could not signal PID 1: {e}")
80
+ )
81
+ self.stdout.write("Attempting direct worker termination...")
82
+ try:
83
+ os.kill(old_pid, signal.SIGTERM)
84
+ self.stdout.write(
85
+ self.style.SUCCESS(f"SIGTERM sent to worker {old_pid}")
86
+ )
87
+ restart_sent = True
88
+ except OSError as e2:
89
+ raise CommandError(f"Failed to stop worker: {e2}")
90
+
91
+ if not restart_sent:
92
+ raise CommandError("Failed to send restart signal")
93
+
94
+ # Wait for restart to complete
95
+ self.stdout.write(f"Waiting up to {timeout}s for workers to restart...")
96
+
97
+ start_time = time.time()
98
+ while time.time() - start_time < timeout:
99
+ time.sleep(2)
100
+
101
+ # Check if PID changed (new worker started)
102
+ if os.path.exists(PID_FILE):
103
+ try:
104
+ with open(PID_FILE, "r") as f:
105
+ new_pid = int(f.read().strip())
106
+ if new_pid != old_pid:
107
+ self.stdout.write(
108
+ self.style.SUCCESS(
109
+ f"New worker started with PID {new_pid}"
110
+ )
111
+ )
112
+ return
113
+ except (ValueError, IOError):
114
+ pass
115
+
116
+ # Check if new heartbeat received after restart
117
+ settings = GlobalSettings.get_settings()
118
+ settings.refresh_from_db()
119
+ if (
120
+ settings.worker_heartbeat_at
121
+ and settings.worker_heartbeat_at > restart_requested_at
122
+ ):
123
+ self.stdout.write(
124
+ self.style.SUCCESS("Workers restarted successfully!")
125
+ )
126
+ return
127
+
128
+ self.stdout.write(
129
+ self.style.WARNING(
130
+ f"Timeout waiting for restart confirmation. "
131
+ "Check worker logs for status."
132
+ )
133
+ )
core/management/commands/setup.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Management command for initial setup.
3
+
4
+ This command is designed to be used as a Docker entrypoint or for
5
+ scripted deployments where browser-based setup is not available.
6
+ """
7
+
8
+ from django.core.management.base import BaseCommand
9
+
10
+ from core.services.setup_service import SetupService
11
+
12
+
13
+ class Command(BaseCommand):
14
+ help = "Run initial setup (migrations + default environment)"
15
+
16
+ def add_arguments(self, parser):
17
+ parser.add_argument(
18
+ "--skip-env",
19
+ action="store_true",
20
+ help="Skip creating the default Python environment",
21
+ )
22
+ parser.add_argument(
23
+ "--force",
24
+ action="store_true",
25
+ help="Run setup even if already completed",
26
+ )
27
+
28
+ def handle(self, *args, **options):
29
+ skip_env = options["skip_env"]
30
+ force = options["force"]
31
+
32
+ self.stdout.write("Starting PyRunner setup...\n")
33
+
34
+ # ALWAYS run migrations (required for upgrades)
35
+ self.stdout.write("Running database migrations...")
36
+ success, message = SetupService.run_migrations()
37
+ if success:
38
+ self.stdout.write(self.style.SUCCESS(" Migrations complete"))
39
+ else:
40
+ self.stdout.write(self.style.ERROR(f" Migration failed: {message}"))
41
+ return
42
+
43
+ # Check if full setup is needed (environment creation, etc.)
44
+ if not force and not SetupService.is_setup_needed():
45
+ self.stdout.write(
46
+ self.style.SUCCESS("Setup already completed.")
47
+ )
48
+ return
49
+
50
+ # Create default environment (only on initial setup)
51
+ if not skip_env:
52
+ self.stdout.write("Creating default Python environment...")
53
+ success, message = SetupService.create_default_environment()
54
+ if success:
55
+ self.stdout.write(self.style.SUCCESS(f" {message}"))
56
+ else:
57
+ self.stdout.write(self.style.ERROR(f" Failed: {message}"))
58
+ return
59
+ else:
60
+ self.stdout.write(
61
+ self.style.WARNING(" Skipping default environment (--skip-env)")
62
+ )
63
+
64
+ # Mark setup as complete
65
+ SetupService.complete_setup()
66
+
67
+ self.stdout.write("")
68
+ self.stdout.write(self.style.SUCCESS("Setup completed successfully!"))
69
+ self.stdout.write("")
70
+ self.stdout.write("Next steps:")
71
+ self.stdout.write(" 1. Start the task worker: python manage.py qcluster")
72
+ self.stdout.write(" 2. Start the web server: python manage.py runserver")
73
+ self.stdout.write(" 3. Log in - the first user becomes the administrator")
core/management/commands/setup_default_env.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Management command to set up the default Python environment.
3
+
4
+ Creates a virtual environment at data/environments/default/ and registers it
5
+ in the database as the default environment for script execution.
6
+ """
7
+
8
+ import os
9
+ import subprocess
10
+ import sys
11
+
12
+ from django.conf import settings
13
+ from django.core.management.base import BaseCommand, CommandError
14
+
15
+ from core.models import Environment
16
+
17
+
18
+ class Command(BaseCommand):
19
+ help = "Create and register the default Python environment"
20
+
21
+ def add_arguments(self, parser):
22
+ parser.add_argument(
23
+ "--force",
24
+ action="store_true",
25
+ help="Recreate the environment even if it already exists",
26
+ )
27
+
28
+ def handle(self, *args, **options):
29
+ force = options["force"]
30
+
31
+ # Check if default environment already exists in DB
32
+ existing = Environment.objects.filter(is_default=True).first()
33
+ if existing and not force:
34
+ self.stdout.write(
35
+ self.style.WARNING(
36
+ f'Default environment already exists: "{existing.name}" '
37
+ f"at {existing.get_full_path()}"
38
+ )
39
+ )
40
+ self.stdout.write("Use --force to recreate it.")
41
+ return
42
+
43
+ # Define paths
44
+ env_path = "default"
45
+ full_path = os.path.join(settings.ENVIRONMENTS_ROOT, env_path)
46
+
47
+ # Check if directory already exists
48
+ if os.path.exists(full_path):
49
+ if not force:
50
+ self.stdout.write(
51
+ self.style.WARNING(
52
+ f"Environment directory already exists: {full_path}"
53
+ )
54
+ )
55
+ self.stdout.write("Use --force to recreate it.")
56
+ return
57
+ else:
58
+ self.stdout.write(f"Removing existing environment at {full_path}...")
59
+ import shutil
60
+
61
+ shutil.rmtree(full_path)
62
+
63
+ # Create the virtual environment
64
+ self.stdout.write(f"Creating virtual environment at {full_path}...")
65
+ try:
66
+ subprocess.run(
67
+ [sys.executable, "-m", "venv", full_path],
68
+ check=True,
69
+ capture_output=True,
70
+ text=True,
71
+ )
72
+ except subprocess.CalledProcessError as e:
73
+ raise CommandError(f"Failed to create virtual environment: {e.stderr}")
74
+
75
+ # Get Python version from the new venv
76
+ if os.name == "nt":
77
+ python_path = os.path.join(full_path, "Scripts", "python.exe")
78
+ else:
79
+ python_path = os.path.join(full_path, "bin", "python")
80
+
81
+ try:
82
+ result = subprocess.run(
83
+ [python_path, "--version"],
84
+ check=True,
85
+ capture_output=True,
86
+ text=True,
87
+ )
88
+ python_version = result.stdout.strip().replace("Python ", "")
89
+ except subprocess.CalledProcessError:
90
+ python_version = "unknown"
91
+
92
+ # Create or update the Environment record
93
+ if existing and force:
94
+ # Update existing record
95
+ existing.path = env_path
96
+ existing.python_version = python_version
97
+ existing.save()
98
+ self.stdout.write(
99
+ self.style.SUCCESS(
100
+ f'Updated default environment "{existing.name}" '
101
+ f"(Python {python_version})"
102
+ )
103
+ )
104
+ else:
105
+ # Create new record
106
+ Environment.objects.create(
107
+ name="Default Environment",
108
+ description="Auto-created default Python environment",
109
+ path=env_path,
110
+ python_version=python_version,
111
+ is_default=True,
112
+ is_active=True,
113
+ )
114
+ self.stdout.write(
115
+ self.style.SUCCESS(
116
+ f"Created default environment (Python {python_version})"
117
+ )
118
+ )
119
+
120
+ self.stdout.write(f"Environment path: {full_path}")
121
+ self.stdout.write(f"Python executable: {python_path}")