Skip to content

Commit fc7afe5

Browse files
committed
contest-hw: initial implementation
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
1 parent 04f4ebe commit fc7afe5

21 files changed

Lines changed: 4453 additions & 8 deletions

contest/__init__.py

Whitespace-only changes.

contest/hw/README.rst

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,22 @@ Config
216216

217217
- reservation timeout, seconds
218218

219+
CLI
220+
---
221+
222+
The ``nipa-mctrl`` CLI (``/usr/local/bin/nipa-mctrl`` on ctrl) provides
223+
command-line access to the machine_control API::
224+
225+
nipa-mctrl machines # list machines and health state
226+
nipa-mctrl nics # list NICs
227+
nipa-mctrl sol --machine-id 1 # view SOL logs
228+
nipa-mctrl reserve --machine-ids 1,2 # reserve machines
229+
nipa-mctrl close --reservation-id 5 # release a reservation
230+
nipa-mctrl power-cycle --machine-id 1 # power cycle via BMC
231+
232+
Add ``--json`` for machine-parseable output. Defaults to
233+
``http://localhost:5050``; override with ``--url`` or ``MC_URL`` env var.
234+
219235
In-memory state
220236
---------------
221237

@@ -256,14 +272,12 @@ The service discovers all machines using the ``machine_info`` table at startup.
256272
SOL collection
257273
~~~~~~~~~~~~~~
258274

259-
Service assumes BMC of the machines is already configured to send SOL
260-
logs to the correct place. The service uses ipmitool call to
261-
enable the SOL output at startup (and disable it at shutdown).
262-
263-
The service maintains a UDP socket to receive the logs.
264-
The BMC ipaddr from ``machine_info_sec`` is used to identify the sending
265-
machine. The service inserts the logs into the correct table
266-
and does line chunking if necessary.
275+
At startup the service spawns a persistent ``ipmitool sol activate``
276+
session for each machine (using BMC credentials from ``machine_info_sec``).
277+
Each session runs in its own thread, reading stdout and inserting lines
278+
into the ``sol`` table. If a session drops it is automatically
279+
reconnected after a short delay. Stale sessions are deactivated before
280+
each new connection attempt.
267281

268282
Managing reservations
269283
~~~~~~~~~~~~~~~~~~~~~

contest/hw/hw_worker.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#!/usr/bin/env python3
2+
# SPDX-License-Identifier: GPL-2.0
3+
4+
"""NIPA HW worker — one-shot on-boot test runner."""
5+
6+
import json
7+
import os
8+
import subprocess
9+
10+
from lib.runner import find_newest_unseen, mark_all_seen, run_tests
11+
12+
13+
TESTS_DIR = '/srv/hw-worker/tests'
14+
RESULTS_DIR = '/srv/hw-worker/results'
15+
16+
# kselftest net.config keys (see drivers/net/README.rst)
17+
_NET_CONFIG_KEYS = ['NETIF', 'LOCAL_V4', 'LOCAL_V6', 'REMOTE_V4', 'REMOTE_V6',
18+
'LOCAL_PREFIX_V6', 'REMOTE_TYPE', 'REMOTE_ARGS']
19+
20+
21+
def _parse_env_file(path):
22+
"""Parse a simple KEY=VALUE env file."""
23+
env = {}
24+
if not os.path.exists(path):
25+
return env
26+
with open(path, encoding='utf-8') as fp:
27+
for line in fp:
28+
line = line.strip()
29+
if not line or line.startswith('#'):
30+
continue
31+
key, sep, val = line.partition('=')
32+
if sep:
33+
env[key.strip()] = val.strip()
34+
return env
35+
36+
37+
def _ensure_link_up(ifname):
38+
"""Bring a network interface up if not already."""
39+
ret = subprocess.run(['ip', 'link', 'set', ifname, 'up'],
40+
capture_output=True, check=False)
41+
if ret.returncode != 0:
42+
stderr = ret.stderr.decode('utf-8', 'ignore').strip()
43+
raise RuntimeError(f"Failed to bring up {ifname}: {stderr}")
44+
45+
46+
def _ensure_addr(ifname, addr):
47+
"""Add an IP address to an interface if not already present."""
48+
bare_addr = addr.split('/')[0]
49+
ret = subprocess.run(['ip', 'addr', 'show', 'dev', ifname],
50+
capture_output=True, check=False)
51+
if bare_addr in ret.stdout.decode():
52+
return
53+
if '/' not in addr:
54+
addr += '/64' if ':' in addr else '/24'
55+
subprocess.run(['ip', 'addr', 'add', addr, 'dev', ifname], check=True)
56+
57+
58+
def setup_test_interfaces(test_dir):
59+
"""Configure test NICs and write net.config from nic-test.env.
60+
61+
The hwksft orchestrator deploys nic-test.env with interface names,
62+
IP addresses, and remote connectivity info. This function:
63+
1. Brings up the DUT and peer interfaces
64+
2. Adds IP addresses if not already configured
65+
3. Writes drivers/net/net.config for the kselftest framework
66+
"""
67+
env = _parse_env_file(os.path.join(test_dir, 'nic-test.env'))
68+
if not env:
69+
return
70+
71+
# Configure DUT interface
72+
netif = env.get('NETIF')
73+
if netif:
74+
_ensure_link_up(netif)
75+
if env.get('LOCAL_V4'):
76+
_ensure_addr(netif, env['LOCAL_V4'])
77+
if env.get('LOCAL_V6'):
78+
_ensure_addr(netif, env['LOCAL_V6'])
79+
80+
# Configure peer interface (for loopback / same-machine peers)
81+
remote_ifname = env.get('REMOTE_IFNAME')
82+
if remote_ifname:
83+
_ensure_link_up(remote_ifname)
84+
if env.get('REMOTE_V4'):
85+
_ensure_addr(remote_ifname, env['REMOTE_V4'])
86+
if env.get('REMOTE_V6'):
87+
_ensure_addr(remote_ifname, env['REMOTE_V6'])
88+
89+
# Write net.config for the kselftest framework
90+
config_lines = []
91+
for key in _NET_CONFIG_KEYS:
92+
if env.get(key):
93+
config_lines.append(f'{key}={env[key]}')
94+
95+
if config_lines:
96+
config_content = '\n'.join(config_lines) + '\n'
97+
for subdir in ['drivers/net', 'drivers/net/hw']:
98+
config_dir = os.path.join(test_dir, subdir)
99+
if os.path.isdir(config_dir):
100+
path = os.path.join(config_dir, 'net.config')
101+
with open(path, 'w', encoding='utf-8') as fp:
102+
fp.write(config_content)
103+
print(f"Wrote {path}")
104+
105+
106+
def main():
107+
"""Find pending tests, run them, and write results."""
108+
tests_dir = TESTS_DIR
109+
results_base = RESULTS_DIR
110+
111+
test_dir = find_newest_unseen(tests_dir)
112+
if test_dir is None:
113+
print("No outstanding tests found")
114+
return
115+
116+
# Verify we booted into the expected test kernel by comparing
117+
# the deployed kernel version against the running kernel.
118+
kver_path = os.path.join(test_dir, '.kernel-version')
119+
if not os.path.exists(kver_path):
120+
print("No kernel version file, skipping")
121+
return
122+
with open(kver_path, encoding='utf-8') as fp:
123+
expected = fp.read().strip()
124+
125+
actual = os.uname().release
126+
# The kernel version includes the git hash and instance name
127+
# (via CONFIG_LOCALVERSION), so accidental prefix collisions
128+
# (e.g. "6.1" matching "6.12.0") cannot happen in practice.
129+
# The '-' separator check is an extra safety measure.
130+
if actual != expected and not actual.startswith(expected + '-'):
131+
print(f"Kernel mismatch: running {actual}, expected {expected}")
132+
return
133+
134+
mark_all_seen(tests_dir)
135+
136+
# Configure test interfaces and write net.config
137+
setup_test_interfaces(test_dir)
138+
139+
reservation_id = os.path.basename(test_dir)
140+
results_dir = os.path.join(results_base, reservation_id)
141+
os.makedirs(results_dir, exist_ok=True)
142+
143+
results = run_tests(test_dir, results_dir)
144+
145+
results_file = os.path.join(results_dir, 'results.json')
146+
fd = os.open(results_file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC)
147+
with os.fdopen(fd, 'w') as fp:
148+
json.dump(results, fp)
149+
fp.flush()
150+
os.fsync(fp.fileno())
151+
152+
print(f"Completed {len(results)} tests, results in {results_dir}")
153+
154+
155+
if __name__ == '__main__':
156+
main()

0 commit comments

Comments
 (0)