xref: /linux/tools/testing/selftests/drivers/net/hw/rss_ctx.py (revision b9c8fc2caea6ff7e45c6942de8fee53515c66b34)
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_run, ksft_pr, ksft_exit
9from lib.py import ksft_eq, ksft_ne, ksft_ge, ksft_in, ksft_lt, ksft_true, ksft_raises
10from lib.py import NetDrvEpEnv
11from lib.py import EthtoolFamily, NetdevFamily
12from lib.py import KsftSkipEx, KsftFailEx
13from lib.py import ksft_disruptive
14from lib.py import rand_port
15from lib.py import cmd, ethtool, ip, defer, GenerateTraffic, CmdExitFailure, wait_file
16
17
18def _rss_key_str(key):
19    return ":".join(["{:02x}".format(x) for x in key])
20
21
22def _rss_key_rand(length):
23    return [random.randint(0, 255) for _ in range(length)]
24
25
26def _rss_key_check(cfg, data=None, context=0):
27    if data is None:
28        data = get_rss(cfg, context=context)
29    if 'rss-hash-key' not in data:
30        return
31    non_zero = [x for x in data['rss-hash-key'] if x != 0]
32    ksft_eq(bool(non_zero), True, comment=f"RSS key is all zero {data['rss-hash-key']}")
33
34
35def get_rss(cfg, context=0):
36    return ethtool(f"-x {cfg.ifname} context {context}", json=True)[0]
37
38
39def get_drop_err_sum(cfg):
40    stats = ip("-s -s link show dev " + cfg.ifname, json=True)[0]
41    cnt = 0
42    for key in ['errors', 'dropped', 'over_errors', 'fifo_errors',
43                'length_errors', 'crc_errors', 'missed_errors',
44                'frame_errors']:
45        cnt += stats["stats64"]["rx"][key]
46    return cnt, stats["stats64"]["tx"]["carrier_changes"]
47
48
49def ethtool_create(cfg, act, opts):
50    output = ethtool(f"{act} {cfg.ifname} {opts}").stdout
51    # Output will be something like: "New RSS context is 1" or
52    # "Added rule with ID 7", we want the integer from the end
53    return int(output.split()[-1])
54
55
56def require_ntuple(cfg):
57    features = ethtool(f"-k {cfg.ifname}", json=True)[0]
58    if not features["ntuple-filters"]["active"]:
59        # ntuple is more of a capability than a config knob, don't bother
60        # trying to enable it (until some driver actually needs it).
61        raise KsftSkipEx("Ntuple filters not enabled on the device: " + str(features["ntuple-filters"]))
62
63
64def require_context_cnt(cfg, need_cnt):
65    # There's no good API to get the context count, so the tests
66    # which try to add a lot opportunisitically set the count they
67    # discovered. Careful with test ordering!
68    if need_cnt and cfg.context_cnt and cfg.context_cnt < need_cnt:
69        raise KsftSkipEx(f"Test requires at least {need_cnt} contexts, but device only has {cfg.context_cnt}")
70
71
72# Get Rx packet counts for all queues, as a simple list of integers
73# if @prev is specified the prev counts will be subtracted
74def _get_rx_cnts(cfg, prev=None):
75    cfg.wait_hw_stats_settle()
76    data = cfg.netdevnl.qstats_get({"ifindex": cfg.ifindex, "scope": ["queue"]}, dump=True)
77    data = [x for x in data if x['queue-type'] == "rx"]
78    max_q = max([x["queue-id"] for x in data])
79    queue_stats = [0] * (max_q + 1)
80    for q in data:
81        queue_stats[q["queue-id"]] = q["rx-packets"]
82        if prev and q["queue-id"] < len(prev):
83            queue_stats[q["queue-id"]] -= prev[q["queue-id"]]
84    return queue_stats
85
86
87def _send_traffic_check(cfg, port, name, params):
88    # params is a dict with 3 possible keys:
89    #  - "target": required, which queues we expect to get iperf traffic
90    #  - "empty": optional, which queues should see no traffic at all
91    #  - "noise": optional, which queues we expect to see low traffic;
92    #             used for queues of the main context, since some background
93    #             OS activity may use those queues while we're testing
94    # the value for each is a list, or some other iterable containing queue ids.
95
96    cnts = _get_rx_cnts(cfg)
97    GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000)
98    cnts = _get_rx_cnts(cfg, prev=cnts)
99
100    directed = sum(cnts[i] for i in params['target'])
101
102    ksft_ge(directed, 20000, f"traffic on {name}: " + str(cnts))
103    if params.get('noise'):
104        ksft_lt(sum(cnts[i] for i in params['noise']), directed / 2,
105                f"traffic on other queues ({name})':" + str(cnts))
106    if params.get('empty'):
107        ksft_eq(sum(cnts[i] for i in params['empty']), 0,
108                f"traffic on inactive queues ({name}): " + str(cnts))
109
110
111def _ntuple_rule_check(cfg, rule_id, ctx_id):
112    """Check that ntuple rule references RSS context ID"""
113    text = ethtool(f"-n {cfg.ifname} rule {rule_id}").stdout
114    pattern = f"RSS Context (ID: )?{ctx_id}"
115    ksft_true(re.search(pattern, text), "RSS context not referenced in ntuple rule")
116
117
118def test_rss_key_indir(cfg):
119    """Test basics like updating the main RSS key and indirection table."""
120
121    qcnt = len(_get_rx_cnts(cfg))
122    if qcnt < 3:
123        raise KsftSkipEx("Device has fewer than 3 queues (or doesn't support queue stats)")
124
125    data = get_rss(cfg)
126    want_keys = ['rss-hash-key', 'rss-hash-function', 'rss-indirection-table']
127    for k in want_keys:
128        if k not in data:
129            raise KsftFailEx("ethtool results missing key: " + k)
130        if not data[k]:
131            raise KsftFailEx(f"ethtool results empty for '{k}': {data[k]}")
132
133    _rss_key_check(cfg, data=data)
134    key_len = len(data['rss-hash-key'])
135
136    # Set the key
137    key = _rss_key_rand(key_len)
138    ethtool(f"-X {cfg.ifname} hkey " + _rss_key_str(key))
139
140    data = get_rss(cfg)
141    ksft_eq(key, data['rss-hash-key'])
142
143    # Set the indirection table and the key together
144    key = _rss_key_rand(key_len)
145    ethtool(f"-X {cfg.ifname} equal 3 hkey " + _rss_key_str(key))
146    reset_indir = defer(ethtool, f"-X {cfg.ifname} default")
147
148    data = get_rss(cfg)
149    _rss_key_check(cfg, data=data)
150    ksft_eq(0, min(data['rss-indirection-table']))
151    ksft_eq(2, max(data['rss-indirection-table']))
152
153    # Reset indirection table and set the key
154    key = _rss_key_rand(key_len)
155    ethtool(f"-X {cfg.ifname} default hkey " + _rss_key_str(key))
156    data = get_rss(cfg)
157    _rss_key_check(cfg, data=data)
158    ksft_eq(0, min(data['rss-indirection-table']))
159    ksft_eq(qcnt - 1, max(data['rss-indirection-table']))
160
161    # Set the indirection table
162    ethtool(f"-X {cfg.ifname} equal 2")
163    data = get_rss(cfg)
164    ksft_eq(0, min(data['rss-indirection-table']))
165    ksft_eq(1, max(data['rss-indirection-table']))
166
167    # Check we only get traffic on the first 2 queues
168    cnts = _get_rx_cnts(cfg)
169    GenerateTraffic(cfg).wait_pkts_and_stop(20000)
170    cnts = _get_rx_cnts(cfg, prev=cnts)
171    # 2 queues, 20k packets, must be at least 5k per queue
172    ksft_ge(cnts[0], 5000, "traffic on main context (1/2): " + str(cnts))
173    ksft_ge(cnts[1], 5000, "traffic on main context (2/2): " + str(cnts))
174    # The other queues should be unused
175    ksft_eq(sum(cnts[2:]), 0, "traffic on unused queues: " + str(cnts))
176
177    # Restore, and check traffic gets spread again
178    reset_indir.exec()
179
180    cnts = _get_rx_cnts(cfg)
181    GenerateTraffic(cfg).wait_pkts_and_stop(20000)
182    cnts = _get_rx_cnts(cfg, prev=cnts)
183    if qcnt > 4:
184        # First two queues get less traffic than all the rest
185        ksft_lt(sum(cnts[:2]), sum(cnts[2:]),
186                "traffic distributed: " + str(cnts))
187    else:
188        # When queue count is low make sure third queue got significant pkts
189        ksft_ge(cnts[2], 3500, "traffic distributed: " + str(cnts))
190
191
192def test_rss_queue_reconfigure(cfg, main_ctx=True):
193    """Make sure queue changes can't override requested RSS config.
194
195    By default main RSS table should change to include all queues.
196    When user sets a specific RSS config the driver should preserve it,
197    even when queue count changes. Driver should refuse to deactivate
198    queues used in the user-set RSS config.
199    """
200
201    if not main_ctx:
202        require_ntuple(cfg)
203
204    # Start with 4 queues, an arbitrary known number.
205    try:
206        qcnt = len(_get_rx_cnts(cfg))
207        ethtool(f"-L {cfg.ifname} combined 4")
208        defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
209    except:
210        raise KsftSkipEx("Not enough queues for the test or qstat not supported")
211
212    if main_ctx:
213        ctx_id = 0
214        ctx_ref = ""
215    else:
216        ctx_id = ethtool_create(cfg, "-X", "context new")
217        ctx_ref = f"context {ctx_id}"
218        defer(ethtool, f"-X {cfg.ifname} {ctx_ref} delete")
219
220    # Indirection table should be distributing to all queues.
221    data = get_rss(cfg, context=ctx_id)
222    ksft_eq(0, min(data['rss-indirection-table']))
223    ksft_eq(3, max(data['rss-indirection-table']))
224
225    # Increase queues, indirection table should be distributing to all queues.
226    # It's unclear whether tables of additional contexts should be reset, too.
227    if main_ctx:
228        ethtool(f"-L {cfg.ifname} combined 5")
229        data = get_rss(cfg)
230        ksft_eq(0, min(data['rss-indirection-table']))
231        ksft_eq(4, max(data['rss-indirection-table']))
232        ethtool(f"-L {cfg.ifname} combined 4")
233
234    # Configure the table explicitly
235    port = rand_port()
236    ethtool(f"-X {cfg.ifname} {ctx_ref} weight 1 0 0 1")
237    if main_ctx:
238        other_key = 'empty'
239        defer(ethtool, f"-X {cfg.ifname} default")
240    else:
241        other_key = 'noise'
242        flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id}"
243        ntuple = ethtool_create(cfg, "-N", flow)
244        defer(ethtool, f"-N {cfg.ifname} delete {ntuple}")
245
246    _send_traffic_check(cfg, port, ctx_ref, { 'target': (0, 3),
247                                              other_key: (1, 2) })
248
249    # We should be able to increase queues, but table should be left untouched
250    ethtool(f"-L {cfg.ifname} combined 5")
251    data = get_rss(cfg, context=ctx_id)
252    ksft_eq({0, 3}, set(data['rss-indirection-table']))
253
254    _send_traffic_check(cfg, port, ctx_ref, { 'target': (0, 3),
255                                              other_key: (1, 2, 4) })
256
257    # Setting queue count to 3 should fail, queue 3 is used
258    try:
259        ethtool(f"-L {cfg.ifname} combined 3")
260    except CmdExitFailure:
261        pass
262    else:
263        raise Exception(f"Driver didn't prevent us from deactivating a used queue (context {ctx_id})")
264
265    if not main_ctx:
266        ethtool(f"-L {cfg.ifname} combined 4")
267        flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id} action 1"
268        try:
269            # this targets queue 4, which doesn't exist
270            ntuple2 = ethtool_create(cfg, "-N", flow)
271            defer(ethtool, f"-N {cfg.ifname} delete {ntuple2}")
272        except CmdExitFailure:
273            pass
274        else:
275            raise Exception(f"Driver didn't prevent us from targeting a nonexistent queue (context {ctx_id})")
276        # change the table to target queues 0 and 2
277        ethtool(f"-X {cfg.ifname} {ctx_ref} weight 1 0 1 0")
278        # ntuple rule therefore targets queues 1 and 3
279        try:
280            ntuple2 = ethtool_create(cfg, "-N", flow)
281        except CmdExitFailure:
282            ksft_pr("Driver does not support rss + queue offset")
283            return
284
285        defer(ethtool, f"-N {cfg.ifname} delete {ntuple2}")
286        # should replace existing filter
287        ksft_eq(ntuple, ntuple2)
288        _send_traffic_check(cfg, port, ctx_ref, { 'target': (1, 3),
289                                                  'noise' : (0, 2) })
290        # Setting queue count to 3 should fail, queue 3 is used
291        try:
292            ethtool(f"-L {cfg.ifname} combined 3")
293        except CmdExitFailure:
294            pass
295        else:
296            raise Exception(f"Driver didn't prevent us from deactivating a used queue (context {ctx_id})")
297
298
299def test_rss_resize(cfg):
300    """Test resizing of the RSS table.
301
302    Some devices dynamically increase and decrease the size of the RSS
303    indirection table based on the number of enabled queues.
304    When that happens driver must maintain the balance of entries
305    (preferably duplicating the smaller table).
306    """
307
308    channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
309    ch_max = channels['combined-max']
310    qcnt = channels['combined-count']
311
312    if ch_max < 2:
313        raise KsftSkipEx(f"Not enough queues for the test: {ch_max}")
314
315    ethtool(f"-L {cfg.ifname} combined 2")
316    defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
317
318    ethtool(f"-X {cfg.ifname} weight 1 7")
319    defer(ethtool, f"-X {cfg.ifname} default")
320
321    ethtool(f"-L {cfg.ifname} combined {ch_max}")
322    data = get_rss(cfg)
323    ksft_eq(0, min(data['rss-indirection-table']))
324    ksft_eq(1, max(data['rss-indirection-table']))
325
326    ksft_eq(7,
327            data['rss-indirection-table'].count(1) /
328            data['rss-indirection-table'].count(0),
329            f"Table imbalance after resize: {data['rss-indirection-table']}")
330
331
332def test_hitless_key_update(cfg):
333    """Test that flows may be rehashed without impacting traffic.
334
335    Some workloads may want to rehash the flows in response to an imbalance.
336    Most effective way to do that is changing the RSS key. Check that changing
337    the key does not cause link flaps or traffic disruption.
338
339    Disrupting traffic for key update is not a bug, but makes the key
340    update unusable for rehashing under load.
341    """
342    data = get_rss(cfg)
343    key_len = len(data['rss-hash-key'])
344
345    ethnl = EthtoolFamily()
346    key = random.randbytes(key_len)
347
348    tgen = GenerateTraffic(cfg)
349    try:
350        errors0, carrier0 = get_drop_err_sum(cfg)
351        t0 = datetime.datetime.now()
352        ethnl.rss_set({"header": {"dev-index": cfg.ifindex}, "hkey": key})
353        t1 = datetime.datetime.now()
354        errors1, carrier1 = get_drop_err_sum(cfg)
355    finally:
356        tgen.wait_pkts_and_stop(5000)
357
358    ksft_lt((t1 - t0).total_seconds(), 0.15)
359    ksft_eq(errors1 - errors1, 0)
360    ksft_eq(carrier1 - carrier0, 0)
361
362
363def test_rss_context_dump(cfg):
364    """
365    Test dumping RSS contexts. This tests mostly exercises the kernel APIs.
366    """
367
368    # Get a random key of the right size
369    data = get_rss(cfg)
370    if 'rss-hash-key' in data:
371        key_data = _rss_key_rand(len(data['rss-hash-key']))
372        key = _rss_key_str(key_data)
373    else:
374        key_data = []
375        key = "ba:ad"
376
377    ids = []
378    try:
379        ids.append(ethtool_create(cfg, "-X", f"context new"))
380        defer(ethtool, f"-X {cfg.ifname} context {ids[-1]} delete")
381
382        ids.append(ethtool_create(cfg, "-X", f"context new weight 1 1"))
383        defer(ethtool, f"-X {cfg.ifname} context {ids[-1]} delete")
384
385        ids.append(ethtool_create(cfg, "-X", f"context new hkey {key}"))
386        defer(ethtool, f"-X {cfg.ifname} context {ids[-1]} delete")
387    except CmdExitFailure:
388        if not ids:
389            raise KsftSkipEx("Unable to add any contexts")
390        ksft_pr(f"Added only {len(ids)} out of 3 contexts")
391
392    expect_tuples = set([(cfg.ifname, -1)] + [(cfg.ifname, ctx_id) for ctx_id in ids])
393
394    # Dump all
395    ctxs = cfg.ethnl.rss_get({}, dump=True)
396    tuples = [(c['header']['dev-name'], c.get('context', -1)) for c in ctxs]
397    ksft_eq(len(tuples), len(set(tuples)), "duplicates in context dump")
398    ctx_tuples = set([ctx for ctx in tuples if ctx[0] == cfg.ifname])
399    ksft_eq(expect_tuples, ctx_tuples)
400
401    # Sanity-check the results
402    for data in ctxs:
403        ksft_ne(set(data.get('indir', [1])), {0}, "indir table is all zero")
404        ksft_ne(set(data.get('hkey', [1])), {0}, "key is all zero")
405
406        # More specific checks
407        if len(ids) > 1 and data.get('context') == ids[1]:
408            ksft_eq(set(data['indir']), {0, 1},
409                    "ctx1 - indir table mismatch")
410        if len(ids) > 2 and data.get('context') == ids[2]:
411            ksft_eq(data['hkey'], bytes(key_data), "ctx2 - key mismatch")
412
413    # Ifindex filter
414    ctxs = cfg.ethnl.rss_get({'header': {'dev-name': cfg.ifname}}, dump=True)
415    tuples = [(c['header']['dev-name'], c.get('context', -1)) for c in ctxs]
416    ctx_tuples = set(tuples)
417    ksft_eq(len(tuples), len(ctx_tuples), "duplicates in context dump")
418    ksft_eq(expect_tuples, ctx_tuples)
419
420    # Skip ctx 0
421    expect_tuples.remove((cfg.ifname, -1))
422
423    ctxs = cfg.ethnl.rss_get({'start-context': 1}, dump=True)
424    tuples = [(c['header']['dev-name'], c.get('context', -1)) for c in ctxs]
425    ksft_eq(len(tuples), len(set(tuples)), "duplicates in context dump")
426    ctx_tuples = set([ctx for ctx in tuples if ctx[0] == cfg.ifname])
427    ksft_eq(expect_tuples, ctx_tuples)
428
429    # And finally both with ifindex and skip main
430    ctxs = cfg.ethnl.rss_get({'header': {'dev-name': cfg.ifname}, 'start-context': 1}, dump=True)
431    ctx_tuples = set([(c['header']['dev-name'], c.get('context', -1)) for c in ctxs])
432    ksft_eq(expect_tuples, ctx_tuples)
433
434
435def test_rss_context(cfg, ctx_cnt=1, create_with_cfg=None):
436    """
437    Test separating traffic into RSS contexts.
438    The queues will be allocated 2 for each context:
439     ctx0  ctx1  ctx2  ctx3
440    [0 1] [2 3] [4 5] [6 7] ...
441    """
442
443    require_ntuple(cfg)
444
445    requested_ctx_cnt = ctx_cnt
446
447    # Try to allocate more queues when necessary
448    qcnt = len(_get_rx_cnts(cfg))
449    if qcnt < 2 + 2 * ctx_cnt:
450        try:
451            ksft_pr(f"Increasing queue count {qcnt} -> {2 + 2 * ctx_cnt}")
452            ethtool(f"-L {cfg.ifname} combined {2 + 2 * ctx_cnt}")
453            defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
454        except:
455            raise KsftSkipEx("Not enough queues for the test")
456
457    ports = []
458
459    # Use queues 0 and 1 for normal traffic
460    ethtool(f"-X {cfg.ifname} equal 2")
461    defer(ethtool, f"-X {cfg.ifname} default")
462
463    for i in range(ctx_cnt):
464        want_cfg = f"start {2 + i * 2} equal 2"
465        create_cfg = want_cfg if create_with_cfg else ""
466
467        try:
468            ctx_id = ethtool_create(cfg, "-X", f"context new {create_cfg}")
469            defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
470        except CmdExitFailure:
471            # try to carry on and skip at the end
472            if i == 0:
473                raise
474            ksft_pr(f"Failed to create context {i + 1}, trying to test what we got")
475            ctx_cnt = i
476            if cfg.context_cnt is None:
477                cfg.context_cnt = ctx_cnt
478            break
479
480        _rss_key_check(cfg, context=ctx_id)
481
482        if not create_with_cfg:
483            ethtool(f"-X {cfg.ifname} context {ctx_id} {want_cfg}")
484            _rss_key_check(cfg, context=ctx_id)
485
486        # Sanity check the context we just created
487        data = get_rss(cfg, ctx_id)
488        ksft_eq(min(data['rss-indirection-table']), 2 + i * 2, "Unexpected context cfg: " + str(data))
489        ksft_eq(max(data['rss-indirection-table']), 2 + i * 2 + 1, "Unexpected context cfg: " + str(data))
490
491        ports.append(rand_port())
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 = []
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        ports.append(rand_port())
580        flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {ports[i]} context {ctx_id}"
581        ntuple_id = ethtool_create(cfg, "-N", flow)
582        ntuple.append(defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}"))
583
584    check_traffic()
585
586    # Remove middle context
587    remove_ctx(ctx_cnt // 2)
588    check_traffic()
589
590    # Remove first context
591    remove_ctx(0)
592    check_traffic()
593
594    # Remove last context
595    remove_ctx(-1)
596    check_traffic()
597
598
599def test_rss_context_overlap(cfg, other_ctx=0):
600    """
601    Test contexts overlapping with each other.
602    Use 4 queues for the main context, but only queues 2 and 3 for context 1.
603    """
604
605    require_ntuple(cfg)
606    if other_ctx:
607        require_context_cnt(cfg, 2)
608
609    queue_cnt = len(_get_rx_cnts(cfg))
610    if queue_cnt < 4:
611        try:
612            ksft_pr(f"Increasing queue count {queue_cnt} -> 4")
613            ethtool(f"-L {cfg.ifname} combined 4")
614            defer(ethtool, f"-L {cfg.ifname} combined {queue_cnt}")
615        except:
616            raise KsftSkipEx("Not enough queues for the test")
617
618    if other_ctx == 0:
619        ethtool(f"-X {cfg.ifname} equal 4")
620        defer(ethtool, f"-X {cfg.ifname} default")
621    else:
622        other_ctx = ethtool_create(cfg, "-X", "context new")
623        ethtool(f"-X {cfg.ifname} context {other_ctx} equal 4")
624        defer(ethtool, f"-X {cfg.ifname} context {other_ctx} delete")
625
626    ctx_id = ethtool_create(cfg, "-X", "context new")
627    ethtool(f"-X {cfg.ifname} context {ctx_id} start 2 equal 2")
628    defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
629
630    port = rand_port()
631    if other_ctx:
632        flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {other_ctx}"
633        ntuple_id = ethtool_create(cfg, "-N", flow)
634        ntuple = defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
635
636    # Test the main context
637    cnts = _get_rx_cnts(cfg)
638    GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000)
639    cnts = _get_rx_cnts(cfg, prev=cnts)
640
641    ksft_ge(sum(cnts[ :4]), 20000, "traffic on main context: " + str(cnts))
642    ksft_ge(sum(cnts[ :2]),  7000, "traffic on main context (1/2): " + str(cnts))
643    ksft_ge(sum(cnts[2:4]),  7000, "traffic on main context (2/2): " + str(cnts))
644    if other_ctx == 0:
645        ksft_eq(sum(cnts[4: ]),     0, "traffic on other queues: " + str(cnts))
646
647    # Now create a rule for context 1 and make sure traffic goes to a subset
648    if other_ctx:
649        ntuple.exec()
650    flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id}"
651    ntuple_id = ethtool_create(cfg, "-N", flow)
652    defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
653
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    directed = sum(cnts[2:4])
659    ksft_lt(sum(cnts[ :2]), directed / 2, "traffic on main context: " + str(cnts))
660    ksft_ge(directed, 20000, "traffic on extra context: " + str(cnts))
661    if other_ctx == 0:
662        ksft_eq(sum(cnts[4: ]),     0, "traffic on other queues: " + str(cnts))
663
664
665def test_rss_context_overlap2(cfg):
666    test_rss_context_overlap(cfg, True)
667
668
669def test_flow_add_context_missing(cfg):
670    """
671    Test that we are not allowed to add a rule pointing to an RSS context
672    which was never created.
673    """
674
675    require_ntuple(cfg)
676
677    # Find a context which doesn't exist
678    for ctx_id in range(1, 100):
679        try:
680            get_rss(cfg, context=ctx_id)
681        except CmdExitFailure:
682            break
683
684    with ksft_raises(CmdExitFailure) as cm:
685        flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port 1234 context {ctx_id}"
686        ntuple_id = ethtool_create(cfg, "-N", flow)
687        ethtool(f"-N {cfg.ifname} delete {ntuple_id}")
688    if cm.exception:
689        ksft_in('Invalid argument', cm.exception.cmd.stderr)
690
691
692def test_delete_rss_context_busy(cfg):
693    """
694    Test that deletion returns -EBUSY when an rss context is being used
695    by an ntuple filter.
696    """
697
698    require_ntuple(cfg)
699
700    # create additional rss context
701    ctx_id = ethtool_create(cfg, "-X", "context new")
702    ctx_deleter = defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
703
704    # utilize context from ntuple filter
705    port = rand_port()
706    flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id}"
707    ntuple_id = ethtool_create(cfg, "-N", flow)
708    defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
709
710    # attempt to delete in-use context
711    try:
712        ctx_deleter.exec_only()
713        ctx_deleter.cancel()
714        raise KsftFailEx(f"deleted context {ctx_id} used by rule {ntuple_id}")
715    except CmdExitFailure:
716        pass
717
718
719def test_rss_ntuple_addition(cfg):
720    """
721    Test that the queue offset (ring_cookie) of an ntuple rule is added
722    to the queue number read from the indirection table.
723    """
724
725    require_ntuple(cfg)
726
727    queue_cnt = len(_get_rx_cnts(cfg))
728    if queue_cnt < 4:
729        try:
730            ksft_pr(f"Increasing queue count {queue_cnt} -> 4")
731            ethtool(f"-L {cfg.ifname} combined 4")
732            defer(ethtool, f"-L {cfg.ifname} combined {queue_cnt}")
733        except:
734            raise KsftSkipEx("Not enough queues for the test")
735
736    # Use queue 0 for normal traffic
737    ethtool(f"-X {cfg.ifname} equal 1")
738    defer(ethtool, f"-X {cfg.ifname} default")
739
740    # create additional rss context
741    ctx_id = ethtool_create(cfg, "-X", "context new equal 2")
742    defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
743
744    # utilize context from ntuple filter
745    port = rand_port()
746    flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id} action 2"
747    try:
748        ntuple_id = ethtool_create(cfg, "-N", flow)
749    except CmdExitFailure:
750        raise KsftSkipEx("Ntuple filter with RSS and nonzero action not supported")
751    defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
752
753    _send_traffic_check(cfg, port, f"context {ctx_id}", { 'target': (2, 3),
754                                                          'empty' : (1,),
755                                                          'noise' : (0,) })
756
757
758def test_rss_default_context_rule(cfg):
759    """
760    Allocate a port, direct this port to context 0, then create a new RSS
761    context and steer all TCP traffic to it (context 1).  Verify that:
762      * Traffic to the specific port continues to use queues of the main
763        context (0/1).
764      * Traffic to any other TCP port is redirected to the new context
765        (queues 2/3).
766    """
767
768    require_ntuple(cfg)
769
770    queue_cnt = len(_get_rx_cnts(cfg))
771    if queue_cnt < 4:
772        try:
773            ksft_pr(f"Increasing queue count {queue_cnt} -> 4")
774            ethtool(f"-L {cfg.ifname} combined 4")
775            defer(ethtool, f"-L {cfg.ifname} combined {queue_cnt}")
776        except Exception as exc:
777            raise KsftSkipEx("Not enough queues for the test") from exc
778
779    # Use queues 0 and 1 for the main context
780    ethtool(f"-X {cfg.ifname} equal 2")
781    defer(ethtool, f"-X {cfg.ifname} default")
782
783    # Create a new RSS context that uses queues 2 and 3
784    ctx_id = ethtool_create(cfg, "-X", "context new start 2 equal 2")
785    defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
786
787    # Generic low-priority rule: redirect all TCP traffic to the new context.
788    # Give it an explicit higher location number (lower priority).
789    flow_generic = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} context {ctx_id} loc 1"
790    ethtool(f"-N {cfg.ifname} {flow_generic}")
791    defer(ethtool, f"-N {cfg.ifname} delete 1")
792
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 = rand_port()
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 = rand_port()
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