@@ -302,6 +302,33 @@ def health_check():
302302 })
303303
304304
305+ @app .route ('/send_sysrq' , methods = ['POST' ])
306+ def send_sysrq ():
307+ """Send a SysRq key to a machine via the active SOL session."""
308+ data = request .get_json () or {}
309+ machine_id = data .get ('machine_id' )
310+ key = data .get ('key' , '' )
311+
312+ if machine_id is None :
313+ return jsonify ({'error' : 'machine_id required' }), 400
314+
315+ if len (key ) != 1 or not key .isascii ():
316+ return jsonify ({'error' : 'key must be a single ASCII character' }), 400
317+
318+ if not _check_auth (machine_id ):
319+ return jsonify ({'error' : 'unauthorized' }), 403
320+
321+ if sol_listener is None :
322+ return jsonify ({'error' : 'SOL not running' }), 503
323+
324+ ok = sol_listener .send_sysrq (machine_id , key )
325+ if not ok :
326+ return jsonify ({'error' : f'No active SOL session for machine { machine_id } ' }), 404
327+
328+ print (f"SysRq: sent '{ key } ' to machine { machine_id } " )
329+ return jsonify ({'ok' : True , 'machine_id' : machine_id , 'key' : key })
330+
331+
305332@app .route ('/reserve' , methods = ['POST' ])
306333def reserve ():
307334 """Atomically reserve a group of machines."""
@@ -383,7 +410,7 @@ def reservation_close():
383410
384411def main ():
385412 """Initialize services and run Flask app."""
386- global db_pool , health_checker , res_mgr # pylint: disable=global-statement
413+ global db_pool , health_checker , res_mgr , sol_listener # pylint: disable=global-statement
387414
388415 config = configparser .ConfigParser ()
389416 cfg_paths = ['hw.config' , 'machine_control.config' ]
@@ -426,6 +453,7 @@ def main():
426453
427454 def _start_threads ():
428455 """Start background threads — called after gunicorn fork."""
456+ global sol_listener # pylint: disable=global-statement
429457 sol_listener = SOLCollector (db_pool , bmc_map )
430458 sol_listener .start ()
431459 health_checker .start ()
0 commit comments