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