xref: /linux/tools/testing/selftests/drivers/net/hw/rss_ctx.py (revision 4dec64c52e24c2c9a15f81c115f1be5ea35121cb)
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0
3
4import datetime
5import random
6from lib.py import ksft_run, ksft_pr, ksft_exit, ksft_eq, ksft_ge, ksft_lt
7from lib.py import NetDrvEpEnv
8from lib.py import NetdevFamily
9from lib.py import KsftSkipEx
10from lib.py import rand_port
11from lib.py import ethtool, ip, defer, GenerateTraffic, CmdExitFailure
12
13
14def _rss_key_str(key):
15    return ":".join(["{:02x}".format(x) for x in key])
16
17
18def _rss_key_rand(length):
19    return [random.randint(0, 255) for _ in range(length)]
20
21
22def get_rss(cfg, context=0):
23    return ethtool(f"-x {cfg.ifname} context {context}", json=True)[0]
24
25
26def get_drop_err_sum(cfg):
27    stats = ip("-s -s link show dev " + cfg.ifname, json=True)[0]
28    cnt = 0
29    for key in ['errors', 'dropped', 'over_errors', 'fifo_errors',
30                'length_errors', 'crc_errors', 'missed_errors',
31                'frame_errors']:
32        cnt += stats["stats64"]["rx"][key]
33    return cnt, stats["stats64"]["tx"]["carrier_changes"]
34
35
36def ethtool_create(cfg, act, opts):
37    output = ethtool(f"{act} {cfg.ifname} {opts}").stdout
38    # Output will be something like: "New RSS context is 1" or
39    # "Added rule with ID 7", we want the integer from the end
40    return int(output.split()[-1])
41
42
43def require_ntuple(cfg):
44    features = ethtool(f"-k {cfg.ifname}", json=True)[0]
45    if not features["ntuple-filters"]["active"]:
46        # ntuple is more of a capability than a config knob, don't bother
47        # trying to enable it (until some driver actually needs it).
48        raise KsftSkipEx("Ntuple filters not enabled on the device: " + str(features["ntuple-filters"]))
49
50
51# Get Rx packet counts for all queues, as a simple list of integers
52# if @prev is specified the prev counts will be subtracted
53def _get_rx_cnts(cfg, prev=None):
54    cfg.wait_hw_stats_settle()
55    data = cfg.netdevnl.qstats_get({"ifindex": cfg.ifindex, "scope": ["queue"]}, dump=True)
56    data = [x for x in data if x['queue-type'] == "rx"]
57    max_q = max([x["queue-id"] for x in data])
58    queue_stats = [0] * (max_q + 1)
59    for q in data:
60        queue_stats[q["queue-id"]] = q["rx-packets"]
61        if prev and q["queue-id"] < len(prev):
62            queue_stats[q["queue-id"]] -= prev[q["queue-id"]]
63    return queue_stats
64
65
66def test_rss_key_indir(cfg):
67    """
68    Test basics like updating the main RSS key and indirection table.
69    """
70    if len(_get_rx_cnts(cfg)) < 2:
71        KsftSkipEx("Device has only one queue (or doesn't support queue stats)")
72
73    data = get_rss(cfg)
74    want_keys = ['rss-hash-key', 'rss-hash-function', 'rss-indirection-table']
75    for k in want_keys:
76        if k not in data:
77            raise KsftFailEx("ethtool results missing key: " + k)
78        if not data[k]:
79            raise KsftFailEx(f"ethtool results empty for '{k}': {data[k]}")
80
81    key_len = len(data['rss-hash-key'])
82
83    # Set the key
84    key = _rss_key_rand(key_len)
85    ethtool(f"-X {cfg.ifname} hkey " + _rss_key_str(key))
86
87    data = get_rss(cfg)
88    ksft_eq(key, data['rss-hash-key'])
89
90    # Set the indirection table
91    ethtool(f"-X {cfg.ifname} equal 2")
92    data = get_rss(cfg)
93    ksft_eq(0, min(data['rss-indirection-table']))
94    ksft_eq(1, max(data['rss-indirection-table']))
95
96    # Check we only get traffic on the first 2 queues
97    cnts = _get_rx_cnts(cfg)
98    GenerateTraffic(cfg).wait_pkts_and_stop(20000)
99    cnts = _get_rx_cnts(cfg, prev=cnts)
100    # 2 queues, 20k packets, must be at least 5k per queue
101    ksft_ge(cnts[0], 5000, "traffic on main context (1/2): " + str(cnts))
102    ksft_ge(cnts[1], 5000, "traffic on main context (2/2): " + str(cnts))
103    # The other queues should be unused
104    ksft_eq(sum(cnts[2:]), 0, "traffic on unused queues: " + str(cnts))
105
106    # Restore, and check traffic gets spread again
107    ethtool(f"-X {cfg.ifname} default")
108
109    cnts = _get_rx_cnts(cfg)
110    GenerateTraffic(cfg).wait_pkts_and_stop(20000)
111    cnts = _get_rx_cnts(cfg, prev=cnts)
112    # First two queues get less traffic than all the rest
113    ksft_lt(sum(cnts[:2]), sum(cnts[2:]), "traffic distributed: " + str(cnts))
114
115
116def test_rss_context(cfg, ctx_cnt=1, create_with_cfg=None):
117    """
118    Test separating traffic into RSS contexts.
119    The queues will be allocated 2 for each context:
120     ctx0  ctx1  ctx2  ctx3
121    [0 1] [2 3] [4 5] [6 7] ...
122    """
123
124    require_ntuple(cfg)
125
126    requested_ctx_cnt = ctx_cnt
127
128    # Try to allocate more queues when necessary
129    qcnt = len(_get_rx_cnts(cfg))
130    if qcnt < 2 + 2 * ctx_cnt:
131        try:
132            ksft_pr(f"Increasing queue count {qcnt} -> {2 + 2 * ctx_cnt}")
133            ethtool(f"-L {cfg.ifname} combined {2 + 2 * ctx_cnt}")
134            defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
135        except:
136            raise KsftSkipEx("Not enough queues for the test")
137
138    ports = []
139
140    # Use queues 0 and 1 for normal traffic
141    ethtool(f"-X {cfg.ifname} equal 2")
142    defer(ethtool, f"-X {cfg.ifname} default")
143
144    for i in range(ctx_cnt):
145        want_cfg = f"start {2 + i * 2} equal 2"
146        create_cfg = want_cfg if create_with_cfg else ""
147
148        try:
149            ctx_id = ethtool_create(cfg, "-X", f"context new {create_cfg}")
150            defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
151        except CmdExitFailure:
152            # try to carry on and skip at the end
153            if i == 0:
154                raise
155            ksft_pr(f"Failed to create context {i + 1}, trying to test what we got")
156            ctx_cnt = i
157            break
158
159        if not create_with_cfg:
160            ethtool(f"-X {cfg.ifname} context {ctx_id} {want_cfg}")
161
162        # Sanity check the context we just created
163        data = get_rss(cfg, ctx_id)
164        ksft_eq(min(data['rss-indirection-table']), 2 + i * 2, "Unexpected context cfg: " + str(data))
165        ksft_eq(max(data['rss-indirection-table']), 2 + i * 2 + 1, "Unexpected context cfg: " + str(data))
166
167        ports.append(rand_port())
168        flow = f"flow-type tcp{cfg.addr_ipver} dst-port {ports[i]} context {ctx_id}"
169        ntuple = ethtool_create(cfg, "-N", flow)
170        defer(ethtool, f"-N {cfg.ifname} delete {ntuple}")
171
172    for i in range(ctx_cnt):
173        cnts = _get_rx_cnts(cfg)
174        GenerateTraffic(cfg, port=ports[i]).wait_pkts_and_stop(20000)
175        cnts = _get_rx_cnts(cfg, prev=cnts)
176
177        ksft_lt(sum(cnts[ :2]), 10000, "traffic on main context:" + str(cnts))
178        ksft_ge(sum(cnts[2+i*2:4+i*2]), 20000, f"traffic on context {i}: " + str(cnts))
179        ksft_eq(sum(cnts[2:2+i*2] + cnts[4+i*2:]), 0, "traffic on other contexts: " + str(cnts))
180
181    if requested_ctx_cnt != ctx_cnt:
182        raise KsftSkipEx(f"Tested only {ctx_cnt} contexts, wanted {requested_ctx_cnt}")
183
184
185def test_rss_context4(cfg):
186    test_rss_context(cfg, 4)
187
188
189def test_rss_context32(cfg):
190    test_rss_context(cfg, 32)
191
192
193def test_rss_context4_create_with_cfg(cfg):
194    test_rss_context(cfg, 4, create_with_cfg=True)
195
196
197def test_rss_context_out_of_order(cfg, ctx_cnt=4):
198    """
199    Test separating traffic into RSS contexts.
200    Contexts are removed in semi-random order, and steering re-tested
201    to make sure removal doesn't break steering to surviving contexts.
202    Test requires 3 contexts to work.
203    """
204
205    require_ntuple(cfg)
206
207    requested_ctx_cnt = ctx_cnt
208
209    # Try to allocate more queues when necessary
210    qcnt = len(_get_rx_cnts(cfg))
211    if qcnt < 2 + 2 * ctx_cnt:
212        try:
213            ksft_pr(f"Increasing queue count {qcnt} -> {2 + 2 * ctx_cnt}")
214            ethtool(f"-L {cfg.ifname} combined {2 + 2 * ctx_cnt}")
215            defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
216        except:
217            raise KsftSkipEx("Not enough queues for the test")
218
219    ntuple = []
220    ctx = []
221    ports = []
222
223    def remove_ctx(idx):
224        ntuple[idx].exec()
225        ntuple[idx] = None
226        ctx[idx].exec()
227        ctx[idx] = None
228
229    def check_traffic():
230        for i in range(ctx_cnt):
231            cnts = _get_rx_cnts(cfg)
232            GenerateTraffic(cfg, port=ports[i]).wait_pkts_and_stop(20000)
233            cnts = _get_rx_cnts(cfg, prev=cnts)
234
235            if ctx[i]:
236                ksft_lt(sum(cnts[ :2]), 10000, "traffic on main context:" + str(cnts))
237                ksft_ge(sum(cnts[2+i*2:4+i*2]), 20000, f"traffic on context {i}: " + str(cnts))
238                ksft_eq(sum(cnts[2:2+i*2] + cnts[4+i*2:]), 0, "traffic on other contexts: " + str(cnts))
239            else:
240                ksft_ge(sum(cnts[ :2]), 20000, "traffic on main context:" + str(cnts))
241                ksft_eq(sum(cnts[2: ]),     0, "traffic on other contexts: " + str(cnts))
242
243    # Use queues 0 and 1 for normal traffic
244    ethtool(f"-X {cfg.ifname} equal 2")
245    defer(ethtool, f"-X {cfg.ifname} default")
246
247    for i in range(ctx_cnt):
248        ctx_id = ethtool_create(cfg, "-X", f"context new start {2 + i * 2} equal 2")
249        ctx.append(defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete"))
250
251        ports.append(rand_port())
252        flow = f"flow-type tcp{cfg.addr_ipver} dst-port {ports[i]} context {ctx_id}"
253        ntuple_id = ethtool_create(cfg, "-N", flow)
254        ntuple.append(defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}"))
255
256    check_traffic()
257
258    # Remove middle context
259    remove_ctx(ctx_cnt // 2)
260    check_traffic()
261
262    # Remove first context
263    remove_ctx(0)
264    check_traffic()
265
266    # Remove last context
267    remove_ctx(-1)
268    check_traffic()
269
270    if requested_ctx_cnt != ctx_cnt:
271        raise KsftSkipEx(f"Tested only {ctx_cnt} contexts, wanted {requested_ctx_cnt}")
272
273
274def test_rss_context_overlap(cfg, other_ctx=0):
275    """
276    Test contexts overlapping with each other.
277    Use 4 queues for the main context, but only queues 2 and 3 for context 1.
278    """
279
280    require_ntuple(cfg)
281
282    queue_cnt = len(_get_rx_cnts(cfg))
283    if queue_cnt < 4:
284        try:
285            ksft_pr(f"Increasing queue count {queue_cnt} -> 4")
286            ethtool(f"-L {cfg.ifname} combined 4")
287            defer(ethtool, f"-L {cfg.ifname} combined {queue_cnt}")
288        except:
289            raise KsftSkipEx("Not enough queues for the test")
290
291    if other_ctx == 0:
292        ethtool(f"-X {cfg.ifname} equal 4")
293        defer(ethtool, f"-X {cfg.ifname} default")
294    else:
295        other_ctx = ethtool_create(cfg, "-X", "context new")
296        ethtool(f"-X {cfg.ifname} context {other_ctx} equal 4")
297        defer(ethtool, f"-X {cfg.ifname} context {other_ctx} delete")
298
299    ctx_id = ethtool_create(cfg, "-X", "context new")
300    ethtool(f"-X {cfg.ifname} context {ctx_id} start 2 equal 2")
301    defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
302
303    port = rand_port()
304    if other_ctx:
305        flow = f"flow-type tcp{cfg.addr_ipver} dst-port {port} context {other_ctx}"
306        ntuple_id = ethtool_create(cfg, "-N", flow)
307        ntuple = defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
308
309    # Test the main context
310    cnts = _get_rx_cnts(cfg)
311    GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000)
312    cnts = _get_rx_cnts(cfg, prev=cnts)
313
314    ksft_ge(sum(cnts[ :4]), 20000, "traffic on main context: " + str(cnts))
315    ksft_ge(sum(cnts[ :2]),  7000, "traffic on main context (1/2): " + str(cnts))
316    ksft_ge(sum(cnts[2:4]),  7000, "traffic on main context (2/2): " + str(cnts))
317    if other_ctx == 0:
318        ksft_eq(sum(cnts[4: ]),     0, "traffic on other queues: " + str(cnts))
319
320    # Now create a rule for context 1 and make sure traffic goes to a subset
321    if other_ctx:
322        ntuple.exec()
323    flow = f"flow-type tcp{cfg.addr_ipver} dst-port {port} context {ctx_id}"
324    ntuple_id = ethtool_create(cfg, "-N", flow)
325    defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
326
327    cnts = _get_rx_cnts(cfg)
328    GenerateTraffic(cfg, port=port).wait_pkts_and_stop(20000)
329    cnts = _get_rx_cnts(cfg, prev=cnts)
330
331    ksft_lt(sum(cnts[ :2]),  7000, "traffic on main context: " + str(cnts))
332    ksft_ge(sum(cnts[2:4]), 20000, "traffic on extra context: " + str(cnts))
333    if other_ctx == 0:
334        ksft_eq(sum(cnts[4: ]),     0, "traffic on other queues: " + str(cnts))
335
336
337def test_rss_context_overlap2(cfg):
338    test_rss_context_overlap(cfg, True)
339
340
341def main() -> None:
342    with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
343        cfg.netdevnl = NetdevFamily()
344
345        ksft_run([test_rss_key_indir,
346                  test_rss_context, test_rss_context4, test_rss_context32,
347                  test_rss_context_overlap, test_rss_context_overlap2,
348                  test_rss_context_out_of_order, test_rss_context4_create_with_cfg],
349                 args=(cfg, ))
350    ksft_exit()
351
352
353if __name__ == "__main__":
354    main()
355