Skip to content

Commit 74a8708

Browse files
committed
contest: hw: mctrl: add support for sysrq
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
1 parent c2bc207 commit 74a8708

4 files changed

Lines changed: 80 additions & 1 deletion

File tree

contest/hw/lib/mc_client.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,17 @@ def health_check(self, machine_id):
9696
r.raise_for_status()
9797
return r.json()
9898

99+
def send_sysrq(self, machine_id, key):
100+
"""Send a SysRq key to a machine via SOL."""
101+
data = {
102+
'caller': self.caller,
103+
'machine_id': machine_id,
104+
'key': key,
105+
}
106+
r = requests.post(f'{self.base_url}/send_sysrq', json=data, timeout=30)
107+
r.raise_for_status()
108+
return r.json()
109+
99110

100111
def resolve_nic_id(nic_info_list, vendor, model):
101112
"""Resolve a NIC id from vendor and model strings."""

contest/hw/lib/sol_listener.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ def __init__(self, db_pool, bmc_map):
3434
self.bmc_map = bmc_map
3535
self._stop_event = threading.Event()
3636
self._threads = []
37+
self._master_fds = {} # machine_id -> master_fd
38+
self._fds_lock = threading.Lock()
3739

3840
def start(self):
3941
"""Start a reader thread for each machine."""
@@ -48,6 +50,22 @@ def stop(self):
4850
"""Signal all sessions to stop."""
4951
self._stop_event.set()
5052

53+
def send_sysrq(self, machine_id, key):
54+
"""Send a SysRq key via the SOL session.
55+
56+
Writes ipmitool's break escape sequence (~B) followed by the
57+
key character to the active SOL pty. Returns True on success.
58+
"""
59+
with self._fds_lock:
60+
fd = self._master_fds.get(machine_id)
61+
if fd is None:
62+
return False
63+
try:
64+
os.write(fd, b'~B' + key.encode('ascii'))
65+
return True
66+
except OSError:
67+
return False
68+
5169
def _insert_chunk(self, machine_id, line, eol):
5270
ts = datetime.datetime.now(datetime.UTC)
5371
conn = self.db_pool.getconn()
@@ -114,6 +132,8 @@ def _run_session(self, machine_id, bmc):
114132
print(f"SOL: session started for machine {machine_id} "
115133
f"(BMC {bmc.bmc_ipaddr})")
116134

135+
with self._fds_lock:
136+
self._master_fds[machine_id] = master_fd
117137
try:
118138
while not self._stop_event.is_set():
119139
try:
@@ -129,6 +149,8 @@ def _run_session(self, machine_id, bmc):
129149
except Exception as e:
130150
print(f"SOL: DB error for machine {machine_id}: {e}")
131151
finally:
152+
with self._fds_lock:
153+
self._master_fds.pop(machine_id, None)
132154
os.close(master_fd)
133155
proc.terminate()
134156
try:

contest/hw/machine_control.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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'])
306333
def reserve():
307334
"""Atomically reserve a group of machines."""
@@ -383,7 +410,7 @@ def reservation_close():
383410

384411
def 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()

contest/hw/mc_cli.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,16 @@ def cmd_health_check(args, mc):
193193
return 0
194194

195195

196+
def cmd_sysrq(args, mc):
197+
"""Send a SysRq key to a machine via SOL."""
198+
result = mc.send_sysrq(args.machine_id, args.key)
199+
if args.json:
200+
print(json.dumps(result, indent=2))
201+
return 0
202+
print(f"Sent SysRq '{args.key}' to machine {args.machine_id}")
203+
return 0
204+
205+
196206
def main(argv=None):
197207
"""Entry point: parse args and dispatch subcommand."""
198208
parser = argparse.ArgumentParser(
@@ -254,6 +264,13 @@ def main(argv=None):
254264
p_hc.add_argument('--machine-id', type=int, required=True,
255265
help='machine ID')
256266

267+
p_sysrq = sub.add_parser('sysrq',
268+
help='send SysRq key to a machine via SOL')
269+
p_sysrq.add_argument('--machine-id', type=int, required=True,
270+
help='machine ID')
271+
p_sysrq.add_argument('--key', required=True,
272+
help='SysRq key (e.g. c=crashdump, b=reboot, e=SIGTERM)')
273+
257274
args = parser.parse_args(argv)
258275

259276
if not args.command:
@@ -275,6 +292,7 @@ def main(argv=None):
275292
'close': cmd_close,
276293
'power-cycle': cmd_power_cycle,
277294
'health-check': cmd_health_check,
295+
'sysrq': cmd_sysrq,
278296
}
279297
try:
280298
return commands[args.command](args, mc)

0 commit comments

Comments
 (0)