xref: /linux/tools/testing/selftests/drivers/net/hw/rss_ctx.py (revision 26ba30221c03364d6ed9910be8da4c1fd871b07b)
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0
3
4import datetime
5import random
6import re
7import time
8from lib.py import ksft_disruptive
9from lib.py import ksft_run, ksft_pr, ksft_exit
10from lib.py import ksft_eq, ksft_ne, ksft_ge, ksft_in, ksft_lt, ksft_true, ksft_raises
11from lib.py import NetDrvEpEnv
12from lib.py import EthtoolFamily, NetdevFamily, NlError
13from lib.py import KsftSkipEx, KsftFailEx
14from lib.py import rand_port, rand_ports
15from lib.py import cmd, ethtool, ip, defer, CmdExitFailure, wait_file
16from lib.py import GenerateTraffic
17
18
19def _rss_key_str(key):
20    return ":".join(["{:02x}".format(x) for x in key])
21
22
23def _rss_key_rand(length):
24    return [random.randint(0, 255) for _ in range(length)]
25
26
27def _rss_key_check(cfg, data=None, context=0):
28    if data is None:
29        data = get_rss(cfg, context=context)
30    if 'rss-hash-key' not in data:
31        return
32    non_zero = [x for x in data['rss-hash-key'] if x != 0]
33    ksft_eq(bool(non_zero), True, comment=f"RSS key is all zero {data['rss-hash-key']}")
34
35
36def get_rss(cfg, context=0):
37    return ethtool(f"-x {cfg.ifname} context {context}", json=True)[0]
38
39
40def get_drop_err_sum(cfg):
41    stats = ip("-s -s link show dev " + cfg.ifname, json=True)[0]
42    cnt = 0
43    for key in ['errors', 'dropped', 'over_errors', 'fifo_errors',
44                'length_errors', 'crc_errors', 'missed_errors',
45                'frame_errors']:
46        cnt += stats["stats64"]["rx"][key]
47    return cnt, stats["stats64"]["tx"]["carrier_changes"]
48
49
50def ethtool_create(cfg, act, opts):
51    output = ethtool(f"{act} {cfg.ifname} {opts}").stdout
52    # Output will be something like: "New RSS context is 1" or
53    # "Added rule with ID 7", we want the integer from the end
54    return int(output.split()[-1])
55
56
57def require_ntuple(cfg):
58    features = ethtool(f"-k {cfg.ifname}", json=True)[0]
59    if not features["ntuple-filters"]["active"]:
60        if features["ntuple-filters"]["fixed"]:
61            raise KsftSkipEx("Device does not support ntuple-filters")
62        ethtool(f"-K {cfg.ifname} ntuple-filters on")
63        defer(ethtool, f"-K {cfg.ifname} ntuple-filters off")
64
65
66def require_context_cnt(cfg, need_cnt):
67    # There's no good API to get the context count, so the tests
68    # which try to add a lot opportunisitically set the count they
69    # discovered. Careful with test ordering!
70    if need_cnt and cfg.context_cnt and cfg.context_cnt < need_cnt:
71        raise KsftSkipEx(f"Test requires at least {need_cnt} contexts, but device only has {cfg.context_cnt}")
72
73
74# Get Rx packet counts for all queues, as a simple list of integers
75# if @prev is specified the prev counts will be subtracted
76def _get_rx_cnts(cfg, prev=None):
77    cfg.wait_hw_stats_settle()
78    data = cfg.netdevnl.qstats_get({"ifindex": cfg.ifindex, "scope": ["queue"]}, dump=True)
79    data = [x for x in data if x['queue-type'] == "rx"]
80    max_q = max([x["queue-id"] for x in data])
81    queue_stats = [0] * (max_q + 1)
82    for q in data:
83        queue_stats[q["queue-id"]] = q["rx-packets"]
84        if prev and q["queue-id"] < len(prev):
85            queue_stats[q["queue-id"]] -= prev[q["queue-id"]]
86    return queue_stats
87
88
89def _send_traffic_check(cfg, port, name, params):
90    # params is a dict with 3 possible keys:
91    #  - "target": required, which queues we expect to get iperf traffic
92    #  - "empty": optional, which queues should see no traffic at all
93    #  - "noise": optional, which queues we expect to see low traffic;
94    #             used for queues of the main context, since some background
95    #             OS activity may use those queues while we're testing
96    # the value for each is a list, or some other iterable containing queue ids.
97
98    cnts = _get_rx_cnts(cfg)
99    GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000)
100    cnts = _get_rx_cnts(cfg, prev=cnts)
101
102    directed = sum(cnts[i] for i in params['target'])
103
104    ksft_ge(directed, 20000, f"traffic on {name}: " + str(cnts))
105    if params.get('noise'):
106        ksft_lt(sum(cnts[i] for i in params['noise']), directed / 2,
107                f"traffic on other queues ({name})':" + str(cnts))
108    if params.get('empty'):
109        ksft_eq(sum(cnts[i] for i in params['empty']), 0,
110                f"traffic on inactive queues ({name}): " + str(cnts))
111
112
113def _ntuple_rule_check(cfg, rule_id, ctx_id):
114    """Check that ntuple rule references RSS context ID"""
115    text = ethtool(f"-n {cfg.ifname} rule {rule_id}").stdout
116    pattern = f"RSS Context (ID: )?{ctx_id}"
117    ksft_true(re.search(pattern, text), "RSS context not referenced in ntuple rule")
118
119
120def test_rss_key_indir(cfg):
121    """Test basics like updating the main RSS key and indirection table."""
122
123    qcnt = len(_get_rx_cnts(cfg))
124    if qcnt < 3:
125        raise KsftSkipEx("Device has fewer than 3 queues (or doesn't support queue stats)")
126
127    data = get_rss(cfg)
128    want_keys = ['rss-hash-key', 'rss-hash-function', 'rss-indirection-table']
129    for k in want_keys:
130        if k not in data:
131            raise KsftFailEx("ethtool results missing key: " + k)
132        if not data[k]:
133            raise KsftFailEx(f"ethtool results empty for '{k}': {data[k]}")
134
135    _rss_key_check(cfg, data=data)
136    key_len = len(data['rss-hash-key'])
137
138    # Set the key
139    key = _rss_key_rand(key_len)
140    ethtool(f"-X {cfg.ifname} hkey " + _rss_key_str(key))
141
142    data = get_rss(cfg)
143    ksft_eq(key, data['rss-hash-key'])
144
145    # Set the indirection table and the key together
146    key = _rss_key_rand(key_len)
147    ethtool(f"-X {cfg.ifname} equal 3 hkey " + _rss_key_str(key))
148    reset_indir = defer(ethtool, f"-X {cfg.ifname} default")
149
150    data = get_rss(cfg)
151    _rss_key_check(cfg, data=data)
152    ksft_eq(0, min(data['rss-indirection-table']))
153    ksft_eq(2, max(data['rss-indirection-table']))
154
155    # Reset indirection table and set the key
156    key = _rss_key_rand(key_len)
157    ethtool(f"-X {cfg.ifname} default hkey " + _rss_key_str(key))
158    data = get_rss(cfg)
159    _rss_key_check(cfg, data=data)
160    ksft_eq(0, min(data['rss-indirection-table']))
161    ksft_eq(qcnt - 1, max(data['rss-indirection-table']))
162
163    # Set the indirection table
164    ethtool(f"-X {cfg.ifname} equal 2")
165    data = get_rss(cfg)
166    ksft_eq(0, min(data['rss-indirection-table']))
167    ksft_eq(1, max(data['rss-indirection-table']))
168
169    # Check we only get traffic on the first 2 queues
170
171    # Retry a few times in case the flows skew to a single queue.
172    attempts = 3
173    for attempt in range(attempts):
174        cnts = _get_rx_cnts(cfg)
175        GenerateTraffic(cfg).wait_pkts_and_stop(20000)
176        cnts = _get_rx_cnts(cfg, prev=cnts)
177        if cnts[0] >= 5000 and cnts[1] >= 5000:
178            break
179        ksft_pr(f"Skewed queue distribution, attempt {attempt + 1}/{attempts}: " + str(cnts))
180
181    # 2 queues, 20k packets, must be at least 5k per queue
182    ksft_ge(cnts[0], 5000, "traffic on main context (1/2): " + str(cnts))
183    ksft_ge(cnts[1], 5000, "traffic on main context (2/2): " + str(cnts))
184    # The other queues should be unused
185    ksft_eq(sum(cnts[2:]), 0, "traffic on unused queues: " + str(cnts))
186
187    # Restore, and check traffic gets spread again
188    reset_indir.exec()
189
190    for attempt in range(attempts):
191        cnts = _get_rx_cnts(cfg)
192        GenerateTraffic(cfg).wait_pkts_and_stop(20000)
193        cnts = _get_rx_cnts(cfg, prev=cnts)
194        if qcnt > 4:
195            if sum(cnts[:2]) < sum(cnts[2:]):
196                break
197        else:
198            if cnts[2] >= 3500:
199                break
200        ksft_pr(f"Skewed queue distribution, attempt {attempt + 1}/{attempts}: " + str(cnts))
201
202    if qcnt > 4:
203        # First two queues get less traffic than all the rest
204        ksft_lt(sum(cnts[:2]), sum(cnts[2:]),
205                "traffic distributed: " + str(cnts))
206    else:
207        # When queue count is low make sure third queue got significant pkts
208        ksft_ge(cnts[2], 3500, "traffic distributed: " + str(cnts))
209
210
211def test_rss_queue_reconfigure(cfg, main_ctx=True):
212    """Make sure queue changes can't override requested RSS config.
213
214    By default main RSS table should change to include all queues.
215    When user sets a specific RSS config the driver should preserve it,
216    even when queue count changes. Driver should refuse to deactivate
217    queues used in the user-set RSS config.
218    """
219
220    if not main_ctx:
221        require_ntuple(cfg)
222
223    # Start with 4 queues, an arbitrary known number.
224    try:
225        qcnt = len(_get_rx_cnts(cfg))
226        ethtool(f"-L {cfg.ifname} combined 4")
227        defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
228    except:
229        raise KsftSkipEx("Not enough queues for the test or qstat not supported")
230
231    if main_ctx:
232        ctx_id = 0
233        ctx_ref = ""
234    else:
235        ctx_id = ethtool_create(cfg, "-X", "context new")
236        ctx_ref = f"context {ctx_id}"
237        defer(ethtool, f"-X {cfg.ifname} {ctx_ref} delete")
238
239    # Indirection table should be distributing to all queues.
240    data = get_rss(cfg, context=ctx_id)
241    ksft_eq(0, min(data['rss-indirection-table']))
242    ksft_eq(3, max(data['rss-indirection-table']))
243
244    # Increase queues, indirection table should be distributing to all queues.
245    # It's unclear whether tables of additional contexts should be reset, too.
246    if main_ctx:
247        ethtool(f"-L {cfg.ifname} combined 5")
248        data = get_rss(cfg)
249        ksft_eq(0, min(data['rss-indirection-table']))
250        ksft_eq(4, max(data['rss-indirection-table']))
251        ethtool(f"-L {cfg.ifname} combined 4")
252
253    # Configure the table explicitly
254    port = rand_port()
255    ethtool(f"-X {cfg.ifname} {ctx_ref} weight 1 0 0 1")
256    if main_ctx:
257        other_key = 'empty'
258        defer(ethtool, f"-X {cfg.ifname} default")
259    else:
260        other_key = 'noise'
261        flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id}"
262        ntuple = ethtool_create(cfg, "-N", flow)
263        defer(ethtool, f"-N {cfg.ifname} delete {ntuple}")
264
265    _send_traffic_check(cfg, port, ctx_ref, { 'target': (0, 3),
266                                              other_key: (1, 2) })
267
268    # We should be able to increase queues, but table should be left untouched
269    ethtool(f"-L {cfg.ifname} combined 5")
270    data = get_rss(cfg, context=ctx_id)
271    ksft_eq({0, 3}, set(data['rss-indirection-table']))
272
273    _send_traffic_check(cfg, port, ctx_ref, { 'target': (0, 3),
274                                              other_key: (1, 2, 4) })
275
276    # Setting queue count to 3 should fail, queue 3 is used
277    try:
278        ethtool(f"-L {cfg.ifname} combined 3")
279    except CmdExitFailure:
280        pass
281    else:
282        raise Exception(f"Driver didn't prevent us from deactivating a used queue (context {ctx_id})")
283
284    if not main_ctx:
285        ethtool(f"-L {cfg.ifname} combined 4")
286        flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id} action 1"
287        try:
288            # this targets queue 4, which doesn't exist
289            ntuple2 = ethtool_create(cfg, "-N", flow)
290            defer(ethtool, f"-N {cfg.ifname} delete {ntuple2}")
291        except CmdExitFailure:
292            pass
293        else:
294            raise Exception(f"Driver didn't prevent us from targeting a nonexistent queue (context {ctx_id})")
295        # change the table to target queues 0 and 2
296        ethtool(f"-X {cfg.ifname} {ctx_ref} weight 1 0 1 0")
297        # ntuple rule therefore targets queues 1 and 3
298        try:
299            ntuple2 = ethtool_create(cfg, "-N", flow)
300        except CmdExitFailure:
301            ksft_pr("Driver does not support rss + queue offset")
302            return
303
304        defer(ethtool, f"-N {cfg.ifname} delete {ntuple2}")
305        # should replace existing filter
306        ksft_eq(ntuple, ntuple2)
307        _send_traffic_check(cfg, port, ctx_ref, { 'target': (1, 3),
308                                                  'noise' : (0, 2) })
309        # Setting queue count to 3 should fail, queue 3 is used
310        try:
311            ethtool(f"-L {cfg.ifname} combined 3")
312        except CmdExitFailure:
313            pass
314        else:
315            raise Exception(f"Driver didn't prevent us from deactivating a used queue (context {ctx_id})")
316
317
318def test_rss_resize(cfg):
319    """Test resizing of the RSS table.
320
321    Some devices dynamically increase and decrease the size of the RSS
322    indirection table based on the number of enabled queues.
323    When that happens driver must maintain the balance of entries
324    (preferably duplicating the smaller table).
325    """
326
327    channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
328    ch_max = channels['combined-max']
329    qcnt = channels['combined-count']
330
331    if ch_max < 2:
332        raise KsftSkipEx(f"Not enough queues for the test: {ch_max}")
333
334    ethtool(f"-L {cfg.ifname} combined 2")
335    defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
336
337    ethtool(f"-X {cfg.ifname} weight 1 7")
338    defer(ethtool, f"-X {cfg.ifname} default")
339
340    ethtool(f"-L {cfg.ifname} combined {ch_max}")
341    data = get_rss(cfg)
342    ksft_eq(0, min(data['rss-indirection-table']))
343    ksft_eq(1, max(data['rss-indirection-table']))
344
345    ksft_eq(7,
346            data['rss-indirection-table'].count(1) /
347            data['rss-indirection-table'].count(0),
348            f"Table imbalance after resize: {data['rss-indirection-table']}")
349
350
351def test_hitless_key_update(cfg):
352    """Test that flows may be rehashed without impacting traffic.
353
354    Some workloads may want to rehash the flows in response to an imbalance.
355    Most effective way to do that is changing the RSS key. Check that changing
356    the key does not cause link flaps or traffic disruption.
357
358    Disrupting traffic for key update is not a bug, but makes the key
359    update unusable for rehashing under load.
360    """
361    data = get_rss(cfg)
362    key_len = len(data['rss-hash-key'])
363
364    ethnl = EthtoolFamily()
365    key = random.randbytes(key_len)
366
367    tgen = GenerateTraffic(cfg)
368    try:
369        errors0, carrier0 = get_drop_err_sum(cfg)
370        t0 = datetime.datetime.now()
371        ethnl.rss_set({"header": {"dev-index": cfg.ifindex}, "hkey": key})
372        t1 = datetime.datetime.now()
373        errors1, carrier1 = get_drop_err_sum(cfg)
374    finally:
375        tgen.wait_pkts_and_stop(5000)
376
377    ksft_lt((t1 - t0).total_seconds(), 0.15)
378    ksft_eq(errors1 - errors0, 0)
379    ksft_eq(carrier1 - carrier0, 0)
380
381
382def test_rss_context_dump(cfg):
383    """
384    Test dumping RSS contexts. This tests mostly exercises the kernel APIs.
385    """
386
387    # Get a random key of the right size
388    data = get_rss(cfg)
389    if 'rss-hash-key' in data:
390        key_data = _rss_key_rand(len(data['rss-hash-key']))
391        key = _rss_key_str(key_data)
392    else:
393        key_data = []
394        key = "ba:ad"
395
396    ids = []
397    try:
398        ids.append(ethtool_create(cfg, "-X", f"context new"))
399        defer(ethtool, f"-X {cfg.ifname} context {ids[-1]} delete")
400
401        ids.append(ethtool_create(cfg, "-X", f"context new weight 1 1"))
402        defer(ethtool, f"-X {cfg.ifname} context {ids[-1]} delete")
403
404        ids.append(ethtool_create(cfg, "-X", f"context new hkey {key}"))
405        defer(ethtool, f"-X {cfg.ifname} context {ids[-1]} delete")
406    except CmdExitFailure:
407        if not ids:
408            raise KsftSkipEx("Unable to add any contexts")
409        ksft_pr(f"Added only {len(ids)} out of 3 contexts")
410
411    expect_tuples = set([(cfg.ifname, -1)] + [(cfg.ifname, ctx_id) for ctx_id in ids])
412
413    # Dump all
414    ctxs = cfg.ethnl.rss_get({}, dump=True)
415    tuples = [(c['header']['dev-name'], c.get('context', -1)) for c in ctxs]
416    ksft_eq(len(tuples), len(set(tuples)), "duplicates in context dump")
417    ctx_tuples = set([ctx for ctx in tuples if ctx[0] == cfg.ifname])
418    ksft_eq(expect_tuples, ctx_tuples)
419
420    # Sanity-check the results
421    for data in ctxs:
422        ksft_ne(set(data.get('indir', [1])), {0}, "indir table is all zero")
423        ksft_ne(set(data.get('hkey', [1])), {0}, "key is all zero")
424
425        # More specific checks
426        if len(ids) > 1 and data.get('context') == ids[1]:
427            ksft_eq(set(data['indir']), {0, 1},
428                    "ctx1 - indir table mismatch")
429        if len(ids) > 2 and data.get('context') == ids[2]:
430            ksft_eq(data['hkey'], bytes(key_data), "ctx2 - key mismatch")
431
432    # Ifindex filter
433    ctxs = cfg.ethnl.rss_get({'header': {'dev-name': cfg.ifname}}, dump=True)
434    tuples = [(c['header']['dev-name'], c.get('context', -1)) for c in ctxs]
435    ctx_tuples = set(tuples)
436    ksft_eq(len(tuples), len(ctx_tuples), "duplicates in context dump")
437    ksft_eq(expect_tuples, ctx_tuples)
438
439    # Skip ctx 0
440    expect_tuples.remove((cfg.ifname, -1))
441
442    ctxs = cfg.ethnl.rss_get({'start-context': 1}, dump=True)
443    tuples = [(c['header']['dev-name'], c.get('context', -1)) for c in ctxs]
444    ksft_eq(len(tuples), len(set(tuples)), "duplicates in context dump")
445    ctx_tuples = set([ctx for ctx in tuples if ctx[0] == cfg.ifname])
446    ksft_eq(expect_tuples, ctx_tuples)
447
448    # And finally both with ifindex and skip main
449    ctxs = cfg.ethnl.rss_get({'header': {'dev-name': cfg.ifname}, 'start-context': 1}, dump=True)
450    ctx_tuples = set([(c['header']['dev-name'], c.get('context', -1)) for c in ctxs])
451    ksft_eq(expect_tuples, ctx_tuples)
452
453
454def test_rss_context(cfg, ctx_cnt=1, create_with_cfg=None):
455    """
456    Test separating traffic into RSS contexts.
457    The queues will be allocated 2 for each context:
458     ctx0  ctx1  ctx2  ctx3
459    [0 1] [2 3] [4 5] [6 7] ...
460    """
461
462    require_ntuple(cfg)
463
464    requested_ctx_cnt = ctx_cnt
465
466    # Try to allocate more queues when necessary
467    qcnt = len(_get_rx_cnts(cfg))
468    if qcnt < 2 + 2 * ctx_cnt:
469        try:
470            ksft_pr(f"Increasing queue count {qcnt} -> {2 + 2 * ctx_cnt}")
471            ethtool(f"-L {cfg.ifname} combined {2 + 2 * ctx_cnt}")
472            defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
473        except:
474            raise KsftSkipEx("Not enough queues for the test")
475
476    ports = rand_ports(ctx_cnt)
477
478    # Use queues 0 and 1 for normal traffic
479    ethtool(f"-X {cfg.ifname} equal 2")
480    defer(ethtool, f"-X {cfg.ifname} default")
481
482    for i in range(ctx_cnt):
483        want_cfg = f"start {2 + i * 2} equal 2"
484        create_cfg = want_cfg if create_with_cfg else ""
485
486        try:
487            ctx_id = ethtool_create(cfg, "-X", f"context new {create_cfg}")
488            defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
489        except CmdExitFailure:
490            # try to carry on and skip at the end
491            if i == 0:
492                raise
493            ksft_pr(f"Failed to create context {i + 1}, trying to test what we got")
494            ctx_cnt = i
495            if cfg.context_cnt is None:
496                cfg.context_cnt = ctx_cnt
497            break
498
499        _rss_key_check(cfg, context=ctx_id)
500
501        if not create_with_cfg:
502            ethtool(f"-X {cfg.ifname} context {ctx_id} {want_cfg}")
503            _rss_key_check(cfg, context=ctx_id)
504
505        # Sanity check the context we just created
506        data = get_rss(cfg, ctx_id)
507        ksft_eq(min(data['rss-indirection-table']), 2 + i * 2, "Unexpected context cfg: " + str(data))
508        ksft_eq(max(data['rss-indirection-table']), 2 + i * 2 + 1, "Unexpected context cfg: " + str(data))
509
510        flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {ports[i]} context {ctx_id}"
511        ntuple = ethtool_create(cfg, "-N", flow)
512        defer(ethtool, f"-N {cfg.ifname} delete {ntuple}")
513
514        _ntuple_rule_check(cfg, ntuple, ctx_id)
515
516    for i in range(ctx_cnt):
517        _send_traffic_check(cfg, ports[i], f"context {i}",
518                            { 'target': (2+i*2, 3+i*2),
519                              'noise': (0, 1),
520                              'empty': list(range(2, 2+i*2)) + list(range(4+i*2, 2+2*ctx_cnt)) })
521
522    if requested_ctx_cnt != ctx_cnt:
523        raise KsftSkipEx(f"Tested only {ctx_cnt} contexts, wanted {requested_ctx_cnt}")
524
525
526def test_rss_context4(cfg):
527    test_rss_context(cfg, 4)
528
529
530def test_rss_context32(cfg):
531    test_rss_context(cfg, 32)
532
533
534def test_rss_context4_create_with_cfg(cfg):
535    test_rss_context(cfg, 4, create_with_cfg=True)
536
537
538def test_rss_context_queue_reconfigure(cfg):
539    test_rss_queue_reconfigure(cfg, main_ctx=False)
540
541
542def test_rss_context_out_of_order(cfg, ctx_cnt=4):
543    """
544    Test separating traffic into RSS contexts.
545    Contexts are removed in semi-random order, and steering re-tested
546    to make sure removal doesn't break steering to surviving contexts.
547    Test requires 3 contexts to work.
548    """
549
550    require_ntuple(cfg)
551    require_context_cnt(cfg, 4)
552
553    # Try to allocate more queues when necessary
554    qcnt = len(_get_rx_cnts(cfg))
555    if qcnt < 2 + 2 * ctx_cnt:
556        try:
557            ksft_pr(f"Increasing queue count {qcnt} -> {2 + 2 * ctx_cnt}")
558            ethtool(f"-L {cfg.ifname} combined {2 + 2 * ctx_cnt}")
559            defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
560        except:
561            raise KsftSkipEx("Not enough queues for the test")
562
563    ntuple = []
564    ctx = []
565    ports = rand_ports(ctx_cnt)
566
567    def remove_ctx(idx):
568        ntuple[idx].exec()
569        ntuple[idx] = None
570        ctx[idx].exec()
571        ctx[idx] = None
572
573    def check_traffic():
574        for i in range(ctx_cnt):
575            if ctx[i]:
576                expected = {
577                    'target': (2+i*2, 3+i*2),
578                    'noise': (0, 1),
579                    'empty': list(range(2, 2+i*2)) + list(range(4+i*2, 2+2*ctx_cnt))
580                }
581            else:
582                expected = {
583                    'target': (0, 1),
584                    'empty':  range(2, 2+2*ctx_cnt)
585                }
586
587            _send_traffic_check(cfg, ports[i], f"context {i}", expected)
588
589    # Use queues 0 and 1 for normal traffic
590    ethtool(f"-X {cfg.ifname} equal 2")
591    defer(ethtool, f"-X {cfg.ifname} default")
592
593    for i in range(ctx_cnt):
594        ctx_id = ethtool_create(cfg, "-X", f"context new start {2 + i * 2} equal 2")
595        ctx.append(defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete"))
596
597        flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {ports[i]} context {ctx_id}"
598        ntuple_id = ethtool_create(cfg, "-N", flow)
599        ntuple.append(defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}"))
600
601    check_traffic()
602
603    # Remove middle context
604    remove_ctx(ctx_cnt // 2)
605    check_traffic()
606
607    # Remove first context
608    remove_ctx(0)
609    check_traffic()
610
611    # Remove last context
612    remove_ctx(-1)
613    check_traffic()
614
615
616def test_rss_context_overlap(cfg, other_ctx=0):
617    """
618    Test contexts overlapping with each other.
619    Use 4 queues for the main context, but only queues 2 and 3 for context 1.
620    """
621
622    require_ntuple(cfg)
623    if other_ctx:
624        require_context_cnt(cfg, 2)
625
626    queue_cnt = len(_get_rx_cnts(cfg))
627    if queue_cnt < 4:
628        try:
629            ksft_pr(f"Increasing queue count {queue_cnt} -> 4")
630            ethtool(f"-L {cfg.ifname} combined 4")
631            defer(ethtool, f"-L {cfg.ifname} combined {queue_cnt}")
632        except:
633            raise KsftSkipEx("Not enough queues for the test")
634
635    if other_ctx == 0:
636        ethtool(f"-X {cfg.ifname} equal 4")
637        defer(ethtool, f"-X {cfg.ifname} default")
638    else:
639        other_ctx = ethtool_create(cfg, "-X", "context new")
640        ethtool(f"-X {cfg.ifname} context {other_ctx} equal 4")
641        defer(ethtool, f"-X {cfg.ifname} context {other_ctx} delete")
642
643    ctx_id = ethtool_create(cfg, "-X", "context new")
644    ethtool(f"-X {cfg.ifname} context {ctx_id} start 2 equal 2")
645    defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
646
647    port = rand_port()
648    if other_ctx:
649        flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {other_ctx}"
650        ntuple_id = ethtool_create(cfg, "-N", flow)
651        ntuple = defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
652
653    # Test the main context
654    attempts = 3
655    for attempt in range(attempts):
656        cnts = _get_rx_cnts(cfg)
657        GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000)
658        cnts = _get_rx_cnts(cfg, prev=cnts)
659        if sum(cnts[:2]) >= 7000 and sum(cnts[2:4]) >= 7000:
660            break
661        ksft_pr(f"Skewed queue distribution, attempt {attempt + 1}/{attempts}: " + str(cnts))
662
663    ksft_ge(sum(cnts[ :4]), 20000, "traffic on main context: " + str(cnts))
664    ksft_ge(sum(cnts[ :2]),  7000, "traffic on main context (1/2): " + str(cnts))
665    ksft_ge(sum(cnts[2:4]),  7000, "traffic on main context (2/2): " + str(cnts))
666    if other_ctx == 0:
667        ksft_eq(sum(cnts[4: ]),     0, "traffic on other queues: " + str(cnts))
668
669    # Now create a rule for context 1 and make sure traffic goes to a subset
670    if other_ctx:
671        ntuple.exec()
672    flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id}"
673    ntuple_id = ethtool_create(cfg, "-N", flow)
674    defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
675
676    cnts = _get_rx_cnts(cfg)
677    GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000)
678    cnts = _get_rx_cnts(cfg, prev=cnts)
679
680    directed = sum(cnts[2:4])
681    ksft_lt(sum(cnts[ :2]), directed / 2, "traffic on main context: " + str(cnts))
682    ksft_ge(directed, 20000, "traffic on extra context: " + str(cnts))
683    if other_ctx == 0:
684        ksft_eq(sum(cnts[4: ]),     0, "traffic on other queues: " + str(cnts))
685
686
687def test_rss_context_overlap2(cfg):
688    test_rss_context_overlap(cfg, True)
689
690
691def test_flow_add_context_missing(cfg):
692    """
693    Test that we are not allowed to add a rule pointing to an RSS context
694    which was never created.
695    """
696
697    require_ntuple(cfg)
698
699    # Find a context which doesn't exist
700    for ctx_id in range(1, 100):
701        try:
702            get_rss(cfg, context=ctx_id)
703        except CmdExitFailure:
704            break
705
706    with ksft_raises(CmdExitFailure) as cm:
707        flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port 1234 context {ctx_id}"
708        ntuple_id = ethtool_create(cfg, "-N", flow)
709        ethtool(f"-N {cfg.ifname} delete {ntuple_id}")
710    if cm.exception:
711        ksft_in('Invalid argument', cm.exception.cmd.stderr)
712
713
714def test_delete_rss_context_busy(cfg):
715    """
716    Test that deletion returns -EBUSY when an rss context is being used
717    by an ntuple filter.
718    """
719
720    require_ntuple(cfg)
721
722    # create additional rss context
723    ctx_id = ethtool_create(cfg, "-X", "context new")
724    ctx_deleter = defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
725
726    # utilize context from ntuple filter
727    port = rand_port()
728    flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id}"
729    ntuple_id = ethtool_create(cfg, "-N", flow)
730    defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
731
732    # attempt to delete in-use context
733    try:
734        ctx_deleter.exec_only()
735        ctx_deleter.cancel()
736        raise KsftFailEx(f"deleted context {ctx_id} used by rule {ntuple_id}")
737    except CmdExitFailure:
738        pass
739
740
741def test_rss_ntuple_addition(cfg):
742    """
743    Test that the queue offset (ring_cookie) of an ntuple rule is added
744    to the queue number read from the indirection table.
745    """
746
747    require_ntuple(cfg)
748
749    queue_cnt = len(_get_rx_cnts(cfg))
750    if queue_cnt < 4:
751        try:
752            ksft_pr(f"Increasing queue count {queue_cnt} -> 4")
753            ethtool(f"-L {cfg.ifname} combined 4")
754            defer(ethtool, f"-L {cfg.ifname} combined {queue_cnt}")
755        except:
756            raise KsftSkipEx("Not enough queues for the test")
757
758    # Use queue 0 for normal traffic
759    ethtool(f"-X {cfg.ifname} equal 1")
760    defer(ethtool, f"-X {cfg.ifname} default")
761
762    # create additional rss context
763    ctx_id = ethtool_create(cfg, "-X", "context new equal 2")
764    defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
765
766    # utilize context from ntuple filter
767    port = rand_port()
768    flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id} action 2"
769    try:
770        ntuple_id = ethtool_create(cfg, "-N", flow)
771    except CmdExitFailure:
772        raise KsftSkipEx("Ntuple filter with RSS and nonzero action not supported")
773    defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
774
775    _send_traffic_check(cfg, port, f"context {ctx_id}", { 'target': (2, 3),
776                                                          'empty' : (1,),
777                                                          'noise' : (0,) })
778
779
780def test_rss_default_context_rule(cfg):
781    """
782    Allocate a port, direct this port to context 0, then create a new RSS
783    context and steer all TCP traffic to it (context 1).  Verify that:
784      * Traffic to the specific port continues to use queues of the main
785        context (0/1).
786      * Traffic to any other TCP port is redirected to the new context
787        (queues 2/3).
788    """
789
790    require_ntuple(cfg)
791
792    queue_cnt = len(_get_rx_cnts(cfg))
793    if queue_cnt < 4:
794        try:
795            ksft_pr(f"Increasing queue count {queue_cnt} -> 4")
796            ethtool(f"-L {cfg.ifname} combined 4")
797            defer(ethtool, f"-L {cfg.ifname} combined {queue_cnt}")
798        except Exception as exc:
799            raise KsftSkipEx("Not enough queues for the test") from exc
800
801    # Use queues 0 and 1 for the main context
802    ethtool(f"-X {cfg.ifname} equal 2")
803    defer(ethtool, f"-X {cfg.ifname} default")
804
805    # Create a new RSS context that uses queues 2 and 3
806    ctx_id = ethtool_create(cfg, "-X", "context new start 2 equal 2")
807    defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
808
809    # Generic low-priority rule: redirect all TCP traffic to the new context.
810    # Give it an explicit higher location number (lower priority).
811    flow_generic = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} context {ctx_id} loc 1"
812    ethtool(f"-N {cfg.ifname} {flow_generic}")
813    defer(ethtool, f"-N {cfg.ifname} delete 1")
814
815    ports = rand_ports(2)
816    # Specific high-priority rule for a random port that should stay on context 0.
817    # Assign loc 0 so it is evaluated before the generic rule.
818    port_main = ports[0]
819    flow_main = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port_main} context 0 loc 0"
820    ethtool(f"-N {cfg.ifname} {flow_main}")
821    defer(ethtool, f"-N {cfg.ifname} delete 0")
822
823    _ntuple_rule_check(cfg, 1, ctx_id)
824
825    # Verify that traffic matching the specific rule still goes to queues 0/1
826    _send_traffic_check(cfg, port_main, "context 0",
827                        { 'target': (0, 1),
828                          'empty' : (2, 3) })
829
830    # And that traffic for any other port is steered to the new context
831    port_other = ports[1]
832    _send_traffic_check(cfg, port_other, f"context {ctx_id}",
833                        { 'target': (2, 3),
834                          'noise' : (0, 1) })
835
836
837def _set_flow_hash(cfg, fl_type, fields, context=0):
838    req = {"header": {"dev-index": cfg.ifindex},
839           "flow-hash": {fl_type: fields}}
840    if context:
841        req["context"] = context
842    cfg.ethnl.rss_set(req)
843
844
845def _get_flow_hash(cfg, fl_type, context=0):
846    req = {"header": {"dev-index": cfg.ifindex}}
847    if context:
848        req["context"] = context
849    rss = cfg.ethnl.rss_get(req)
850    return rss.get("flow-hash", {}).get(fl_type, set())
851
852
853def test_rss_context_flow_hash(cfg):
854    """
855    Validate, with traffic, that an additional RSS context honors the
856    flow-hash field selection. If the driver lacks per-context field
857    configuration ("ops->rxfh_per_ctx_fields") fall back to setting the
858    fields on the main context, which the kernel applies device-wide.
859    """
860
861    require_ntuple(cfg)
862
863    queue_cnt = len(_get_rx_cnts(cfg))
864    if queue_cnt < 6:
865        try:
866            ksft_pr(f"Increasing queue count {queue_cnt} -> 6")
867            ethtool(f"-L {cfg.ifname} combined 6")
868            defer(ethtool, f"-L {cfg.ifname} combined {queue_cnt}")
869        except CmdExitFailure as exc:
870            raise KsftSkipEx("Not enough queues for the test") from exc
871
872    fl_type = f"tcp{cfg.addr_ipver}"
873    if not _get_flow_hash(cfg, fl_type):
874        raise KsftSkipEx(f"Device does not report flow-hash for {fl_type}")
875
876    # Reserve queues 0/1 for main, build a new context spanning 2..5
877    ethtool(f"-X {cfg.ifname} equal 2")
878    defer(ethtool, f"-X {cfg.ifname} default")
879    ctx_id = ethtool_create(cfg, "-X", "context new start 2 equal 4")
880    defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
881
882    port = rand_port()
883    flow = f"flow-type {fl_type} dst-ip {cfg.addr} dst-port {port} context {ctx_id}"
884    ntuple = ethtool_create(cfg, "-N", flow)
885    defer(ethtool, f"-N {cfg.ifname} delete {ntuple}")
886
887    ip_only = {"ip-src", "ip-dst"}
888    ip_l4   = ip_only | {"l4-b-0-1", "l4-b-2-3"}
889
890    # Try per-context flow-hash; fall back to main context if unsupported.
891    cfg_ctx = ctx_id
892    try:
893        orig = _get_flow_hash(cfg, fl_type, context=ctx_id)
894        _set_flow_hash(cfg, fl_type, ip_only, context=ctx_id)
895    except NlError:
896        ksft_pr("Per-context flow-hash not supported, using device-wide")
897        cfg_ctx = 0
898        orig = _get_flow_hash(cfg, fl_type)
899        _set_flow_hash(cfg, fl_type, ip_only)
900    defer(_set_flow_hash, cfg, fl_type, orig, context=cfg_ctx)
901
902    def measure():
903        cnts = _get_rx_cnts(cfg)
904        GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000)
905        cnts = _get_rx_cnts(cfg, prev=cnts)
906        ctx_cnts = cnts[2:6]
907        directed = sum(ctx_cnts)
908        used = sum(1 for c in ctx_cnts if c > directed / 200)
909        return cnts, directed, used
910
911    # IP-only hash: iperf3 streams share src/dst IP, all should land on the
912    # same queue inside the context's range.
913    cnts, directed, used = measure()
914    ksft_ge(directed, 20000, f"traffic on context {ctx_id} (IP-only): {cnts}")
915    ksft_eq(used, 1, f"IP-only hash should use one queue in context {ctx_id}, got: {cnts}")
916
917    # IP+L4 hash: streams have distinct src ports, traffic should spread.
918    _set_flow_hash(cfg, fl_type, ip_l4, context=cfg_ctx)
919
920    cnts, directed, used = measure()
921    ksft_ge(directed, 20000, f"traffic on context {ctx_id} (IP+L4): {cnts}")
922    ksft_ge(used, 2, f"IP+L4 hash should spread across context {ctx_id} queues, got: {cnts}")
923
924
925@ksft_disruptive
926def test_rss_context_persist_ifupdown(cfg, pre_down=False):
927    """
928    Test that RSS contexts and their associated ntuple filters persist across
929    an interface down/up cycle.
930
931    """
932
933    require_ntuple(cfg)
934
935    qcnt = len(_get_rx_cnts(cfg))
936    if qcnt < 6:
937        try:
938            ethtool(f"-L {cfg.ifname} combined 6")
939            defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
940        except Exception as exc:
941            raise KsftSkipEx("Not enough queues for the test") from exc
942
943    ethtool(f"-X {cfg.ifname} equal 2")
944    defer(ethtool, f"-X {cfg.ifname} default")
945
946    ifup = defer(ip, f"link set dev {cfg.ifname} up")
947    if pre_down:
948        ip(f"link set dev {cfg.ifname} down")
949
950    try:
951        ctx1_id = ethtool_create(cfg, "-X", "context new start 2 equal 2")
952        defer(ethtool, f"-X {cfg.ifname} context {ctx1_id} delete")
953    except CmdExitFailure as exc:
954        raise KsftSkipEx("Create context not supported with interface down") from exc
955
956    ctx2_id = ethtool_create(cfg, "-X", "context new start 4 equal 2")
957    defer(ethtool, f"-X {cfg.ifname} context {ctx2_id} delete")
958
959    port_ctx2 = rand_port()
960    flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port_ctx2} context {ctx2_id}"
961    ntuple_id = ethtool_create(cfg, "-N", flow)
962    defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
963
964    if not pre_down:
965        ip(f"link set dev {cfg.ifname} down")
966    ifup.exec()
967
968    wait_file(f"/sys/class/net/{cfg.ifname}/carrier",
969        lambda x: x.strip() == "1", deadline=20)
970
971    remote_addr = cfg.remote_addr_v[cfg.addr_ipver]
972    for _ in range(10):
973        if cmd(f"ping -c 1 -W 1 {remote_addr}", fail=False).ret == 0:
974            break
975        time.sleep(1)
976    else:
977        raise KsftSkipEx("Cannot reach remote host after interface up")
978
979    ctxs = cfg.ethnl.rss_get({'header': {'dev-name': cfg.ifname}}, dump=True)
980
981    data1 = [c for c in ctxs if c.get('context') == ctx1_id]
982    ksft_eq(len(data1), 1, f"Context {ctx1_id} should persist after ifup")
983
984    data2 = [c for c in ctxs if c.get('context') == ctx2_id]
985    ksft_eq(len(data2), 1, f"Context {ctx2_id} should persist after ifup")
986
987    _ntuple_rule_check(cfg, ntuple_id, ctx2_id)
988
989    cnts = _get_rx_cnts(cfg)
990    GenerateTraffic(cfg).wait_pkts_and_stop(20000)
991    cnts = _get_rx_cnts(cfg, prev=cnts)
992
993    main_traffic = sum(cnts[0:2])
994    ksft_ge(main_traffic, 18000, f"Main context traffic distribution: {cnts}")
995    ksft_lt(sum(cnts[2:6]), 500, f"Other context queues should be mostly empty: {cnts}")
996
997    _send_traffic_check(cfg, port_ctx2, f"context {ctx2_id}",
998                        {'target': (4, 5),
999                         'noise': (0, 1),
1000                         'empty': (2, 3)})
1001
1002
1003def test_rss_context_persist_create_and_ifdown(cfg):
1004    """
1005    Create RSS contexts then cycle the interface down and up.
1006    """
1007    test_rss_context_persist_ifupdown(cfg, pre_down=False)
1008
1009
1010def test_rss_context_persist_ifdown_and_create(cfg):
1011    """
1012    Bring interface down first, then create RSS contexts and bring up.
1013    """
1014    test_rss_context_persist_ifupdown(cfg, pre_down=True)
1015
1016
1017def main() -> None:
1018    with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
1019        cfg.context_cnt = None
1020        cfg.ethnl = EthtoolFamily()
1021        cfg.netdevnl = NetdevFamily()
1022
1023        ksft_run([test_rss_key_indir, test_rss_queue_reconfigure,
1024                  test_rss_resize, test_hitless_key_update,
1025                  test_rss_context, test_rss_context4, test_rss_context32,
1026                  test_rss_context_dump, test_rss_context_queue_reconfigure,
1027                  test_rss_context_overlap, test_rss_context_overlap2,
1028                  test_rss_context_out_of_order, test_rss_context4_create_with_cfg,
1029                  test_flow_add_context_missing,
1030                  test_delete_rss_context_busy, test_rss_ntuple_addition,
1031                  test_rss_default_context_rule,
1032                  test_rss_context_flow_hash,
1033                  test_rss_context_persist_create_and_ifdown,
1034                  test_rss_context_persist_ifdown_and_create],
1035                 args=(cfg, ))
1036    ksft_exit()
1037
1038
1039if __name__ == "__main__":
1040    main()
1041