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