xref: /linux/tools/testing/selftests/drivers/net/hw/rss_ctx.py (revision c301658dfe08880a6a864fe192ca5967994c2e54)
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    cnts = _get_rx_cnts(cfg)
655    GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000)
656    cnts = _get_rx_cnts(cfg, prev=cnts)
657
658    ksft_ge(sum(cnts[ :4]), 20000, "traffic on main context: " + str(cnts))
659    ksft_ge(sum(cnts[ :2]),  7000, "traffic on main context (1/2): " + str(cnts))
660    ksft_ge(sum(cnts[2:4]),  7000, "traffic on main context (2/2): " + str(cnts))
661    if other_ctx == 0:
662        ksft_eq(sum(cnts[4: ]),     0, "traffic on other queues: " + str(cnts))
663
664    # Now create a rule for context 1 and make sure traffic goes to a subset
665    if other_ctx:
666        ntuple.exec()
667    flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id}"
668    ntuple_id = ethtool_create(cfg, "-N", flow)
669    defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
670
671    cnts = _get_rx_cnts(cfg)
672    GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000)
673    cnts = _get_rx_cnts(cfg, prev=cnts)
674
675    directed = sum(cnts[2:4])
676    ksft_lt(sum(cnts[ :2]), directed / 2, "traffic on main context: " + str(cnts))
677    ksft_ge(directed, 20000, "traffic on extra context: " + str(cnts))
678    if other_ctx == 0:
679        ksft_eq(sum(cnts[4: ]),     0, "traffic on other queues: " + str(cnts))
680
681
682def test_rss_context_overlap2(cfg):
683    test_rss_context_overlap(cfg, True)
684
685
686def test_flow_add_context_missing(cfg):
687    """
688    Test that we are not allowed to add a rule pointing to an RSS context
689    which was never created.
690    """
691
692    require_ntuple(cfg)
693
694    # Find a context which doesn't exist
695    for ctx_id in range(1, 100):
696        try:
697            get_rss(cfg, context=ctx_id)
698        except CmdExitFailure:
699            break
700
701    with ksft_raises(CmdExitFailure) as cm:
702        flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port 1234 context {ctx_id}"
703        ntuple_id = ethtool_create(cfg, "-N", flow)
704        ethtool(f"-N {cfg.ifname} delete {ntuple_id}")
705    if cm.exception:
706        ksft_in('Invalid argument', cm.exception.cmd.stderr)
707
708
709def test_delete_rss_context_busy(cfg):
710    """
711    Test that deletion returns -EBUSY when an rss context is being used
712    by an ntuple filter.
713    """
714
715    require_ntuple(cfg)
716
717    # create additional rss context
718    ctx_id = ethtool_create(cfg, "-X", "context new")
719    ctx_deleter = defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
720
721    # utilize context from ntuple filter
722    port = rand_port()
723    flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id}"
724    ntuple_id = ethtool_create(cfg, "-N", flow)
725    defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
726
727    # attempt to delete in-use context
728    try:
729        ctx_deleter.exec_only()
730        ctx_deleter.cancel()
731        raise KsftFailEx(f"deleted context {ctx_id} used by rule {ntuple_id}")
732    except CmdExitFailure:
733        pass
734
735
736def test_rss_ntuple_addition(cfg):
737    """
738    Test that the queue offset (ring_cookie) of an ntuple rule is added
739    to the queue number read from the indirection table.
740    """
741
742    require_ntuple(cfg)
743
744    queue_cnt = len(_get_rx_cnts(cfg))
745    if queue_cnt < 4:
746        try:
747            ksft_pr(f"Increasing queue count {queue_cnt} -> 4")
748            ethtool(f"-L {cfg.ifname} combined 4")
749            defer(ethtool, f"-L {cfg.ifname} combined {queue_cnt}")
750        except:
751            raise KsftSkipEx("Not enough queues for the test")
752
753    # Use queue 0 for normal traffic
754    ethtool(f"-X {cfg.ifname} equal 1")
755    defer(ethtool, f"-X {cfg.ifname} default")
756
757    # create additional rss context
758    ctx_id = ethtool_create(cfg, "-X", "context new equal 2")
759    defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
760
761    # utilize context from ntuple filter
762    port = rand_port()
763    flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id} action 2"
764    try:
765        ntuple_id = ethtool_create(cfg, "-N", flow)
766    except CmdExitFailure:
767        raise KsftSkipEx("Ntuple filter with RSS and nonzero action not supported")
768    defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
769
770    _send_traffic_check(cfg, port, f"context {ctx_id}", { 'target': (2, 3),
771                                                          'empty' : (1,),
772                                                          'noise' : (0,) })
773
774
775def test_rss_default_context_rule(cfg):
776    """
777    Allocate a port, direct this port to context 0, then create a new RSS
778    context and steer all TCP traffic to it (context 1).  Verify that:
779      * Traffic to the specific port continues to use queues of the main
780        context (0/1).
781      * Traffic to any other TCP port is redirected to the new context
782        (queues 2/3).
783    """
784
785    require_ntuple(cfg)
786
787    queue_cnt = len(_get_rx_cnts(cfg))
788    if queue_cnt < 4:
789        try:
790            ksft_pr(f"Increasing queue count {queue_cnt} -> 4")
791            ethtool(f"-L {cfg.ifname} combined 4")
792            defer(ethtool, f"-L {cfg.ifname} combined {queue_cnt}")
793        except Exception as exc:
794            raise KsftSkipEx("Not enough queues for the test") from exc
795
796    # Use queues 0 and 1 for the main context
797    ethtool(f"-X {cfg.ifname} equal 2")
798    defer(ethtool, f"-X {cfg.ifname} default")
799
800    # Create a new RSS context that uses queues 2 and 3
801    ctx_id = ethtool_create(cfg, "-X", "context new start 2 equal 2")
802    defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
803
804    # Generic low-priority rule: redirect all TCP traffic to the new context.
805    # Give it an explicit higher location number (lower priority).
806    flow_generic = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} context {ctx_id} loc 1"
807    ethtool(f"-N {cfg.ifname} {flow_generic}")
808    defer(ethtool, f"-N {cfg.ifname} delete 1")
809
810    ports = rand_ports(2)
811    # Specific high-priority rule for a random port that should stay on context 0.
812    # Assign loc 0 so it is evaluated before the generic rule.
813    port_main = ports[0]
814    flow_main = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port_main} context 0 loc 0"
815    ethtool(f"-N {cfg.ifname} {flow_main}")
816    defer(ethtool, f"-N {cfg.ifname} delete 0")
817
818    _ntuple_rule_check(cfg, 1, ctx_id)
819
820    # Verify that traffic matching the specific rule still goes to queues 0/1
821    _send_traffic_check(cfg, port_main, "context 0",
822                        { 'target': (0, 1),
823                          'empty' : (2, 3) })
824
825    # And that traffic for any other port is steered to the new context
826    port_other = ports[1]
827    _send_traffic_check(cfg, port_other, f"context {ctx_id}",
828                        { 'target': (2, 3),
829                          'noise' : (0, 1) })
830
831
832def _set_flow_hash(cfg, fl_type, fields, context=0):
833    req = {"header": {"dev-index": cfg.ifindex},
834           "flow-hash": {fl_type: fields}}
835    if context:
836        req["context"] = context
837    cfg.ethnl.rss_set(req)
838
839
840def _get_flow_hash(cfg, fl_type, context=0):
841    req = {"header": {"dev-index": cfg.ifindex}}
842    if context:
843        req["context"] = context
844    rss = cfg.ethnl.rss_get(req)
845    return rss.get("flow-hash", {}).get(fl_type, set())
846
847
848def test_rss_context_flow_hash(cfg):
849    """
850    Validate, with traffic, that an additional RSS context honors the
851    flow-hash field selection. If the driver lacks per-context field
852    configuration ("ops->rxfh_per_ctx_fields") fall back to setting the
853    fields on the main context, which the kernel applies device-wide.
854    """
855
856    require_ntuple(cfg)
857
858    queue_cnt = len(_get_rx_cnts(cfg))
859    if queue_cnt < 6:
860        try:
861            ksft_pr(f"Increasing queue count {queue_cnt} -> 6")
862            ethtool(f"-L {cfg.ifname} combined 6")
863            defer(ethtool, f"-L {cfg.ifname} combined {queue_cnt}")
864        except CmdExitFailure as exc:
865            raise KsftSkipEx("Not enough queues for the test") from exc
866
867    fl_type = f"tcp{cfg.addr_ipver}"
868    if not _get_flow_hash(cfg, fl_type):
869        raise KsftSkipEx(f"Device does not report flow-hash for {fl_type}")
870
871    # Reserve queues 0/1 for main, build a new context spanning 2..5
872    ethtool(f"-X {cfg.ifname} equal 2")
873    defer(ethtool, f"-X {cfg.ifname} default")
874    ctx_id = ethtool_create(cfg, "-X", "context new start 2 equal 4")
875    defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
876
877    port = rand_port()
878    flow = f"flow-type {fl_type} dst-ip {cfg.addr} dst-port {port} context {ctx_id}"
879    ntuple = ethtool_create(cfg, "-N", flow)
880    defer(ethtool, f"-N {cfg.ifname} delete {ntuple}")
881
882    ip_only = {"ip-src", "ip-dst"}
883    ip_l4   = ip_only | {"l4-b-0-1", "l4-b-2-3"}
884
885    # Try per-context flow-hash; fall back to main context if unsupported.
886    cfg_ctx = ctx_id
887    try:
888        orig = _get_flow_hash(cfg, fl_type, context=ctx_id)
889        _set_flow_hash(cfg, fl_type, ip_only, context=ctx_id)
890    except NlError:
891        ksft_pr("Per-context flow-hash not supported, using device-wide")
892        cfg_ctx = 0
893        orig = _get_flow_hash(cfg, fl_type)
894        _set_flow_hash(cfg, fl_type, ip_only)
895    defer(_set_flow_hash, cfg, fl_type, orig, context=cfg_ctx)
896
897    def measure():
898        cnts = _get_rx_cnts(cfg)
899        GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000)
900        cnts = _get_rx_cnts(cfg, prev=cnts)
901        ctx_cnts = cnts[2:6]
902        directed = sum(ctx_cnts)
903        used = sum(1 for c in ctx_cnts if c > directed / 200)
904        return cnts, directed, used
905
906    # IP-only hash: iperf3 streams share src/dst IP, all should land on the
907    # same queue inside the context's range.
908    cnts, directed, used = measure()
909    ksft_ge(directed, 20000, f"traffic on context {ctx_id} (IP-only): {cnts}")
910    ksft_eq(used, 1, f"IP-only hash should use one queue in context {ctx_id}, got: {cnts}")
911
912    # IP+L4 hash: streams have distinct src ports, traffic should spread.
913    _set_flow_hash(cfg, fl_type, ip_l4, context=cfg_ctx)
914
915    cnts, directed, used = measure()
916    ksft_ge(directed, 20000, f"traffic on context {ctx_id} (IP+L4): {cnts}")
917    ksft_ge(used, 2, f"IP+L4 hash should spread across context {ctx_id} queues, got: {cnts}")
918
919
920@ksft_disruptive
921def test_rss_context_persist_ifupdown(cfg, pre_down=False):
922    """
923    Test that RSS contexts and their associated ntuple filters persist across
924    an interface down/up cycle.
925
926    """
927
928    require_ntuple(cfg)
929
930    qcnt = len(_get_rx_cnts(cfg))
931    if qcnt < 6:
932        try:
933            ethtool(f"-L {cfg.ifname} combined 6")
934            defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
935        except Exception as exc:
936            raise KsftSkipEx("Not enough queues for the test") from exc
937
938    ethtool(f"-X {cfg.ifname} equal 2")
939    defer(ethtool, f"-X {cfg.ifname} default")
940
941    ifup = defer(ip, f"link set dev {cfg.ifname} up")
942    if pre_down:
943        ip(f"link set dev {cfg.ifname} down")
944
945    try:
946        ctx1_id = ethtool_create(cfg, "-X", "context new start 2 equal 2")
947        defer(ethtool, f"-X {cfg.ifname} context {ctx1_id} delete")
948    except CmdExitFailure as exc:
949        raise KsftSkipEx("Create context not supported with interface down") from exc
950
951    ctx2_id = ethtool_create(cfg, "-X", "context new start 4 equal 2")
952    defer(ethtool, f"-X {cfg.ifname} context {ctx2_id} delete")
953
954    port_ctx2 = rand_port()
955    flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port_ctx2} context {ctx2_id}"
956    ntuple_id = ethtool_create(cfg, "-N", flow)
957    defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
958
959    if not pre_down:
960        ip(f"link set dev {cfg.ifname} down")
961    ifup.exec()
962
963    wait_file(f"/sys/class/net/{cfg.ifname}/carrier",
964        lambda x: x.strip() == "1", deadline=20)
965
966    remote_addr = cfg.remote_addr_v[cfg.addr_ipver]
967    for _ in range(10):
968        if cmd(f"ping -c 1 -W 1 {remote_addr}", fail=False).ret == 0:
969            break
970        time.sleep(1)
971    else:
972        raise KsftSkipEx("Cannot reach remote host after interface up")
973
974    ctxs = cfg.ethnl.rss_get({'header': {'dev-name': cfg.ifname}}, dump=True)
975
976    data1 = [c for c in ctxs if c.get('context') == ctx1_id]
977    ksft_eq(len(data1), 1, f"Context {ctx1_id} should persist after ifup")
978
979    data2 = [c for c in ctxs if c.get('context') == ctx2_id]
980    ksft_eq(len(data2), 1, f"Context {ctx2_id} should persist after ifup")
981
982    _ntuple_rule_check(cfg, ntuple_id, ctx2_id)
983
984    cnts = _get_rx_cnts(cfg)
985    GenerateTraffic(cfg).wait_pkts_and_stop(20000)
986    cnts = _get_rx_cnts(cfg, prev=cnts)
987
988    main_traffic = sum(cnts[0:2])
989    ksft_ge(main_traffic, 18000, f"Main context traffic distribution: {cnts}")
990    ksft_lt(sum(cnts[2:6]), 500, f"Other context queues should be mostly empty: {cnts}")
991
992    _send_traffic_check(cfg, port_ctx2, f"context {ctx2_id}",
993                        {'target': (4, 5),
994                         'noise': (0, 1),
995                         'empty': (2, 3)})
996
997
998def test_rss_context_persist_create_and_ifdown(cfg):
999    """
1000    Create RSS contexts then cycle the interface down and up.
1001    """
1002    test_rss_context_persist_ifupdown(cfg, pre_down=False)
1003
1004
1005def test_rss_context_persist_ifdown_and_create(cfg):
1006    """
1007    Bring interface down first, then create RSS contexts and bring up.
1008    """
1009    test_rss_context_persist_ifupdown(cfg, pre_down=True)
1010
1011
1012def main() -> None:
1013    with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
1014        cfg.context_cnt = None
1015        cfg.ethnl = EthtoolFamily()
1016        cfg.netdevnl = NetdevFamily()
1017
1018        ksft_run([test_rss_key_indir, test_rss_queue_reconfigure,
1019                  test_rss_resize, test_hitless_key_update,
1020                  test_rss_context, test_rss_context4, test_rss_context32,
1021                  test_rss_context_dump, test_rss_context_queue_reconfigure,
1022                  test_rss_context_overlap, test_rss_context_overlap2,
1023                  test_rss_context_out_of_order, test_rss_context4_create_with_cfg,
1024                  test_flow_add_context_missing,
1025                  test_delete_rss_context_busy, test_rss_ntuple_addition,
1026                  test_rss_default_context_rule,
1027                  test_rss_context_flow_hash,
1028                  test_rss_context_persist_create_and_ifdown,
1029                  test_rss_context_persist_ifdown_and_create],
1030                 args=(cfg, ))
1031    ksft_exit()
1032
1033
1034if __name__ == "__main__":
1035    main()
1036