xref: /linux/tools/testing/selftests/drivers/net/hw/rss_ctx.py (revision 1a9239bb4253f9076b5b4b2a1a4e8d7defd77a95)
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        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    # First two queues get less traffic than all the rest
182    ksft_lt(sum(cnts[:2]), sum(cnts[2:]), "traffic distributed: " + str(cnts))
183
184
185def test_rss_queue_reconfigure(cfg, main_ctx=True):
186    """Make sure queue changes can't override requested RSS config.
187
188    By default main RSS table should change to include all queues.
189    When user sets a specific RSS config the driver should preserve it,
190    even when queue count changes. Driver should refuse to deactivate
191    queues used in the user-set RSS config.
192    """
193
194    if not main_ctx:
195        require_ntuple(cfg)
196
197    # Start with 4 queues, an arbitrary known number.
198    try:
199        qcnt = len(_get_rx_cnts(cfg))
200        ethtool(f"-L {cfg.ifname} combined 4")
201        defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
202    except:
203        raise KsftSkipEx("Not enough queues for the test or qstat not supported")
204
205    if main_ctx:
206        ctx_id = 0
207        ctx_ref = ""
208    else:
209        ctx_id = ethtool_create(cfg, "-X", "context new")
210        ctx_ref = f"context {ctx_id}"
211        defer(ethtool, f"-X {cfg.ifname} {ctx_ref} delete")
212
213    # Indirection table should be distributing to all queues.
214    data = get_rss(cfg, context=ctx_id)
215    ksft_eq(0, min(data['rss-indirection-table']))
216    ksft_eq(3, max(data['rss-indirection-table']))
217
218    # Increase queues, indirection table should be distributing to all queues.
219    # It's unclear whether tables of additional contexts should be reset, too.
220    if main_ctx:
221        ethtool(f"-L {cfg.ifname} combined 5")
222        data = get_rss(cfg)
223        ksft_eq(0, min(data['rss-indirection-table']))
224        ksft_eq(4, max(data['rss-indirection-table']))
225        ethtool(f"-L {cfg.ifname} combined 4")
226
227    # Configure the table explicitly
228    port = rand_port()
229    ethtool(f"-X {cfg.ifname} {ctx_ref} weight 1 0 0 1")
230    if main_ctx:
231        other_key = 'empty'
232        defer(ethtool, f"-X {cfg.ifname} default")
233    else:
234        other_key = 'noise'
235        flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id}"
236        ntuple = ethtool_create(cfg, "-N", flow)
237        defer(ethtool, f"-N {cfg.ifname} delete {ntuple}")
238
239    _send_traffic_check(cfg, port, ctx_ref, { 'target': (0, 3),
240                                              other_key: (1, 2) })
241
242    # We should be able to increase queues, but table should be left untouched
243    ethtool(f"-L {cfg.ifname} combined 5")
244    data = get_rss(cfg, context=ctx_id)
245    ksft_eq({0, 3}, set(data['rss-indirection-table']))
246
247    _send_traffic_check(cfg, port, ctx_ref, { 'target': (0, 3),
248                                              other_key: (1, 2, 4) })
249
250    # Setting queue count to 3 should fail, queue 3 is used
251    try:
252        ethtool(f"-L {cfg.ifname} combined 3")
253    except CmdExitFailure:
254        pass
255    else:
256        raise Exception(f"Driver didn't prevent us from deactivating a used queue (context {ctx_id})")
257
258    if not main_ctx:
259        ethtool(f"-L {cfg.ifname} combined 4")
260        flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id} action 1"
261        try:
262            # this targets queue 4, which doesn't exist
263            ntuple2 = ethtool_create(cfg, "-N", flow)
264            defer(ethtool, f"-N {cfg.ifname} delete {ntuple2}")
265        except CmdExitFailure:
266            pass
267        else:
268            raise Exception(f"Driver didn't prevent us from targeting a nonexistent queue (context {ctx_id})")
269        # change the table to target queues 0 and 2
270        ethtool(f"-X {cfg.ifname} {ctx_ref} weight 1 0 1 0")
271        # ntuple rule therefore targets queues 1 and 3
272        try:
273            ntuple2 = ethtool_create(cfg, "-N", flow)
274        except CmdExitFailure:
275            ksft_pr("Driver does not support rss + queue offset")
276            return
277
278        defer(ethtool, f"-N {cfg.ifname} delete {ntuple2}")
279        # should replace existing filter
280        ksft_eq(ntuple, ntuple2)
281        _send_traffic_check(cfg, port, ctx_ref, { 'target': (1, 3),
282                                                  'noise' : (0, 2) })
283        # Setting queue count to 3 should fail, queue 3 is used
284        try:
285            ethtool(f"-L {cfg.ifname} combined 3")
286        except CmdExitFailure:
287            pass
288        else:
289            raise Exception(f"Driver didn't prevent us from deactivating a used queue (context {ctx_id})")
290
291
292def test_rss_resize(cfg):
293    """Test resizing of the RSS table.
294
295    Some devices dynamically increase and decrease the size of the RSS
296    indirection table based on the number of enabled queues.
297    When that happens driver must maintain the balance of entries
298    (preferably duplicating the smaller table).
299    """
300
301    channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
302    ch_max = channels['combined-max']
303    qcnt = channels['combined-count']
304
305    if ch_max < 2:
306        raise KsftSkipEx(f"Not enough queues for the test: {ch_max}")
307
308    ethtool(f"-L {cfg.ifname} combined 2")
309    defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
310
311    ethtool(f"-X {cfg.ifname} weight 1 7")
312    defer(ethtool, f"-X {cfg.ifname} default")
313
314    ethtool(f"-L {cfg.ifname} combined {ch_max}")
315    data = get_rss(cfg)
316    ksft_eq(0, min(data['rss-indirection-table']))
317    ksft_eq(1, max(data['rss-indirection-table']))
318
319    ksft_eq(7,
320            data['rss-indirection-table'].count(1) /
321            data['rss-indirection-table'].count(0),
322            f"Table imbalance after resize: {data['rss-indirection-table']}")
323
324
325def test_hitless_key_update(cfg):
326    """Test that flows may be rehashed without impacting traffic.
327
328    Some workloads may want to rehash the flows in response to an imbalance.
329    Most effective way to do that is changing the RSS key. Check that changing
330    the key does not cause link flaps or traffic disruption.
331
332    Disrupting traffic for key update is not a bug, but makes the key
333    update unusable for rehashing under load.
334    """
335    data = get_rss(cfg)
336    key_len = len(data['rss-hash-key'])
337
338    key = _rss_key_rand(key_len)
339
340    tgen = GenerateTraffic(cfg)
341    try:
342        errors0, carrier0 = get_drop_err_sum(cfg)
343        t0 = datetime.datetime.now()
344        ethtool(f"-X {cfg.ifname} hkey " + _rss_key_str(key))
345        t1 = datetime.datetime.now()
346        errors1, carrier1 = get_drop_err_sum(cfg)
347    finally:
348        tgen.wait_pkts_and_stop(5000)
349
350    ksft_lt((t1 - t0).total_seconds(), 0.2)
351    ksft_eq(errors1 - errors1, 0)
352    ksft_eq(carrier1 - carrier0, 0)
353
354
355def test_rss_context_dump(cfg):
356    """
357    Test dumping RSS contexts. This tests mostly exercises the kernel APIs.
358    """
359
360    # Get a random key of the right size
361    data = get_rss(cfg)
362    if 'rss-hash-key' in data:
363        key_data = _rss_key_rand(len(data['rss-hash-key']))
364        key = _rss_key_str(key_data)
365    else:
366        key_data = []
367        key = "ba:ad"
368
369    ids = []
370    try:
371        ids.append(ethtool_create(cfg, "-X", f"context new"))
372        defer(ethtool, f"-X {cfg.ifname} context {ids[-1]} delete")
373
374        ids.append(ethtool_create(cfg, "-X", f"context new weight 1 1"))
375        defer(ethtool, f"-X {cfg.ifname} context {ids[-1]} delete")
376
377        ids.append(ethtool_create(cfg, "-X", f"context new hkey {key}"))
378        defer(ethtool, f"-X {cfg.ifname} context {ids[-1]} delete")
379    except CmdExitFailure:
380        if not ids:
381            raise KsftSkipEx("Unable to add any contexts")
382        ksft_pr(f"Added only {len(ids)} out of 3 contexts")
383
384    expect_tuples = set([(cfg.ifname, -1)] + [(cfg.ifname, ctx_id) for ctx_id in ids])
385
386    # Dump all
387    ctxs = cfg.ethnl.rss_get({}, dump=True)
388    tuples = [(c['header']['dev-name'], c.get('context', -1)) for c in ctxs]
389    ksft_eq(len(tuples), len(set(tuples)), "duplicates in context dump")
390    ctx_tuples = set([ctx for ctx in tuples if ctx[0] == cfg.ifname])
391    ksft_eq(expect_tuples, ctx_tuples)
392
393    # Sanity-check the results
394    for data in ctxs:
395        ksft_ne(set(data.get('indir', [1])), {0}, "indir table is all zero")
396        ksft_ne(set(data.get('hkey', [1])), {0}, "key is all zero")
397
398        # More specific checks
399        if len(ids) > 1 and data.get('context') == ids[1]:
400            ksft_eq(set(data['indir']), {0, 1},
401                    "ctx1 - indir table mismatch")
402        if len(ids) > 2 and data.get('context') == ids[2]:
403            ksft_eq(data['hkey'], bytes(key_data), "ctx2 - key mismatch")
404
405    # Ifindex filter
406    ctxs = cfg.ethnl.rss_get({'header': {'dev-name': cfg.ifname}}, dump=True)
407    tuples = [(c['header']['dev-name'], c.get('context', -1)) for c in ctxs]
408    ctx_tuples = set(tuples)
409    ksft_eq(len(tuples), len(ctx_tuples), "duplicates in context dump")
410    ksft_eq(expect_tuples, ctx_tuples)
411
412    # Skip ctx 0
413    expect_tuples.remove((cfg.ifname, -1))
414
415    ctxs = cfg.ethnl.rss_get({'start-context': 1}, dump=True)
416    tuples = [(c['header']['dev-name'], c.get('context', -1)) for c in ctxs]
417    ksft_eq(len(tuples), len(set(tuples)), "duplicates in context dump")
418    ctx_tuples = set([ctx for ctx in tuples if ctx[0] == cfg.ifname])
419    ksft_eq(expect_tuples, ctx_tuples)
420
421    # And finally both with ifindex and skip main
422    ctxs = cfg.ethnl.rss_get({'header': {'dev-name': cfg.ifname}, 'start-context': 1}, dump=True)
423    ctx_tuples = set([(c['header']['dev-name'], c.get('context', -1)) for c in ctxs])
424    ksft_eq(expect_tuples, ctx_tuples)
425
426
427def test_rss_context(cfg, ctx_cnt=1, create_with_cfg=None):
428    """
429    Test separating traffic into RSS contexts.
430    The queues will be allocated 2 for each context:
431     ctx0  ctx1  ctx2  ctx3
432    [0 1] [2 3] [4 5] [6 7] ...
433    """
434
435    require_ntuple(cfg)
436
437    requested_ctx_cnt = ctx_cnt
438
439    # Try to allocate more queues when necessary
440    qcnt = len(_get_rx_cnts(cfg))
441    if qcnt < 2 + 2 * ctx_cnt:
442        try:
443            ksft_pr(f"Increasing queue count {qcnt} -> {2 + 2 * ctx_cnt}")
444            ethtool(f"-L {cfg.ifname} combined {2 + 2 * ctx_cnt}")
445            defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
446        except:
447            raise KsftSkipEx("Not enough queues for the test")
448
449    ports = []
450
451    # Use queues 0 and 1 for normal traffic
452    ethtool(f"-X {cfg.ifname} equal 2")
453    defer(ethtool, f"-X {cfg.ifname} default")
454
455    for i in range(ctx_cnt):
456        want_cfg = f"start {2 + i * 2} equal 2"
457        create_cfg = want_cfg if create_with_cfg else ""
458
459        try:
460            ctx_id = ethtool_create(cfg, "-X", f"context new {create_cfg}")
461            defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
462        except CmdExitFailure:
463            # try to carry on and skip at the end
464            if i == 0:
465                raise
466            ksft_pr(f"Failed to create context {i + 1}, trying to test what we got")
467            ctx_cnt = i
468            if cfg.context_cnt is None:
469                cfg.context_cnt = ctx_cnt
470            break
471
472        _rss_key_check(cfg, context=ctx_id)
473
474        if not create_with_cfg:
475            ethtool(f"-X {cfg.ifname} context {ctx_id} {want_cfg}")
476            _rss_key_check(cfg, context=ctx_id)
477
478        # Sanity check the context we just created
479        data = get_rss(cfg, ctx_id)
480        ksft_eq(min(data['rss-indirection-table']), 2 + i * 2, "Unexpected context cfg: " + str(data))
481        ksft_eq(max(data['rss-indirection-table']), 2 + i * 2 + 1, "Unexpected context cfg: " + str(data))
482
483        ports.append(rand_port())
484        flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {ports[i]} context {ctx_id}"
485        ntuple = ethtool_create(cfg, "-N", flow)
486        defer(ethtool, f"-N {cfg.ifname} delete {ntuple}")
487
488        _ntuple_rule_check(cfg, ntuple, ctx_id)
489
490    for i in range(ctx_cnt):
491        _send_traffic_check(cfg, ports[i], f"context {i}",
492                            { 'target': (2+i*2, 3+i*2),
493                              'noise': (0, 1),
494                              'empty': list(range(2, 2+i*2)) + list(range(4+i*2, 2+2*ctx_cnt)) })
495
496    if requested_ctx_cnt != ctx_cnt:
497        raise KsftSkipEx(f"Tested only {ctx_cnt} contexts, wanted {requested_ctx_cnt}")
498
499
500def test_rss_context4(cfg):
501    test_rss_context(cfg, 4)
502
503
504def test_rss_context32(cfg):
505    test_rss_context(cfg, 32)
506
507
508def test_rss_context4_create_with_cfg(cfg):
509    test_rss_context(cfg, 4, create_with_cfg=True)
510
511
512def test_rss_context_queue_reconfigure(cfg):
513    test_rss_queue_reconfigure(cfg, main_ctx=False)
514
515
516def test_rss_context_out_of_order(cfg, ctx_cnt=4):
517    """
518    Test separating traffic into RSS contexts.
519    Contexts are removed in semi-random order, and steering re-tested
520    to make sure removal doesn't break steering to surviving contexts.
521    Test requires 3 contexts to work.
522    """
523
524    require_ntuple(cfg)
525    require_context_cnt(cfg, 4)
526
527    # Try to allocate more queues when necessary
528    qcnt = len(_get_rx_cnts(cfg))
529    if qcnt < 2 + 2 * ctx_cnt:
530        try:
531            ksft_pr(f"Increasing queue count {qcnt} -> {2 + 2 * ctx_cnt}")
532            ethtool(f"-L {cfg.ifname} combined {2 + 2 * ctx_cnt}")
533            defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
534        except:
535            raise KsftSkipEx("Not enough queues for the test")
536
537    ntuple = []
538    ctx = []
539    ports = []
540
541    def remove_ctx(idx):
542        ntuple[idx].exec()
543        ntuple[idx] = None
544        ctx[idx].exec()
545        ctx[idx] = None
546
547    def check_traffic():
548        for i in range(ctx_cnt):
549            if ctx[i]:
550                expected = {
551                    'target': (2+i*2, 3+i*2),
552                    'noise': (0, 1),
553                    'empty': list(range(2, 2+i*2)) + list(range(4+i*2, 2+2*ctx_cnt))
554                }
555            else:
556                expected = {
557                    'target': (0, 1),
558                    'empty':  range(2, 2+2*ctx_cnt)
559                }
560
561            _send_traffic_check(cfg, ports[i], f"context {i}", expected)
562
563    # Use queues 0 and 1 for normal traffic
564    ethtool(f"-X {cfg.ifname} equal 2")
565    defer(ethtool, f"-X {cfg.ifname} default")
566
567    for i in range(ctx_cnt):
568        ctx_id = ethtool_create(cfg, "-X", f"context new start {2 + i * 2} equal 2")
569        ctx.append(defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete"))
570
571        ports.append(rand_port())
572        flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {ports[i]} context {ctx_id}"
573        ntuple_id = ethtool_create(cfg, "-N", flow)
574        ntuple.append(defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}"))
575
576    check_traffic()
577
578    # Remove middle context
579    remove_ctx(ctx_cnt // 2)
580    check_traffic()
581
582    # Remove first context
583    remove_ctx(0)
584    check_traffic()
585
586    # Remove last context
587    remove_ctx(-1)
588    check_traffic()
589
590
591def test_rss_context_overlap(cfg, other_ctx=0):
592    """
593    Test contexts overlapping with each other.
594    Use 4 queues for the main context, but only queues 2 and 3 for context 1.
595    """
596
597    require_ntuple(cfg)
598    if other_ctx:
599        require_context_cnt(cfg, 2)
600
601    queue_cnt = len(_get_rx_cnts(cfg))
602    if queue_cnt < 4:
603        try:
604            ksft_pr(f"Increasing queue count {queue_cnt} -> 4")
605            ethtool(f"-L {cfg.ifname} combined 4")
606            defer(ethtool, f"-L {cfg.ifname} combined {queue_cnt}")
607        except:
608            raise KsftSkipEx("Not enough queues for the test")
609
610    if other_ctx == 0:
611        ethtool(f"-X {cfg.ifname} equal 4")
612        defer(ethtool, f"-X {cfg.ifname} default")
613    else:
614        other_ctx = ethtool_create(cfg, "-X", "context new")
615        ethtool(f"-X {cfg.ifname} context {other_ctx} equal 4")
616        defer(ethtool, f"-X {cfg.ifname} context {other_ctx} delete")
617
618    ctx_id = ethtool_create(cfg, "-X", "context new")
619    ethtool(f"-X {cfg.ifname} context {ctx_id} start 2 equal 2")
620    defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
621
622    port = rand_port()
623    if other_ctx:
624        flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {other_ctx}"
625        ntuple_id = ethtool_create(cfg, "-N", flow)
626        ntuple = defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
627
628    # Test the main context
629    cnts = _get_rx_cnts(cfg)
630    GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000)
631    cnts = _get_rx_cnts(cfg, prev=cnts)
632
633    ksft_ge(sum(cnts[ :4]), 20000, "traffic on main context: " + str(cnts))
634    ksft_ge(sum(cnts[ :2]),  7000, "traffic on main context (1/2): " + str(cnts))
635    ksft_ge(sum(cnts[2:4]),  7000, "traffic on main context (2/2): " + str(cnts))
636    if other_ctx == 0:
637        ksft_eq(sum(cnts[4: ]),     0, "traffic on other queues: " + str(cnts))
638
639    # Now create a rule for context 1 and make sure traffic goes to a subset
640    if other_ctx:
641        ntuple.exec()
642    flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id}"
643    ntuple_id = ethtool_create(cfg, "-N", flow)
644    defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
645
646    cnts = _get_rx_cnts(cfg)
647    GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000)
648    cnts = _get_rx_cnts(cfg, prev=cnts)
649
650    directed = sum(cnts[2:4])
651    ksft_lt(sum(cnts[ :2]), directed / 2, "traffic on main context: " + str(cnts))
652    ksft_ge(directed, 20000, "traffic on extra context: " + str(cnts))
653    if other_ctx == 0:
654        ksft_eq(sum(cnts[4: ]),     0, "traffic on other queues: " + str(cnts))
655
656
657def test_rss_context_overlap2(cfg):
658    test_rss_context_overlap(cfg, True)
659
660
661def test_flow_add_context_missing(cfg):
662    """
663    Test that we are not allowed to add a rule pointing to an RSS context
664    which was never created.
665    """
666
667    require_ntuple(cfg)
668
669    # Find a context which doesn't exist
670    for ctx_id in range(1, 100):
671        try:
672            get_rss(cfg, context=ctx_id)
673        except CmdExitFailure:
674            break
675
676    with ksft_raises(CmdExitFailure) as cm:
677        flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port 1234 context {ctx_id}"
678        ntuple_id = ethtool_create(cfg, "-N", flow)
679        ethtool(f"-N {cfg.ifname} delete {ntuple_id}")
680    if cm.exception:
681        ksft_in('Invalid argument', cm.exception.cmd.stderr)
682
683
684def test_delete_rss_context_busy(cfg):
685    """
686    Test that deletion returns -EBUSY when an rss context is being used
687    by an ntuple filter.
688    """
689
690    require_ntuple(cfg)
691
692    # create additional rss context
693    ctx_id = ethtool_create(cfg, "-X", "context new")
694    ctx_deleter = defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
695
696    # utilize context from ntuple filter
697    port = rand_port()
698    flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id}"
699    ntuple_id = ethtool_create(cfg, "-N", flow)
700    defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
701
702    # attempt to delete in-use context
703    try:
704        ctx_deleter.exec_only()
705        ctx_deleter.cancel()
706        raise KsftFailEx(f"deleted context {ctx_id} used by rule {ntuple_id}")
707    except CmdExitFailure:
708        pass
709
710
711def test_rss_ntuple_addition(cfg):
712    """
713    Test that the queue offset (ring_cookie) of an ntuple rule is added
714    to the queue number read from the indirection table.
715    """
716
717    require_ntuple(cfg)
718
719    queue_cnt = len(_get_rx_cnts(cfg))
720    if queue_cnt < 4:
721        try:
722            ksft_pr(f"Increasing queue count {queue_cnt} -> 4")
723            ethtool(f"-L {cfg.ifname} combined 4")
724            defer(ethtool, f"-L {cfg.ifname} combined {queue_cnt}")
725        except:
726            raise KsftSkipEx("Not enough queues for the test")
727
728    # Use queue 0 for normal traffic
729    ethtool(f"-X {cfg.ifname} equal 1")
730    defer(ethtool, f"-X {cfg.ifname} default")
731
732    # create additional rss context
733    ctx_id = ethtool_create(cfg, "-X", "context new equal 2")
734    defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
735
736    # utilize context from ntuple filter
737    port = rand_port()
738    flow = f"flow-type tcp{cfg.addr_ipver} dst-ip {cfg.addr} dst-port {port} context {ctx_id} action 2"
739    try:
740        ntuple_id = ethtool_create(cfg, "-N", flow)
741    except CmdExitFailure:
742        raise KsftSkipEx("Ntuple filter with RSS and nonzero action not supported")
743    defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
744
745    _send_traffic_check(cfg, port, f"context {ctx_id}", { 'target': (2, 3),
746                                                          'empty' : (1,),
747                                                          'noise' : (0,) })
748
749
750def main() -> None:
751    with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
752        cfg.context_cnt = None
753        cfg.ethnl = EthtoolFamily()
754        cfg.netdevnl = NetdevFamily()
755
756        ksft_run([test_rss_key_indir, test_rss_queue_reconfigure,
757                  test_rss_resize, test_hitless_key_update,
758                  test_rss_context, test_rss_context4, test_rss_context32,
759                  test_rss_context_dump, test_rss_context_queue_reconfigure,
760                  test_rss_context_overlap, test_rss_context_overlap2,
761                  test_rss_context_out_of_order, test_rss_context4_create_with_cfg,
762                  test_flow_add_context_missing,
763                  test_delete_rss_context_busy, test_rss_ntuple_addition],
764                 args=(cfg, ))
765    ksft_exit()
766
767
768if __name__ == "__main__":
769    main()
770