77from fastapi .middleware .cors import CORSMiddleware
88from sqlmodel import Session , select
99from app .core .policies import DISTRIBOX_ADMIN_POLICY
10- from app .routes import vm , image , host , auth , user_management , tunnel , event
10+ from app .routes import vm , image , host , auth , user_management , tunnel , event , slave
11+ from app .routes import slave_agent
1112from app .orm .user import UserORM
1213from app .orm .vm_credential import VmCredentialORM # noqa: F401
1314from app .orm .event import EventORM , EventParticipantORM # noqa: F401
1415from app .orm .user_settings import UserSettingsORM # noqa: F401
16+ from app .orm .slave import SlaveORM # noqa: F401
1517from app .utils .auth import hash_password
16- from app .core .config import engine , get_env_or_default , init_db
18+ from app .core .config import engine , get_env_or_default , init_db , DISTRIBOX_MODE , QEMUConfig
1719from app .utils .crypto import encrypt_secret , is_encrypted_secret
1820from app .services .vm_service import VmService
1921
2325
2426frontend_url = get_env_or_default ("FRONTEND_URL" , "http://localhost:3000" )
2527
26- # CORS configuration
2728app .add_middleware (
2829 CORSMiddleware ,
2930 allow_origins = [frontend_url ],
3334)
3435
3536
37+ @app .on_event ("shutdown" )
38+ async def shutdown_event ():
39+ if DISTRIBOX_MODE != "slave" :
40+ return
41+
42+ _stop_all_local_vms ()
43+
44+ import httpx
45+ from app .core .config import MASTER_URL , SLAVE_API_KEY
46+
47+ if not MASTER_URL or not SLAVE_API_KEY :
48+ return
49+ try :
50+ async with httpx .AsyncClient (timeout = 5.0 ) as client :
51+ await client .post (
52+ f"{ MASTER_URL } /slaves/shutdown" ,
53+ headers = {"X-Slave-Token" : SLAVE_API_KEY },
54+ )
55+ logger .info ("Shutdown notification sent to master" )
56+ except Exception :
57+ logger .warning ("Failed to notify master of shutdown" )
58+
59+
60+ def _stop_all_local_vms ():
61+ try :
62+ conn = QEMUConfig .get_connection ()
63+ domains = conn .listAllDomains (0 )
64+ for domain in domains :
65+ if domain .isActive ():
66+ try :
67+ domain .destroy ()
68+ logger .info ("Destroyed VM %s" , domain .name ())
69+ except Exception :
70+ logger .warning ("Failed to destroy VM %s" , domain .name ())
71+ except Exception :
72+ logger .warning ("Failed to stop local VMs during shutdown" )
73+
74+
3675@app .on_event ("startup" )
3776async def startup_event ():
38- """Initialize database and create default admin user if it doesn't exist."""
39- init_db () # This might be temporary
77+ init_db ()
78+
79+ if DISTRIBOX_MODE == "slave" :
80+ print (f"✓ Starting in SLAVE mode" )
81+ asyncio .create_task (_slave_heartbeat_loop ())
82+ return
4083
4184 admin_username = get_env_or_default ("ADMIN_USERNAME" , "admin" )
4285 admin_password = get_env_or_default ("ADMIN_PASSWORD" , "admin" )
4386
4487 with Session (engine ) as session :
45- # Check if admin exists
4688 statement = select (UserORM ).where (UserORM .username == admin_username )
4789 admin = session .exec (statement ).first ()
4890
4991 if not admin :
50- # Create admin user
5192 admin = UserORM (
5293 username = admin_username ,
5394 hashed_password = hash_password (admin_password ),
@@ -93,6 +134,8 @@ async def startup_event():
93134 )
94135
95136 asyncio .create_task (_enforce_event_deadlines ())
137+ asyncio .create_task (_check_stale_slaves ())
138+ print (f"✓ Starting in MASTER mode" )
96139
97140
98141async def _enforce_event_deadlines ():
@@ -133,6 +176,54 @@ async def _enforce_event_deadlines():
133176 await asyncio .sleep (30 )
134177
135178
179+ async def _check_stale_slaves ():
180+ while True :
181+ try :
182+ from app .services .slave_service import SlaveService
183+ SlaveService .check_stale_slaves ()
184+ except Exception :
185+ logger .exception ("Error in stale slave check" )
186+ await asyncio .sleep (30 )
187+
188+
189+ async def _slave_heartbeat_loop ():
190+ import httpx
191+ from app .core .config import MASTER_URL , SLAVE_API_KEY
192+ from app .services .host_service import HostService
193+ import psutil
194+
195+ if not MASTER_URL or not SLAVE_API_KEY :
196+ logger .warning (
197+ "MASTER_URL or SLAVE_API_KEY not set, skipping heartbeat"
198+ )
199+ return
200+
201+ while True :
202+ try :
203+ host_info = HostService .get_host_info ()
204+ mem = psutil .virtual_memory ()
205+ heartbeat = {
206+ "total_cpu" : psutil .cpu_count () or 0 ,
207+ "total_mem" : round (mem .total / 2 ** 30 ),
208+ "total_disk" : round (host_info .disk .total ),
209+ "available_cpu" : round (
210+ 100.0 - host_info .cpu .percent_used_total , 2
211+ ),
212+ "available_mem" : round (host_info .mem .available , 2 ),
213+ "available_disk" : round (host_info .disk .available , 2 ),
214+ }
215+ async with httpx .AsyncClient (timeout = 10.0 ) as client :
216+ await client .post (
217+ f"{ MASTER_URL } /slaves/heartbeat" ,
218+ json = heartbeat ,
219+ headers = {"X-Slave-Token" : SLAVE_API_KEY },
220+ )
221+ logger .debug ("Heartbeat sent to master" )
222+ except Exception :
223+ logger .exception ("Failed to send heartbeat to master" )
224+ await asyncio .sleep (30 )
225+
226+
136227@app .exception_handler (HTTPException )
137228async def http_exception_handler (_ , exc : HTTPException ):
138229 return JSONResponse (
@@ -148,10 +239,14 @@ async def general_exception_handler(_, exc: Exception):
148239 content = {"detail" : str (exc )}
149240 )
150241
151- app .include_router (auth .router , prefix = "/auth" , tags = ["auth" ])
152- app .include_router (user_management .router , tags = ["users" ])
153- app .include_router (vm .router , prefix = "/vms" , tags = ["vms" ])
154- app .include_router (image .router , prefix = "/images" , tags = ["images" ])
155- app .include_router (host .router , prefix = "/host" , tags = ["host" ])
156- app .include_router (tunnel .router , tags = ["tunnel" ])
157- app .include_router (event .router , prefix = "/events" , tags = ["events" ])
242+ if DISTRIBOX_MODE == "slave" :
243+ app .include_router (slave_agent .router , tags = ["slave-agent" ])
244+ else :
245+ app .include_router (auth .router , prefix = "/auth" , tags = ["auth" ])
246+ app .include_router (user_management .router , tags = ["users" ])
247+ app .include_router (vm .router , prefix = "/vms" , tags = ["vms" ])
248+ app .include_router (image .router , prefix = "/images" , tags = ["images" ])
249+ app .include_router (host .router , prefix = "/host" , tags = ["host" ])
250+ app .include_router (tunnel .router , tags = ["tunnel" ])
251+ app .include_router (event .router , prefix = "/events" , tags = ["events" ])
252+ app .include_router (slave .router , prefix = "/slaves" , tags = ["slaves" ])
0 commit comments