Skip to content

Commit 38d5de6

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

19 files changed

Lines changed: 4303 additions & 8 deletions

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: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
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+
subprocess.run(['ip', 'link', 'set', ifname, 'up'], check=True)
40+
41+
42+
def _ensure_addr(ifname, addr):
43+
"""Add an IP address to an interface if not already present."""
44+
bare_addr = addr.split('/')[0]
45+
ret = subprocess.run(['ip', 'addr', 'show', 'dev', ifname],
46+
capture_output=True, check=False)
47+
if bare_addr in ret.stdout.decode():
48+
return
49+
if '/' not in addr:
50+
addr += '/64' if ':' in addr else '/24'
51+
subprocess.run(['ip', 'addr', 'add', addr, 'dev', ifname], check=True)
52+
53+
54+
def setup_test_interfaces(test_dir):
55+
"""Configure test NICs and write net.config from nic-test.env.
56+
57+
The hwksft orchestrator deploys nic-test.env with interface names,
58+
IP addresses, and remote connectivity info. This function:
59+
1. Brings up the DUT and peer interfaces
60+
2. Adds IP addresses if not already configured
61+
3. Writes drivers/net/net.config for the kselftest framework
62+
"""
63+
env = _parse_env_file(os.path.join(test_dir, 'nic-test.env'))
64+
if not env:
65+
return
66+
67+
# Configure DUT interface
68+
netif = env.get('NETIF')
69+
if netif:
70+
_ensure_link_up(netif)
71+
if env.get('LOCAL_V4'):
72+
_ensure_addr(netif, env['LOCAL_V4'])
73+
if env.get('LOCAL_V6'):
74+
_ensure_addr(netif, env['LOCAL_V6'])
75+
76+
# Configure peer interface (for loopback / same-machine peers)
77+
remote_ifname = env.get('REMOTE_IFNAME')
78+
if remote_ifname:
79+
_ensure_link_up(remote_ifname)
80+
if env.get('REMOTE_V4'):
81+
_ensure_addr(remote_ifname, env['REMOTE_V4'])
82+
if env.get('REMOTE_V6'):
83+
_ensure_addr(remote_ifname, env['REMOTE_V6'])
84+
85+
# Write net.config for the kselftest framework
86+
config_lines = []
87+
for key in _NET_CONFIG_KEYS:
88+
if env.get(key):
89+
config_lines.append(f'{key}={env[key]}')
90+
91+
if config_lines:
92+
config_content = '\n'.join(config_lines) + '\n'
93+
for subdir in ['drivers/net', 'drivers/net/hw']:
94+
config_dir = os.path.join(test_dir, subdir)
95+
if os.path.isdir(config_dir):
96+
path = os.path.join(config_dir, 'net.config')
97+
with open(path, 'w', encoding='utf-8') as fp:
98+
fp.write(config_content)
99+
print(f"Wrote {path}")
100+
101+
102+
def main():
103+
"""Find pending tests, run them, and write results."""
104+
tests_dir = TESTS_DIR
105+
results_base = RESULTS_DIR
106+
107+
test_dir = find_newest_unseen(tests_dir)
108+
if test_dir is None:
109+
print("No outstanding tests found")
110+
return
111+
112+
# Verify we booted into the expected test kernel by comparing
113+
# the deployed kernel version against the running kernel.
114+
kver_path = os.path.join(test_dir, '.kernel-version')
115+
if not os.path.exists(kver_path):
116+
print("No kernel version file, skipping")
117+
return
118+
with open(kver_path, encoding='utf-8') as fp:
119+
expected = fp.read().strip()
120+
121+
actual = os.uname().release
122+
# The kernel version includes the git hash and instance name
123+
# (via CONFIG_LOCALVERSION), so accidental prefix collisions
124+
# (e.g. "6.1" matching "6.12.0") cannot happen in practice.
125+
# The '-' separator check is an extra safety measure.
126+
if actual != expected and not actual.startswith(expected + '-'):
127+
print(f"Kernel mismatch: running {actual}, expected {expected}")
128+
return
129+
130+
mark_all_seen(tests_dir)
131+
132+
# Configure test interfaces and write net.config
133+
setup_test_interfaces(test_dir)
134+
135+
reservation_id = os.path.basename(test_dir)
136+
results_dir = os.path.join(results_base, reservation_id)
137+
os.makedirs(results_dir, exist_ok=True)
138+
139+
results = run_tests(test_dir, results_dir)
140+
141+
results_file = os.path.join(results_dir, 'results.json')
142+
fd = os.open(results_file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC)
143+
with os.fdopen(fd, 'w') as fp:
144+
json.dump(results, fp)
145+
fp.flush()
146+
os.fsync(fp.fileno())
147+
148+
print(f"Completed {len(results)} tests, results in {results_dir}")
149+
150+
151+
if __name__ == '__main__':
152+
main()

0 commit comments

Comments
 (0)