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