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