xref: /linux/tools/testing/selftests/drivers/net/hw/rss_api.py (revision 8be4d31cb8aaeea27bde4b7ddb26e28a89062ebf)
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0
3
4"""
5API level tests for RSS (mostly Netlink vs IOCTL).
6"""
7
8import errno
9import glob
10import random
11from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_is, ksft_ne, ksft_raises
12from lib.py import KsftSkipEx, KsftFailEx
13from lib.py import defer, ethtool, CmdExitFailure
14from lib.py import EthtoolFamily, NlError
15from lib.py import NetDrvEnv
16
17
18def _require_2qs(cfg):
19    qcnt = len(glob.glob(f"/sys/class/net/{cfg.ifname}/queues/rx-*"))
20    if qcnt < 2:
21        raise KsftSkipEx(f"Local has only {qcnt} queues")
22    return qcnt
23
24
25def _ethtool_create(cfg, act, opts):
26    output = ethtool(f"{act} {cfg.ifname} {opts}").stdout
27    # Output will be something like: "New RSS context is 1" or
28    # "Added rule with ID 7", we want the integer from the end
29    return int(output.split()[-1])
30
31
32def _ethtool_get_cfg(cfg, fl_type, to_nl=False):
33    descr = ethtool(f"-n {cfg.ifname} rx-flow-hash {fl_type}").stdout
34
35    if to_nl:
36        converter = {
37            "IP SA": "ip-src",
38            "IP DA": "ip-dst",
39            "L4 bytes 0 & 1 [TCP/UDP src port]": "l4-b-0-1",
40            "L4 bytes 2 & 3 [TCP/UDP dst port]": "l4-b-2-3",
41        }
42
43        ret = set()
44    else:
45        converter = {
46            "IP SA": "s",
47            "IP DA": "d",
48            "L3 proto": "t",
49            "L4 bytes 0 & 1 [TCP/UDP src port]": "f",
50            "L4 bytes 2 & 3 [TCP/UDP dst port]": "n",
51        }
52
53        ret = ""
54
55    for line in descr.split("\n")[1:-2]:
56        # if this raises we probably need to add more keys to converter above
57        if to_nl:
58            ret.add(converter[line])
59        else:
60            ret += converter[line]
61    return ret
62
63
64def test_rxfh_nl_set_fail(cfg):
65    """
66    Test error path of Netlink SET.
67    """
68    _require_2qs(cfg)
69
70    ethnl = EthtoolFamily()
71    ethnl.ntf_subscribe("monitor")
72
73    with ksft_raises(NlError):
74        ethnl.rss_set({"header": {"dev-name": "lo"},
75                       "indir": None})
76
77    with ksft_raises(NlError):
78        ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
79                       "indir": [100000]})
80    ntf = next(ethnl.poll_ntf(duration=0.2), None)
81    ksft_is(ntf, None)
82
83
84def test_rxfh_nl_set_indir(cfg):
85    """
86    Test setting indirection table via Netlink.
87    """
88    qcnt = _require_2qs(cfg)
89
90    # Test some SETs with a value
91    reset = defer(cfg.ethnl.rss_set,
92                  {"header": {"dev-index": cfg.ifindex}, "indir": None})
93    cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
94                       "indir": [1]})
95    rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
96    ksft_eq(set(rss.get("indir", [-1])), {1})
97
98    cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
99                       "indir": [0, 1]})
100    rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
101    ksft_eq(set(rss.get("indir", [-1])), {0, 1})
102
103    # Make sure we can't set the queue count below max queue used
104    with ksft_raises(CmdExitFailure):
105        ethtool(f"-L {cfg.ifname} combined 0 rx 1")
106    with ksft_raises(CmdExitFailure):
107        ethtool(f"-L {cfg.ifname} combined 1 rx 0")
108
109    # Test reset back to default
110    reset.exec()
111    rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
112    ksft_eq(set(rss.get("indir", [-1])), set(range(qcnt)))
113
114
115def test_rxfh_nl_set_indir_ctx(cfg):
116    """
117    Test setting indirection table for a custom context via Netlink.
118    """
119    _require_2qs(cfg)
120
121    # Get setting for ctx 0, we'll make sure they don't get clobbered
122    dflt = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
123
124    # Create context
125    ctx_id = _ethtool_create(cfg, "-X", "context new")
126    defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
127
128    cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
129                       "context": ctx_id, "indir": [1]})
130    rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex},
131                             "context": ctx_id})
132    ksft_eq(set(rss.get("indir", [-1])), {1})
133
134    ctx0 = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
135    ksft_eq(ctx0, dflt)
136
137    cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
138                       "context": ctx_id, "indir": [0, 1]})
139    rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex},
140                             "context": ctx_id})
141    ksft_eq(set(rss.get("indir", [-1])), {0, 1})
142
143    ctx0 = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
144    ksft_eq(ctx0, dflt)
145
146    # Make sure we can't set the queue count below max queue used
147    with ksft_raises(CmdExitFailure):
148        ethtool(f"-L {cfg.ifname} combined 0 rx 1")
149    with ksft_raises(CmdExitFailure):
150        ethtool(f"-L {cfg.ifname} combined 1 rx 0")
151
152
153def test_rxfh_indir_ntf(cfg):
154    """
155    Check that Netlink notifications are generated when RSS indirection
156    table was modified.
157    """
158    _require_2qs(cfg)
159
160    ethnl = EthtoolFamily()
161    ethnl.ntf_subscribe("monitor")
162
163    ethtool(f"--disable-netlink -X {cfg.ifname} weight 0 1")
164    reset = defer(ethtool, f"-X {cfg.ifname} default")
165
166    ntf = next(ethnl.poll_ntf(duration=0.2), None)
167    if ntf is None:
168        raise KsftFailEx("No notification received")
169    ksft_eq(ntf["name"], "rss-ntf")
170    ksft_eq(set(ntf["msg"]["indir"]), {1})
171
172    reset.exec()
173    ntf = next(ethnl.poll_ntf(duration=0.2), None)
174    if ntf is None:
175        raise KsftFailEx("No notification received after reset")
176    ksft_eq(ntf["name"], "rss-ntf")
177    ksft_is(ntf["msg"].get("context"), None)
178    ksft_ne(set(ntf["msg"]["indir"]), {1})
179
180
181def test_rxfh_indir_ctx_ntf(cfg):
182    """
183    Check that Netlink notifications are generated when RSS indirection
184    table was modified on an additional RSS context.
185    """
186    _require_2qs(cfg)
187
188    ctx_id = _ethtool_create(cfg, "-X", "context new")
189    defer(ethtool, f"-X {cfg.ifname} context {ctx_id} delete")
190
191    ethnl = EthtoolFamily()
192    ethnl.ntf_subscribe("monitor")
193
194    ethtool(f"--disable-netlink -X {cfg.ifname} context {ctx_id} weight 0 1")
195
196    ntf = next(ethnl.poll_ntf(duration=0.2), None)
197    if ntf is None:
198        raise KsftFailEx("No notification received")
199    ksft_eq(ntf["name"], "rss-ntf")
200    ksft_eq(ntf["msg"].get("context"), ctx_id)
201    ksft_eq(set(ntf["msg"]["indir"]), {1})
202
203
204def test_rxfh_nl_set_key(cfg):
205    """
206    Test setting hashing key via Netlink.
207    """
208
209    dflt = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
210    defer(cfg.ethnl.rss_set,
211          {"header": {"dev-index": cfg.ifindex},
212           "hkey": dflt["hkey"], "indir": None})
213
214    # Empty key should error out
215    with ksft_raises(NlError) as cm:
216        cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
217                           "hkey": None})
218    ksft_eq(cm.exception.nl_msg.extack['bad-attr'], '.hkey')
219
220    # Set key to random
221    mod = random.randbytes(len(dflt["hkey"]))
222    cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
223                       "hkey": mod})
224    rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
225    ksft_eq(rss.get("hkey", [-1]), mod)
226
227    # Set key to random and indir tbl to something at once
228    mod = random.randbytes(len(dflt["hkey"]))
229    cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
230                       "indir": [0, 1], "hkey": mod})
231    rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
232    ksft_eq(rss.get("hkey", [-1]), mod)
233    ksft_eq(set(rss.get("indir", [-1])), {0, 1})
234
235
236def test_rxfh_fields(cfg):
237    """
238    Test reading Rx Flow Hash over Netlink.
239    """
240
241    flow_types = ["tcp4", "tcp6", "udp4", "udp6"]
242    ethnl = EthtoolFamily()
243
244    cfg_nl = ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
245    for fl_type in flow_types:
246        one = _ethtool_get_cfg(cfg, fl_type, to_nl=True)
247        ksft_eq(one, cfg_nl["flow-hash"][fl_type],
248                comment="Config for " + fl_type)
249
250
251def test_rxfh_fields_set(cfg):
252    """ Test configuring Rx Flow Hash over Netlink. """
253
254    flow_types = ["tcp4", "tcp6", "udp4", "udp6"]
255
256    # Collect current settings
257    cfg_old = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
258    # symmetric hashing is config-order-sensitive make sure we leave
259    # symmetric mode, or make the flow-hash sym-compatible first
260    changes = [{"flow-hash": cfg_old["flow-hash"],},
261               {"input-xfrm": cfg_old.get("input-xfrm", {}),}]
262    if cfg_old.get("input-xfrm"):
263        changes = list(reversed(changes))
264    for old in changes:
265        defer(cfg.ethnl.rss_set, {"header": {"dev-index": cfg.ifindex},} | old)
266
267    # symmetric hashing prevents some of the configs below
268    if cfg_old.get("input-xfrm"):
269        cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
270                           "input-xfrm": {}})
271
272    for fl_type in flow_types:
273        cur = _ethtool_get_cfg(cfg, fl_type)
274        if cur == "sdfn":
275            change_nl = {"ip-src", "ip-dst"}
276            change_ic = "sd"
277        else:
278            change_nl = {"l4-b-0-1", "l4-b-2-3", "ip-src", "ip-dst"}
279            change_ic = "sdfn"
280
281        cfg.ethnl.rss_set({
282            "header": {"dev-index": cfg.ifindex},
283            "flow-hash": {fl_type: change_nl}
284        })
285        reset = defer(ethtool, f"--disable-netlink -N {cfg.ifname} "
286                      f"rx-flow-hash {fl_type} {cur}")
287
288        cfg_nl = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
289        ksft_eq(change_nl, cfg_nl["flow-hash"][fl_type],
290                comment=f"Config for {fl_type} over Netlink")
291        cfg_ic = _ethtool_get_cfg(cfg, fl_type)
292        ksft_eq(change_ic, cfg_ic,
293                comment=f"Config for {fl_type} over IOCTL")
294
295        reset.exec()
296        cfg_nl = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
297        ksft_eq(cfg_old["flow-hash"][fl_type], cfg_nl["flow-hash"][fl_type],
298                comment=f"Un-config for {fl_type} over Netlink")
299        cfg_ic = _ethtool_get_cfg(cfg, fl_type)
300        ksft_eq(cur, cfg_ic, comment=f"Un-config for {fl_type} over IOCTL")
301
302    # Try to set multiple at once, the defer was already installed at the start
303    change = {"ip-src"}
304    if change == cfg_old["flow-hash"]["tcp4"]:
305        change = {"ip-dst"}
306    cfg.ethnl.rss_set({
307        "header": {"dev-index": cfg.ifindex},
308        "flow-hash": {x: change for x in flow_types}
309    })
310
311    cfg_nl = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
312    for fl_type in flow_types:
313        ksft_eq(change, cfg_nl["flow-hash"][fl_type],
314                comment=f"multi-config for {fl_type} over Netlink")
315
316
317def test_rxfh_fields_set_xfrm(cfg):
318    """ Test changing Rx Flow Hash vs xfrm_input at once.  """
319
320    def set_rss(cfg, xfrm, fh):
321        cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
322                           "input-xfrm": xfrm, "flow-hash": fh})
323
324    # Install the reset handler
325    cfg_old = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
326    # symmetric hashing is config-order-sensitive make sure we leave
327    # symmetric mode, or make the flow-hash sym-compatible first
328    changes = [{"flow-hash": cfg_old["flow-hash"],},
329               {"input-xfrm": cfg_old.get("input-xfrm", {}),}]
330    if cfg_old.get("input-xfrm"):
331        changes = list(reversed(changes))
332    for old in changes:
333        defer(cfg.ethnl.rss_set, {"header": {"dev-index": cfg.ifindex},} | old)
334
335    # Make sure we start with input-xfrm off, and tcp4 config non-sym
336    set_rss(cfg, {}, {})
337    set_rss(cfg, {}, {"tcp4": {"ip-src"}})
338
339    # Setting sym and fixing tcp4 config not expected to pass right now
340    with ksft_raises(NlError):
341        set_rss(cfg, {"sym-xor"}, {"tcp4": {"ip-src", "ip-dst"}})
342    # One at a time should work, hopefully
343    set_rss(cfg, 0, {"tcp4": {"ip-src", "ip-dst"}})
344    no_support = False
345    try:
346        set_rss(cfg, {"sym-xor"}, {})
347    except NlError:
348        try:
349            set_rss(cfg, {"sym-or-xor"}, {})
350        except NlError:
351            no_support = True
352    if no_support:
353        raise KsftSkipEx("no input-xfrm supported")
354    # Disabling two at once should not work either without kernel changes
355    with ksft_raises(NlError):
356        set_rss(cfg, {}, {"tcp4": {"ip-src"}})
357
358
359def test_rxfh_fields_ntf(cfg):
360    """ Test Rx Flow Hash notifications. """
361
362    cur = _ethtool_get_cfg(cfg, "tcp4")
363    if cur == "sdfn":
364        change = {"ip-src", "ip-dst"}
365    else:
366        change = {"l4-b-0-1", "l4-b-2-3", "ip-src", "ip-dst"}
367
368    ethnl = EthtoolFamily()
369    ethnl.ntf_subscribe("monitor")
370
371    ethnl.rss_set({
372        "header": {"dev-index": cfg.ifindex},
373        "flow-hash": {"tcp4": change}
374    })
375    reset = defer(ethtool,
376                  f"--disable-netlink -N {cfg.ifname} rx-flow-hash tcp4 {cur}")
377
378    ntf = next(ethnl.poll_ntf(duration=0.2), None)
379    if ntf is None:
380        raise KsftFailEx("No notification received after IOCTL change")
381    ksft_eq(ntf["name"], "rss-ntf")
382    ksft_eq(ntf["msg"]["flow-hash"]["tcp4"], change)
383    ksft_eq(next(ethnl.poll_ntf(duration=0.01), None), None)
384
385    reset.exec()
386    ntf = next(ethnl.poll_ntf(duration=0.2), None)
387    if ntf is None:
388        raise KsftFailEx("No notification received after Netlink change")
389    ksft_eq(ntf["name"], "rss-ntf")
390    ksft_ne(ntf["msg"]["flow-hash"]["tcp4"], change)
391    ksft_eq(next(ethnl.poll_ntf(duration=0.01), None), None)
392
393
394def test_rss_ctx_add(cfg):
395    """ Test creating an additional RSS context via Netlink """
396
397    _require_2qs(cfg)
398
399    # Test basic creation
400    ctx = cfg.ethnl.rss_create_act({"header": {"dev-index": cfg.ifindex}})
401    d = defer(ethtool, f"-X {cfg.ifname} context {ctx.get('context')} delete")
402    ksft_ne(ctx.get("context", 0), 0)
403    ksft_ne(set(ctx.get("indir", [0])), {0},
404            comment="Driver should init the indirection table")
405
406    # Try requesting the ID we just got allocated
407    with ksft_raises(NlError) as cm:
408        ctx = cfg.ethnl.rss_create_act({
409            "header": {"dev-index": cfg.ifindex},
410            "context": ctx.get("context"),
411        })
412        ethtool(f"-X {cfg.ifname} context {ctx.get('context')} delete")
413    d.exec()
414    ksft_eq(cm.exception.nl_msg.error, -errno.EBUSY)
415
416    # Test creating with a specified RSS table, and context ID
417    ctx_id = ctx.get("context")
418    ctx = cfg.ethnl.rss_create_act({
419        "header": {"dev-index": cfg.ifindex},
420        "context": ctx_id,
421        "indir": [1],
422    })
423    ethtool(f"-X {cfg.ifname} context {ctx.get('context')} delete")
424    ksft_eq(ctx.get("context"), ctx_id)
425    ksft_eq(set(ctx.get("indir", [0])), {1})
426
427
428def test_rss_ctx_ntf(cfg):
429    """ Test notifications for creating additional RSS contexts """
430
431    ethnl = EthtoolFamily()
432    ethnl.ntf_subscribe("monitor")
433
434    # Create / delete via Netlink
435    ctx = cfg.ethnl.rss_create_act({"header": {"dev-index": cfg.ifindex}})
436    cfg.ethnl.rss_delete_act({
437        "header": {"dev-index": cfg.ifindex},
438        "context": ctx["context"],
439    })
440
441    ntf = next(ethnl.poll_ntf(duration=0.2), None)
442    if ntf is None:
443        raise KsftFailEx("[NL] No notification after context creation")
444    ksft_eq(ntf["name"], "rss-create-ntf")
445    ksft_eq(ctx, ntf["msg"])
446
447    ntf = next(ethnl.poll_ntf(duration=0.2), None)
448    if ntf is None:
449        raise KsftFailEx("[NL] No notification after context deletion")
450    ksft_eq(ntf["name"], "rss-delete-ntf")
451
452    # Create / deleve via IOCTL
453    ctx_id = _ethtool_create(cfg, "--disable-netlink -X", "context new")
454    ethtool(f"--disable-netlink -X {cfg.ifname} context {ctx_id} delete")
455    ntf = next(ethnl.poll_ntf(duration=0.2), None)
456    if ntf is None:
457        raise KsftFailEx("[IOCTL] No notification after context creation")
458    ksft_eq(ntf["name"], "rss-create-ntf")
459
460    ntf = next(ethnl.poll_ntf(duration=0.2), None)
461    if ntf is None:
462        raise KsftFailEx("[IOCTL] No notification after context deletion")
463    ksft_eq(ntf["name"], "rss-delete-ntf")
464
465
466def main() -> None:
467    """ Ksft boiler plate main """
468
469    with NetDrvEnv(__file__, nsim_test=False) as cfg:
470        cfg.ethnl = EthtoolFamily()
471        ksft_run(globs=globals(), case_pfx={"test_"}, args=(cfg, ))
472    ksft_exit()
473
474
475if __name__ == "__main__":
476    main()
477