xref: /linux/tools/testing/selftests/drivers/net/shaper.py (revision cf85f810f911234a06a4ef2439e8694b93b717fc)
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0
3# pylint: disable=too-many-lines
4
5import errno
6import glob
7
8from lib.py import ksft_run, ksft_exit
9from lib.py import ksft_eq, ksft_true, ksft_raises, KsftSkipEx
10from lib.py import EthtoolFamily, NetshaperFamily
11from lib.py import NetDrvEnv
12from lib.py import NlError
13from lib.py import cmd, defer
14
15def _delete_shaper(cfg, nl_shaper, handle) -> None:
16    """ Delete the shaper identified by handle, ignoring a missing-shaper error. """
17    try:
18        nl_shaper.delete({'ifindex': cfg.ifindex,
19                          'handle': handle})
20    except NlError as e:
21        if e.error != errno.ENOENT:
22            raise
23
24def _require_queues(cfg, count):
25    """ Return the netdev TX queue count, skipping the test if fewer than count exist. """
26    qcnt = len(glob.glob(f"/sys/class/net/{cfg.ifname}/queues/tx-*"))
27    if qcnt < count:
28        raise KsftSkipEx(f"netdev has {qcnt} queues, {count} required")
29    return qcnt
30
31def _cap_get(cfg, nl_shaper, scope):
32    """ Return the shaper capabilities for the given scope, caching them on cfg. """
33    if not hasattr(cfg, 'cap_cache'):
34        cfg.cap_cache = {}
35    if scope not in cfg.cap_cache:
36        cfg.cap_cache[scope] = nl_shaper.cap_get({'ifindex': cfg.ifindex,
37                                                  'scope': scope})
38
39    return cfg.cap_cache[scope]
40
41def _require_caps(cfg, nl_shaper, scope, caps, msg) -> None:
42    """ Skip the test unless the given scope advertises all the required caps. """
43    try:
44        supported = _cap_get(cfg, nl_shaper, scope)
45    except NlError as e:
46        if e.error == errno.EOPNOTSUPP:
47            raise KsftSkipEx(f"{scope} scope shapers not supported by the device")
48        raise
49
50    if not set(caps).issubset(supported):
51        raise KsftSkipEx(msg)
52
53def get_shapers(cfg, nl_shaper) -> None:
54    try:
55        shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
56    except NlError as e:
57        if e.error == 95:
58            raise KsftSkipEx("shapers not supported by the device")
59        raise
60
61    # Default configuration: no shapers configured.
62    ksft_eq(len(shapers), 0)
63
64def get_caps(cfg, nl_shaper) -> None:
65    try:
66        caps = nl_shaper.cap_get({'ifindex': cfg.ifindex}, dump=True)
67    except NlError as e:
68        if e.error == 95:
69            raise KsftSkipEx("shapers not supported by the device")
70        raise
71
72    # Each device implementing shaper support must support some
73    # features in at least a scope.
74    ksft_true(len(caps)> 0)
75
76def set_qshapers(cfg, nl_shaper) -> None:
77    try:
78        caps = nl_shaper.cap_get({'ifindex': cfg.ifindex,
79                                 'scope':'queue'})
80    except NlError as e:
81        if e.error == 95:
82            raise KsftSkipEx("shapers not supported by the device")
83        raise
84    if not 'support-bw-max' in caps or not 'support-metric-bps' in caps:
85        raise KsftSkipEx("device does not support queue scope shapers with bw_max and metric bps")
86
87    _require_queues(cfg, 3)
88    cfg.queues = True
89
90    nl_shaper.set({'ifindex': cfg.ifindex,
91                   'handle': {'scope': 'queue', 'id': 1},
92                   'metric': 'bps',
93                   'bw-max': 10000})
94    nl_shaper.set({'ifindex': cfg.ifindex,
95                   'handle': {'scope': 'queue', 'id': 2},
96                   'metric': 'bps',
97                   'bw-max': 20000})
98
99    # Querying a specific shaper not yet configured must fail.
100    raised = False
101    try:
102        shaper_q0 = nl_shaper.get({'ifindex': cfg.ifindex,
103                                   'handle': {'scope': 'queue', 'id': 0}})
104    except (NlError):
105        raised = True
106    ksft_eq(raised, True)
107
108    shaper_q1 = nl_shaper.get({'ifindex': cfg.ifindex,
109                              'handle': {'scope': 'queue', 'id': 1}})
110    ksft_eq(shaper_q1, {'ifindex': cfg.ifindex,
111                        'parent': {'scope': 'netdev'},
112                        'handle': {'scope': 'queue', 'id': 1},
113                        'metric': 'bps',
114                        'bw-max': 10000})
115
116    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
117    ksft_eq(shapers, [{'ifindex': cfg.ifindex,
118                       'parent': {'scope': 'netdev'},
119                       'handle': {'scope': 'queue', 'id': 1},
120                       'metric': 'bps',
121                       'bw-max': 10000},
122                      {'ifindex': cfg.ifindex,
123                       'parent': {'scope': 'netdev'},
124                       'handle': {'scope': 'queue', 'id': 2},
125                       'metric': 'bps',
126                       'bw-max': 20000}])
127
128def del_qshapers(cfg, nl_shaper) -> None:
129    if not cfg.queues:
130        raise KsftSkipEx("queue shapers not supported by device, skipping delete")
131
132    nl_shaper.delete({'ifindex': cfg.ifindex,
133                      'handle': {'scope': 'queue', 'id': 2}})
134    nl_shaper.delete({'ifindex': cfg.ifindex,
135                      'handle': {'scope': 'queue', 'id': 1}})
136    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
137    ksft_eq(len(shapers), 0)
138
139def set_nshapers(cfg, nl_shaper) -> None:
140    # Check required features.
141    try:
142        caps = nl_shaper.cap_get({'ifindex': cfg.ifindex,
143                                  'scope':'netdev'})
144    except NlError as e:
145        if e.error == 95:
146            raise KsftSkipEx("shapers not supported by the device")
147        raise
148    if not 'support-bw-max' in caps or not 'support-metric-bps' in caps:
149        raise KsftSkipEx("device does not support nested netdev scope shapers with weight")
150
151    cfg.netdev = True;
152    nl_shaper.set({'ifindex': cfg.ifindex,
153                   'handle': {'scope': 'netdev', 'id': 0},
154                   'bw-max': 100000})
155
156    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
157    ksft_eq(shapers, [{'ifindex': cfg.ifindex,
158                       'handle': {'scope': 'netdev'},
159                       'metric': 'bps',
160                       'bw-max': 100000}])
161
162def del_nshapers(cfg, nl_shaper) -> None:
163    if not cfg.netdev:
164        raise KsftSkipEx("netdev shaper not supported by device, skipping delete")
165
166    nl_shaper.delete({'ifindex': cfg.ifindex,
167                      'handle': {'scope': 'netdev'}})
168    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
169    ksft_eq(len(shapers), 0)
170
171def set_all_supported_attrs(cfg, nl_shaper) -> None:
172    """ Set every queue-scope attribute the device advertises and verify the read-back. """
173    _require_queues(cfg, 1)
174
175    _require_caps(cfg, nl_shaper, 'queue', [],
176                  "queue scope shapers not supported by the device")
177    caps = _cap_get(cfg, nl_shaper, 'queue')
178
179    attrs = {'ifindex': cfg.ifindex,
180             'handle': {'scope': 'queue', 'id': 0}}
181    expected = {'ifindex': cfg.ifindex,
182                'parent': {'scope': 'netdev'},
183                'handle': {'scope': 'queue', 'id': 0}}
184
185    rate_attrs = {'support-bw-min': ('bw-min', 10000, 100),
186                  'support-bw-max': ('bw-max', 20000, 200),
187                  'support-burst': ('burst', 3000, 30)}
188    rate_attr_supported = any(cap in caps for cap in rate_attrs)
189    bps_supported = 'support-metric-bps' in caps
190    pps_supported = 'support-metric-pps' in caps
191
192    def add_rate_attrs(metric, value_idx) -> None:
193        attrs['metric'] = metric
194        expected['metric'] = metric
195        for cap, (attr, bps_value, pps_value) in rate_attrs.items():
196            if cap not in caps:
197                continue
198
199            value = bps_value if value_idx == 0 else pps_value
200            attrs[attr] = value
201            expected[attr] = value
202
203    if rate_attr_supported:
204        if bps_supported:
205            add_rate_attrs('bps', 0)
206        elif pps_supported:
207            add_rate_attrs('pps', 1)
208
209    if 'support-priority' in caps:
210        attrs['priority'] = 1
211        expected['priority'] = 1
212    if 'support-weight' in caps:
213        attrs['weight'] = 2
214        expected['weight'] = 2
215
216    if len(attrs) == 2:
217        raise KsftSkipEx("device does not advertise any supported queue shaper attributes")
218
219    nl_shaper.set(attrs)
220    defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 0})
221
222    shaper = nl_shaper.get({'ifindex': cfg.ifindex,
223                            'handle': {'scope': 'queue', 'id': 0}})
224    ksft_eq(shaper, expected)
225
226    if rate_attr_supported and bps_supported and pps_supported:
227        add_rate_attrs('pps', 1)
228        nl_shaper.set(attrs)
229
230        shaper = nl_shaper.get({'ifindex': cfg.ifindex,
231                                'handle': {'scope': 'queue', 'id': 0}})
232        ksft_eq(shaper, expected)
233
234    _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': 0})
235    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
236    ksft_eq(len(shapers), 0)
237
238def invalid_set_preserves_state(cfg, nl_shaper) -> None:
239    """ Verify a rejected .set leaves the existing shaper configuration unchanged. """
240    nq = _require_queues(cfg, 1)
241    _require_caps(cfg, nl_shaper, 'queue',
242                  ['support-bw-max', 'support-metric-bps'],
243                  "device does not support queue scope bw_max with bps metric")
244
245    initial = {'ifindex': cfg.ifindex,
246               'parent': {'scope': 'netdev'},
247               'handle': {'scope': 'queue', 'id': 0},
248               'metric': 'bps',
249               'bw-max': 10000}
250    nl_shaper.set({'ifindex': cfg.ifindex,
251                   'handle': {'scope': 'queue', 'id': 0},
252                   'metric': 'bps',
253                   'bw-max': 10000})
254    defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 0})
255
256    with ksft_raises(NlError):
257        nl_shaper.set({'ifindex': cfg.ifindex,
258                       'handle': {'scope': 'node', 'id': 0},
259                       'metric': 'bps',
260                       'bw-max': 20000})
261    shaper = nl_shaper.get({'ifindex': cfg.ifindex,
262                            'handle': {'scope': 'queue', 'id': 0}})
263    ksft_eq(shaper, initial)
264
265    with ksft_raises(NlError):
266        nl_shaper.set({'ifindex': cfg.ifindex,
267                       'handle': {'scope': 'queue', 'id': nq},
268                       'metric': 'bps',
269                       'bw-max': 20000})
270    shaper = nl_shaper.get({'ifindex': cfg.ifindex,
271                            'handle': {'scope': 'queue', 'id': 0}})
272    ksft_eq(shaper, initial)
273
274    _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': 0})
275    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
276    ksft_eq(len(shapers), 0)
277
278def mixed_parent_group_requires_parent(cfg, nl_shaper) -> None:
279    r"""Grouping leaves from different nodes requires an explicit parent.
280
281        netdev             netdev
282        /    \             parent=netdev
283       N1     N2  group        N
284       |      |   {Q0,Q1}     / \
285       Q0     Q1  ------->   Q0  Q1
286
287    Without an explicit parent the group is rejected; parent=netdev
288    collapses the leaves into one new node.
289    """
290    _require_queues(cfg, 2)
291    _require_caps(cfg, nl_shaper, 'node',
292                  ['support-bw-max', 'support-metric-bps'],
293                  "device does not support node scope shapers with bw_max and metric bps")
294    _require_caps(cfg, nl_shaper, 'queue',
295                  ['support-nesting', 'support-weight'],
296                  "device does not support nested queue scope shapers with weight")
297
298    n1_handle = nl_shaper.group({
299                   'ifindex': cfg.ifindex,
300                   'leaves':[{'handle': {'scope': 'queue', 'id': 0},
301                              'weight': 1}],
302                   'handle': {'scope':'node'},
303                   'metric': 'bps',
304                   'bw-max': 10000})
305    n1_id = n1_handle['handle']['id']
306    defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 0})
307
308    n2_handle = nl_shaper.group({
309                   'ifindex': cfg.ifindex,
310                   'leaves':[{'handle': {'scope': 'queue', 'id': 1},
311                              'weight': 2}],
312                   'handle': {'scope':'node'},
313                   'metric': 'bps',
314                   'bw-max': 20000})
315    n2_id = n2_handle['handle']['id']
316    defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 1})
317
318    with ksft_raises(NlError):
319        nl_shaper.group({
320                   'ifindex': cfg.ifindex,
321                   'leaves':[{'handle': {'scope': 'queue', 'id': 0},
322                              'weight': 3},
323                             {'handle': {'scope': 'queue', 'id': 1},
324                              'weight': 4}],
325                   'handle': {'scope':'node'},
326                   'metric': 'bps',
327                   'bw-max': 30000})
328
329    shaper_q0 = nl_shaper.get({'ifindex': cfg.ifindex,
330                               'handle': {'scope': 'queue', 'id': 0}})
331    ksft_eq(shaper_q0, {'ifindex': cfg.ifindex,
332                        'parent': {'scope': 'node', 'id': n1_id},
333                        'handle': {'scope': 'queue', 'id': 0},
334                        'weight': 1})
335    shaper_q1 = nl_shaper.get({'ifindex': cfg.ifindex,
336                               'handle': {'scope': 'queue', 'id': 1}})
337    ksft_eq(shaper_q1, {'ifindex': cfg.ifindex,
338                        'parent': {'scope': 'node', 'id': n2_id},
339                        'handle': {'scope': 'queue', 'id': 1},
340                        'weight': 2})
341
342    node_handle = nl_shaper.group({
343                   'ifindex': cfg.ifindex,
344                   'leaves':[{'handle': {'scope': 'queue', 'id': 0},
345                              'weight': 3},
346                             {'handle': {'scope': 'queue', 'id': 1},
347                              'weight': 4}],
348                   'handle': {'scope':'node'},
349                   'parent': {'scope': 'netdev'},
350                   'metric': 'bps',
351                   'bw-max': 30000})
352    node_id = node_handle['handle']['id']
353
354    for old_id in (n1_id, n2_id):
355        with ksft_raises(NlError):
356            nl_shaper.get({'ifindex': cfg.ifindex,
357                           'handle': {'scope': 'node', 'id': old_id}})
358
359    shaper_q0 = nl_shaper.get({'ifindex': cfg.ifindex,
360                               'handle': {'scope': 'queue', 'id': 0}})
361    ksft_eq(shaper_q0, {'ifindex': cfg.ifindex,
362                        'parent': {'scope': 'node', 'id': node_id},
363                        'handle': {'scope': 'queue', 'id': 0},
364                        'weight': 3})
365    shaper_q1 = nl_shaper.get({'ifindex': cfg.ifindex,
366                               'handle': {'scope': 'queue', 'id': 1}})
367    ksft_eq(shaper_q1, {'ifindex': cfg.ifindex,
368                        'parent': {'scope': 'node', 'id': node_id},
369                        'handle': {'scope': 'queue', 'id': 1},
370                        'weight': 4})
371
372    for i in range(2):
373        _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i})
374    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
375    ksft_eq(len(shapers), 0)
376
377def recursive_empty_node_cleanup(cfg, nl_shaper) -> None:
378    r"""Deleting the last leaf recursively removes the emptied ancestors.
379
380        netdev             netdev
381          |       del Q0
382         N1       ------>   (N1 and N2 removed too)
383          |
384         N2
385          |
386         Q0
387    """
388    _require_queues(cfg, 1)
389    _require_caps(cfg, nl_shaper, 'node',
390                  ['support-bw-max', 'support-metric-bps', 'support-nesting'],
391                  "device does not support nested node scope shapers")
392    _require_caps(cfg, nl_shaper, 'queue',
393                  ['support-nesting', 'support-weight'],
394                  "device does not support nested queue scope shapers with weight")
395
396    n1_handle = nl_shaper.group({
397                   'ifindex': cfg.ifindex,
398                   'leaves':[{'handle': {'scope': 'queue', 'id': 0},
399                              'weight': 1}],
400                   'handle': {'scope':'node'},
401                   'metric': 'bps',
402                   'bw-max': 10000})
403    n1_id = n1_handle['handle']['id']
404    defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 0})
405
406    n2_handle = nl_shaper.group({
407                   'ifindex': cfg.ifindex,
408                   'leaves':[{'handle': {'scope': 'queue', 'id': 0},
409                              'weight': 1}],
410                   'handle': {'scope':'node'},
411                   'parent': {'scope': 'node', 'id': n1_id},
412                   'metric': 'bps',
413                   'bw-max': 5000})
414    n2_id = n2_handle['handle']['id']
415
416    shaper_q0 = nl_shaper.get({'ifindex': cfg.ifindex,
417                               'handle': {'scope': 'queue', 'id': 0}})
418    ksft_eq(shaper_q0, {'ifindex': cfg.ifindex,
419                        'parent': {'scope': 'node', 'id': n2_id},
420                        'handle': {'scope': 'queue', 'id': 0},
421                        'weight': 1})
422
423    nl_shaper.delete({'ifindex': cfg.ifindex,
424                      'handle': {'scope': 'queue', 'id': 0}})
425
426    for handle in ({'scope': 'queue', 'id': 0},
427                   {'scope': 'node', 'id': n2_id},
428                   {'scope': 'node', 'id': n1_id}):
429        with ksft_raises(NlError):
430            nl_shaper.get({'ifindex': cfg.ifindex, 'handle': handle})
431
432    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
433    ksft_eq(len(shapers), 0)
434
435def _group_under_netdev(cfg, nl_shaper, bw_max=None):
436    r"""Group queues under a netdev-scope node; caller owns node teardown.
437
438        netdev               netdev
439         /  \      del Q1,Q2
440        Q1  Q2     ------->   (netdev node persists)
441    """
442    group_args = {
443        'ifindex': cfg.ifindex,
444        'leaves': [{'handle': {'scope': 'queue', 'id': 1},
445                    'weight': 1},
446                   {'handle': {'scope': 'queue', 'id': 2},
447                    'weight': 2}],
448        'handle': {'scope': 'netdev'}}
449    if bw_max:
450        group_args['metric'] = 'bps'
451        group_args['bw-max'] = bw_max
452
453    node_handle = nl_shaper.group(group_args)
454    ksft_eq(node_handle, {'ifindex': cfg.ifindex,
455                          'handle': {'scope': 'netdev'}})
456
457    del_node = defer(_delete_shaper, cfg, nl_shaper, {'scope': 'netdev'})
458    del_queues = [defer(_delete_shaper, cfg, nl_shaper,
459                        {'scope': 'queue', 'id': qid})
460                  for qid in (1, 2)]
461
462    shaper = nl_shaper.get({'ifindex': cfg.ifindex,
463                            'handle': {'scope': 'queue', 'id': 1}})
464    ksft_eq(shaper, {'ifindex': cfg.ifindex,
465                     'parent': {'scope': 'netdev'},
466                     'handle': {'scope': 'queue', 'id': 1},
467                     'weight': 1})
468    for dq in del_queues:
469        dq.exec()
470
471    # Caller owns the node teardown so it can verify the netdev-scope node
472    # survives leaf deletion before removing it.
473    return del_node
474
475def basic_groups(cfg, nl_shaper) -> None:
476    r"""Group queues under a netdev-scope node, then tear it down.
477
478        netdev
479         /  \
480        Q1  Q2
481    """
482    _require_queues(cfg, 3)
483
484    _require_caps(cfg, nl_shaper, 'netdev', [], "netdev scope not supported by the device")
485    _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'],
486                  "queue scope not supported with nesting and weight")
487
488    del_node = _group_under_netdev(cfg, nl_shaper)
489
490    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
491    ksft_eq(shapers, [{'ifindex': cfg.ifindex,
492                       'handle': {'scope': 'netdev'}}])
493
494    del_node.exec()
495    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
496    ksft_eq(len(shapers), 0)
497
498def basic_groups_with_rate(cfg, nl_shaper) -> None:
499    r"""Rate-limited netdev-scope node outlives deletion of its leaves.
500
501        netdev[10kbps]          netdev[10kbps]
502          /  \       del Q1,Q2
503        Q1    Q2     ------->    (node persists)
504    """
505    bw_max = 10000
506
507    _require_queues(cfg, 3)
508
509    _require_caps(cfg, nl_shaper, 'netdev', ['support-bw-max', 'support-metric-bps'],
510                  "device does not support netdev scope rate limiting")
511    _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'],
512                  "device does not support queue scope shapers with nesting and weight")
513
514    del_node = _group_under_netdev(cfg, nl_shaper, bw_max=bw_max)
515
516    # Deleting all the leaves shaper does not affect the node one
517    # when the latter has 'netdev' scope.
518    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
519    ksft_eq(shapers, [{'ifindex': cfg.ifindex,
520                       'handle': {'scope': 'netdev'},
521                       'metric': 'bps',
522                       'bw-max': bw_max}])
523
524    del_node.exec()
525
526def qgroups(cfg, nl_shaper) -> None:
527    _require_queues(cfg, 4)
528    _require_caps(cfg, nl_shaper, 'node',
529                  ['support-bw-max', 'support-metric-bps'],
530                  "device does not support node scope shapers with bw_max and metric bps")
531    _require_caps(cfg, nl_shaper, 'queue',
532                  ['support-nesting', 'support-weight'],
533                  "device does not support nested queue scope shapers with weight")
534
535    node_handle = nl_shaper.group({
536                   'ifindex': cfg.ifindex,
537                   'leaves':[{'handle': {'scope': 'queue', 'id': 1},
538                              'weight': 3},
539                             {'handle': {'scope': 'queue', 'id': 2},
540                              'weight': 2}],
541                   'handle': {'scope':'node'},
542                   'metric': 'bps',
543                   'bw-max': 10000})
544    node_id = node_handle['handle']['id']
545
546    shaper = nl_shaper.get({'ifindex': cfg.ifindex,
547                            'handle': {'scope': 'queue', 'id': 1}})
548    ksft_eq(shaper, {'ifindex': cfg.ifindex,
549                     'parent': {'scope': 'node', 'id': node_id},
550                     'handle': {'scope': 'queue', 'id': 1},
551                     'weight': 3})
552    shaper = nl_shaper.get({'ifindex': cfg.ifindex,
553                            'handle': {'scope': 'node', 'id': node_id}})
554    ksft_eq(shaper, {'ifindex': cfg.ifindex,
555                     'handle': {'scope': 'node', 'id': node_id},
556                     'parent': {'scope': 'netdev'},
557                     'metric': 'bps',
558                     'bw-max': 10000})
559
560    # Grouping to a specified, not existing node scope shaper must fail
561    raised = False
562    try:
563        nl_shaper.group({
564                   'ifindex': cfg.ifindex,
565                   'leaves':[{'handle': {'scope': 'queue', 'id': 3},
566                              'weight': 3}],
567                   'handle': {'scope':'node', 'id': node_id + 1},
568                   'metric': 'bps',
569                   'bw-max': 10000})
570
571    except (NlError):
572        raised = True
573    ksft_eq(raised, True)
574
575    # Add to an existing node
576    node_handle = nl_shaper.group({
577                   'ifindex': cfg.ifindex,
578                   'leaves':[{'handle': {'scope': 'queue', 'id': 3},
579                              'weight': 4}],
580                   'handle': {'scope':'node', 'id': node_id}})
581    ksft_eq(node_handle, {'ifindex': cfg.ifindex,
582                          'handle': {'scope': 'node', 'id': node_id}})
583
584    shaper = nl_shaper.get({'ifindex': cfg.ifindex,
585                            'handle': {'scope': 'queue', 'id': 3}})
586    ksft_eq(shaper, {'ifindex': cfg.ifindex,
587                     'parent': {'scope': 'node', 'id': node_id},
588                     'handle': {'scope': 'queue', 'id': 3},
589                     'weight': 4})
590
591    nl_shaper.delete({'ifindex': cfg.ifindex,
592                      'handle': {'scope': 'queue', 'id': 2}})
593    nl_shaper.delete({'ifindex': cfg.ifindex,
594                      'handle': {'scope': 'queue', 'id': 1}})
595
596    # Deleting a non empty node will move the leaves downstream.
597    nl_shaper.delete({'ifindex': cfg.ifindex,
598                      'handle': {'scope': 'node', 'id': node_id}})
599    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
600    ksft_eq(shapers, [{'ifindex': cfg.ifindex,
601                       'parent': {'scope': 'netdev'},
602                       'handle': {'scope': 'queue', 'id': 3},
603                       'weight': 4}])
604
605    # Finish and verify the complete cleanup.
606    nl_shaper.delete({'ifindex': cfg.ifindex,
607                      'handle': {'scope': 'queue', 'id': 3}})
608    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
609    ksft_eq(len(shapers), 0)
610
611def set_node_shaper(cfg, nl_shaper) -> None:
612    """ Verify a node-scope shaper rate can be updated via .set. """
613    _require_queues(cfg, 2)
614    _require_caps(cfg, nl_shaper, 'node', ['support-bw-max', 'support-metric-bps'],
615                  "device does not support node scope shapers with bw_max and metric bps")
616    _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'],
617                  "device does not support nested queue scope shapers with weight")
618
619    node_handle = nl_shaper.group({
620                   'ifindex': cfg.ifindex,
621                   'leaves':[{'handle': {'scope': 'queue', 'id': 1},
622                              'weight': 1}],
623                   'handle': {'scope':'node'},
624                   'metric': 'bps',
625                   'bw-max': 10000})
626    node_id = node_handle['handle']['id']
627    defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 1})
628
629    # Update the node's rate via .set
630    nl_shaper.set({'ifindex': cfg.ifindex,
631                   'handle': {'scope': 'node', 'id': node_id},
632                   'metric': 'bps',
633                   'bw-max': 20000})
634
635    shaper = nl_shaper.get({'ifindex': cfg.ifindex,
636                            'handle': {'scope': 'node', 'id': node_id}})
637    ksft_eq(shaper, {'ifindex': cfg.ifindex,
638                     'handle': {'scope': 'node', 'id': node_id},
639                     'parent': {'scope': 'netdev'},
640                     'metric': 'bps',
641                     'bw-max': 20000})
642
643    # Cleanup
644    _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': 1})
645    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
646    ksft_eq(len(shapers), 0)
647
648def group_update_rate(cfg, nl_shaper) -> None:
649    """ Verify re-grouping a node updates its rate while leaving the leaves untouched. """
650    _require_queues(cfg, 3)
651    _require_caps(cfg, nl_shaper, 'node', ['support-bw-max', 'support-metric-bps'],
652                  "device does not support node scope shapers with bw_max and metric bps")
653    _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'],
654                  "device does not support nested queue scope shapers with weight")
655
656    # Create node with Q1, Q2 at bw_max=10000
657    node_handle = nl_shaper.group({
658                   'ifindex': cfg.ifindex,
659                   'leaves':[{'handle': {'scope': 'queue', 'id': 1},
660                              'weight': 1},
661                             {'handle': {'scope': 'queue', 'id': 2},
662                              'weight': 1}],
663                   'handle': {'scope':'node'},
664                   'metric': 'bps',
665                   'bw-max': 10000})
666    node_id = node_handle['handle']['id']
667    for i in range(1, 3):
668        defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': i})
669
670    # Update rate via .group on the same node
671    nl_shaper.group({
672                   'ifindex': cfg.ifindex,
673                   'leaves':[{'handle': {'scope': 'queue', 'id': 1},
674                              'weight': 1},
675                             {'handle': {'scope': 'queue', 'id': 2},
676                              'weight': 1}],
677                   'handle': {'scope':'node', 'id': node_id},
678                   'metric': 'bps',
679                   'bw-max': 50000})
680
681    # Verify rate updated
682    shaper = nl_shaper.get({'ifindex': cfg.ifindex,
683                            'handle': {'scope': 'node', 'id': node_id}})
684    ksft_eq(shaper, {'ifindex': cfg.ifindex,
685                     'handle': {'scope': 'node', 'id': node_id},
686                     'parent': {'scope': 'netdev'},
687                     'metric': 'bps',
688                     'bw-max': 50000})
689
690    # Verify leaves unchanged
691    shaper_q1 = nl_shaper.get({'ifindex': cfg.ifindex,
692                               'handle': {'scope': 'queue', 'id': 1}})
693    ksft_eq(shaper_q1, {'ifindex': cfg.ifindex,
694                        'parent': {'scope': 'node', 'id': node_id},
695                        'handle': {'scope': 'queue', 'id': 1},
696                        'weight': 1})
697    shaper_q2 = nl_shaper.get({'ifindex': cfg.ifindex,
698                               'handle': {'scope': 'queue', 'id': 2}})
699    ksft_eq(shaper_q2, {'ifindex': cfg.ifindex,
700                        'parent': {'scope': 'node', 'id': node_id},
701                        'handle': {'scope': 'queue', 'id': 2},
702                        'weight': 1})
703
704    # Make sure we only have 3 shapers including 2 queues and the node
705    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
706    ksft_eq(len(shapers), 3)
707
708    # Cleanup
709    for i in range(1, 3):
710        _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i})
711    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
712    ksft_eq(len(shapers), 0)
713
714def delegation(cfg, nl_shaper) -> None:
715    _require_queues(cfg, 4)
716    _require_caps(cfg, nl_shaper, 'node',
717                  ['support-bw-max', 'support-metric-bps', 'support-nesting'],
718                  "device does not support node scope shapers with bw_max, metric bps and nesting")
719    _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'],
720                  "device does not support nested queue scope shapers with weight")
721
722    node_handle = nl_shaper.group({
723                   'ifindex': cfg.ifindex,
724                   'leaves':[{'handle': {'scope': 'queue', 'id': 1},
725                              'weight': 3},
726                             {'handle': {'scope': 'queue', 'id': 2},
727                              'weight': 2},
728                             {'handle': {'scope': 'queue', 'id': 3},
729                              'weight': 1}],
730                   'handle': {'scope':'node'},
731                   'metric': 'bps',
732                   'bw-max': 10000})
733    node_id = node_handle['handle']['id']
734
735    # Create the nested node and validate the hierarchy
736    nested_node_handle = nl_shaper.group({
737                   'ifindex': cfg.ifindex,
738                   'leaves':[{'handle': {'scope': 'queue', 'id': 1},
739                              'weight': 3},
740                             {'handle': {'scope': 'queue', 'id': 2},
741                              'weight': 2}],
742                   'handle': {'scope':'node'},
743                   'metric': 'bps',
744                   'bw-max': 5000})
745    nested_node_id = nested_node_handle['handle']['id']
746    ksft_true(nested_node_id != node_id)
747    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
748    ksft_eq(shapers, [{'ifindex': cfg.ifindex,
749                       'parent': {'scope': 'node', 'id': nested_node_id},
750                       'handle': {'scope': 'queue', 'id': 1},
751                       'weight': 3},
752                      {'ifindex': cfg.ifindex,
753                       'parent': {'scope': 'node', 'id': nested_node_id},
754                       'handle': {'scope': 'queue', 'id': 2},
755                       'weight': 2},
756                      {'ifindex': cfg.ifindex,
757                       'parent': {'scope': 'node', 'id': node_id},
758                       'handle': {'scope': 'queue', 'id': 3},
759                       'weight': 1},
760                      {'ifindex': cfg.ifindex,
761                       'parent': {'scope': 'netdev'},
762                       'handle': {'scope': 'node', 'id': node_id},
763                       'metric': 'bps',
764                       'bw-max': 10000},
765                      {'ifindex': cfg.ifindex,
766                       'parent': {'scope': 'node', 'id': node_id},
767                       'handle': {'scope': 'node', 'id': nested_node_id},
768                       'metric': 'bps',
769                       'bw-max': 5000}])
770
771    # Deleting a non empty node will move the leaves downstream.
772    nl_shaper.delete({'ifindex': cfg.ifindex,
773                      'handle': {'scope': 'node', 'id': nested_node_id}})
774    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
775    ksft_eq(shapers, [{'ifindex': cfg.ifindex,
776                       'parent': {'scope': 'node', 'id': node_id},
777                       'handle': {'scope': 'queue', 'id': 1},
778                       'weight': 3},
779                      {'ifindex': cfg.ifindex,
780                       'parent': {'scope': 'node', 'id': node_id},
781                       'handle': {'scope': 'queue', 'id': 2},
782                       'weight': 2},
783                      {'ifindex': cfg.ifindex,
784                       'parent': {'scope': 'node', 'id': node_id},
785                       'handle': {'scope': 'queue', 'id': 3},
786                       'weight': 1},
787                      {'ifindex': cfg.ifindex,
788                       'parent': {'scope': 'netdev'},
789                       'handle': {'scope': 'node', 'id': node_id},
790                       'metric': 'bps',
791                       'bw-max': 10000}])
792
793    # Final cleanup.
794    for i in range(1, 4):
795        nl_shaper.delete({'ifindex': cfg.ifindex,
796                          'handle': {'scope': 'queue', 'id': i}})
797    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
798    ksft_eq(len(shapers), 0)
799
800def nested_depth_limit(cfg, nl_shaper) -> None:
801    r"""Nest nodes as deep as the device allows to find the max depth.
802
803        netdev
804          |
805         N1 -- Q1
806          |
807         N2 -- Q2
808          |
809         N3 -- Q3
810          :       (deepen until the driver rejects)
811    """
812    bw_max = 10000
813
814    _require_caps(cfg, nl_shaper, 'node',
815                  ['support-bw-max', 'support-metric-bps', 'support-nesting'],
816                  "device does not support node scope shapers with bw_max, metric bps and nesting")
817    _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'],
818                  "device does not support nested queue scope shapers with weight")
819
820    nq = _require_queues(cfg, 3)
821
822    node_ids = []
823    cleanups = []
824    queue_id = 1
825    max_depth = 0
826    limit_err = None
827
828    # Create initial node with a queue leaf
829    node_id = nl_shaper.group({
830        'ifindex': cfg.ifindex,
831        'leaves': [{'handle': {'scope': 'queue', 'id': queue_id},
832                     'weight': 1}],
833        'handle': {'scope': 'node'},
834        'metric': 'bps',
835        'bw-max': bw_max})['handle']['id']
836    node_ids.append(node_id)
837    cleanups.append(defer(_delete_shaper, cfg, nl_shaper,
838                          {'scope': 'node', 'id': node_id}))
839    cleanups.append(defer(_delete_shaper, cfg, nl_shaper,
840                          {'scope': 'queue', 'id': queue_id}))
841    max_depth = 1
842    shaper = nl_shaper.get({'ifindex': cfg.ifindex,
843                            'handle': {'scope': 'node', 'id': node_id}})
844    ksft_eq(shaper, {'ifindex': cfg.ifindex,
845                     'handle': {'scope': 'node', 'id': node_id},
846                     'parent': {'scope': 'netdev'},
847                     'metric': 'bps',
848                     'bw-max': bw_max})
849    shaper = nl_shaper.get({'ifindex': cfg.ifindex,
850                            'handle': {'scope': 'queue', 'id': queue_id}})
851    ksft_eq(shaper, {'ifindex': cfg.ifindex,
852                     'parent': {'scope': 'node', 'id': node_id},
853                     'handle': {'scope': 'queue', 'id': queue_id},
854                     'weight': 1})
855    queue_id += 1
856
857    # Keep nesting deeper until the driver rejects or queues run out.
858    while queue_id < nq:
859        parent_id = node_ids[-1]
860        try:
861            node_id = nl_shaper.group({
862                'ifindex': cfg.ifindex,
863                'leaves': [{'handle': {'scope': 'queue',
864                                       'id': queue_id},
865                             'weight': 1}],
866                'handle': {'scope': 'node'},
867                'parent': {'scope': 'node',
868                           'id': parent_id},
869                'metric': 'bps',
870                'bw-max': bw_max})['handle']['id']
871        except NlError as e:
872            # Only treat "cannot nest deeper" errors as the depth limit;
873            # drivers report it differently (EOPNOTSUPP/ENOSPC/E2BIG/EINVAL).
874            # Anything else (ENOMEM, EIO, EPERM, driver bug) is a real failure.
875            if e.error not in (errno.EOPNOTSUPP, errno.ENOSPC,
876                               errno.E2BIG, errno.EINVAL):
877                raise
878            limit_err = e
879            break
880
881        node_ids.append(node_id)
882        cleanups.append(defer(_delete_shaper, cfg, nl_shaper,
883                              {'scope': 'node', 'id': node_id}))
884        cleanups.append(defer(_delete_shaper, cfg, nl_shaper,
885                              {'scope': 'queue', 'id': queue_id}))
886        max_depth += 1
887        shaper = nl_shaper.get({'ifindex': cfg.ifindex,
888                                'handle': {'scope': 'node', 'id': node_id}})
889        ksft_eq(shaper, {'ifindex': cfg.ifindex,
890                         'handle': {'scope': 'node', 'id': node_id},
891                         'parent': {'scope': 'node', 'id': parent_id},
892                         'metric': 'bps',
893                         'bw-max': bw_max})
894        shaper = nl_shaper.get({'ifindex': cfg.ifindex,
895                                'handle': {'scope': 'queue',
896                                           'id': queue_id}})
897        ksft_eq(shaper, {'ifindex': cfg.ifindex,
898                         'parent': {'scope': 'node', 'id': node_id},
899                         'handle': {'scope': 'queue', 'id': queue_id},
900                         'weight': 1})
901        queue_id += 1
902
903    if limit_err:
904        print(f"# max nesting depth supported: {max_depth} (errno {limit_err.error})")
905    else:
906        print(f"# max nesting depth tested: {max_depth}")
907    ksft_true(max_depth >= 2,
908              f"max nesting depth: {max_depth}")
909
910    # Cleanup: exec the deferred deletes in reverse creation order, so each
911    # queue leaf and deeper node is removed before its parent node.
912    for cleanup in reversed(cleanups):
913        cleanup.exec()
914    ksft_eq(len(nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)), 0)
915
916def delete_child_reparent(cfg, nl_shaper) -> None:
917    r"""Deleting a child node reparents its queue leaf to the parent.
918
919        netdev              netdev
920          |                   |
921          N1      del N2      N1
922        / | \     ----->    / | \
923      Q1 Q2 N2            Q1 Q2 Q3
924             |
925            Q3
926    """
927    n1_bw_max = 10000
928    n2_bw_max = 5000
929
930    _require_caps(cfg, nl_shaper, 'node',
931                  ['support-bw-max', 'support-metric-bps', 'support-nesting'],
932                  "device does not support node scope shapers with bw_max, metric bps and nesting")
933    _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'],
934                  "device does not support nested queue scope shapers with weight")
935
936    _require_queues(cfg, 4)
937
938    # Create parent node N1 with Q1, Q2
939    n1_handle = nl_shaper.group({
940                   'ifindex': cfg.ifindex,
941                   'leaves':[{'handle': {'scope': 'queue', 'id': 1},
942                              'weight': 1},
943                             {'handle': {'scope': 'queue', 'id': 2},
944                              'weight': 1}],
945                   'handle': {'scope':'node'},
946                   'metric': 'bps',
947                   'bw-max': n1_bw_max})
948    n1_id = n1_handle['handle']['id']
949    for i in range(1, 3):
950        defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': i})
951
952    # Create child node N2 under N1 with Q3
953    n2_handle = nl_shaper.group({
954                   'ifindex': cfg.ifindex,
955                   'leaves':[{'handle': {'scope': 'queue', 'id': 3},
956                              'weight': 1}],
957                   'handle': {'scope':'node'},
958                   'parent': {'scope': 'node', 'id': n1_id},
959                   'metric': 'bps',
960                   'bw-max': n2_bw_max})
961    n2_id = n2_handle['handle']['id']
962    defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 3})
963
964    # Delete child N2 - Q3 should reparent to N1
965    nl_shaper.delete({'ifindex': cfg.ifindex,
966                      'handle': {'scope': 'node', 'id': n2_id}})
967
968    with ksft_raises(NlError):
969        nl_shaper.get({'ifindex': cfg.ifindex,
970                       'handle': {'scope': 'node', 'id': n2_id}})
971
972    shaper_n1 = nl_shaper.get({'ifindex': cfg.ifindex,
973                               'handle': {'scope': 'node', 'id': n1_id}})
974    ksft_eq(shaper_n1, {'ifindex': cfg.ifindex,
975                        'handle': {'scope': 'node', 'id': n1_id},
976                        'parent': {'scope': 'netdev'},
977                        'metric': 'bps',
978                        'bw-max': n1_bw_max})
979    shaper_q3 = nl_shaper.get({'ifindex': cfg.ifindex,
980                               'handle': {'scope': 'queue', 'id': 3}})
981    ksft_eq(shaper_q3, {'ifindex': cfg.ifindex,
982                        'parent': {'scope': 'node', 'id': n1_id},
983                        'handle': {'scope': 'queue', 'id': 3},
984                        'weight': 1})
985
986    # Cleanup
987    for i in range(1, 4):
988        _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i})
989    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
990    ksft_eq(len(shapers), 0)
991
992def move_queue_between_nodes(cfg, nl_shaper) -> None:
993    r"""Move a queue between nodes by re-grouping the destination node.
994
995        netdev                  netdev
996        /    \    .group N2      /    \
997       N1     N2  {Q1,Q3}       N1     N2
998      /  \    |   ------->       |    /  \
999     Q1  Q2  Q3                 Q2  Q1   Q3
1000    """
1001    n1_bw_max = 10000
1002    n2_bw_max = 20000
1003
1004    _require_caps(cfg, nl_shaper, 'node',
1005                  ['support-bw-max', 'support-metric-bps', 'support-nesting'],
1006                  "device does not support node scope shapers with bw_max, metric bps and nesting")
1007    _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'],
1008                  "device does not support nested queue scope shapers with weight")
1009
1010    _require_queues(cfg, 4)
1011
1012    # Create N1 with Q1, Q2
1013    n1_handle = nl_shaper.group({
1014                   'ifindex': cfg.ifindex,
1015                   'leaves':[{'handle': {'scope': 'queue', 'id': 1},
1016                              'weight': 1},
1017                             {'handle': {'scope': 'queue', 'id': 2},
1018                              'weight': 1}],
1019                   'handle': {'scope':'node'},
1020                   'metric': 'bps',
1021                   'bw-max': n1_bw_max})
1022    n1_id = n1_handle['handle']['id']
1023    for i in range(1, 3):
1024        defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': i})
1025
1026    # Create N2 with Q3
1027    n2_handle = nl_shaper.group({
1028                   'ifindex': cfg.ifindex,
1029                   'leaves':[{'handle': {'scope': 'queue', 'id': 3},
1030                              'weight': 1}],
1031                   'handle': {'scope':'node'},
1032                   'metric': 'bps',
1033                   'bw-max': n2_bw_max})
1034    n2_id = n2_handle['handle']['id']
1035    defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 3})
1036
1037    # Move Q1 from N1 to N2 by re-grouping N2 with Q1, Q3
1038    nl_shaper.group({
1039                   'ifindex': cfg.ifindex,
1040                   'leaves':[{'handle': {'scope': 'queue', 'id': 1},
1041                              'weight': 2},
1042                             {'handle': {'scope': 'queue', 'id': 3},
1043                              'weight': 1}],
1044                   'handle': {'scope':'node', 'id': n2_id},
1045                   'metric': 'bps',
1046                   'bw-max': n2_bw_max})
1047
1048    shaper_n1 = nl_shaper.get({'ifindex': cfg.ifindex,
1049                               'handle': {'scope': 'node', 'id': n1_id}})
1050    ksft_eq(shaper_n1, {'ifindex': cfg.ifindex,
1051                        'handle': {'scope': 'node', 'id': n1_id},
1052                        'parent': {'scope': 'netdev'},
1053                        'metric': 'bps',
1054                        'bw-max': n1_bw_max})
1055    shaper_n2 = nl_shaper.get({'ifindex': cfg.ifindex,
1056                               'handle': {'scope': 'node', 'id': n2_id}})
1057    ksft_eq(shaper_n2, {'ifindex': cfg.ifindex,
1058                        'handle': {'scope': 'node', 'id': n2_id},
1059                        'parent': {'scope': 'netdev'},
1060                        'metric': 'bps',
1061                        'bw-max': n2_bw_max})
1062
1063    # Verify Q1 moved to N2
1064    shaper_q1 = nl_shaper.get({'ifindex': cfg.ifindex,
1065                               'handle': {'scope': 'queue', 'id': 1}})
1066    ksft_eq(shaper_q1, {'ifindex': cfg.ifindex,
1067                        'parent': {'scope': 'node', 'id': n2_id},
1068                        'handle': {'scope': 'queue', 'id': 1},
1069                        'weight': 2})
1070
1071    # Verify Q2 still under N1
1072    shaper_q2 = nl_shaper.get({'ifindex': cfg.ifindex,
1073                               'handle': {'scope': 'queue', 'id': 2}})
1074    ksft_eq(shaper_q2, {'ifindex': cfg.ifindex,
1075                        'parent': {'scope': 'node', 'id': n1_id},
1076                        'handle': {'scope': 'queue', 'id': 2},
1077                        'weight': 1})
1078
1079    # Verify Q3 remained under N2
1080    shaper_q3 = nl_shaper.get({'ifindex': cfg.ifindex,
1081                               'handle': {'scope': 'queue', 'id': 3}})
1082    ksft_eq(shaper_q3, {'ifindex': cfg.ifindex,
1083                        'parent': {'scope': 'node', 'id': n2_id},
1084                        'handle': {'scope': 'queue', 'id': 3},
1085                        'weight': 1})
1086
1087    # Cleanup
1088    for i in range(1, 4):
1089        _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i})
1090    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
1091    ksft_eq(len(shapers), 0)
1092
1093def reject_reparenting(cfg, nl_shaper) -> None:
1094    r"""Reject reparenting an existing node; the hierarchy stays intact.
1095
1096        netdev
1097        /    \      rejected:  N3 -> netdev
1098       N1     N2    rejected:  N1 -> N2
1099      /  \    |     (both EOPNOTSUPP)
1100     Q1  N3  Q2
1101         |
1102         Q3
1103    """
1104    node1_bw_max = 10000
1105    node2_bw_max = 5000
1106    node3_bw_max = 20000
1107
1108    _require_caps(cfg, nl_shaper, 'node',
1109                  ['support-bw-max', 'support-metric-bps', 'support-nesting'],
1110                  "device does not support node scope shapers with bw_max, metric bps and nesting")
1111    _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'],
1112                  "device does not support nested queue scope shapers with weight")
1113
1114    _require_queues(cfg, 4)
1115
1116    # Create Node1 under netdev with Q1.
1117    node1_id = nl_shaper.group({
1118                   'ifindex': cfg.ifindex,
1119                   'leaves':[{'handle': {'scope': 'queue', 'id': 1},
1120                              'weight': 1}],
1121                   'handle': {'scope':'node'},
1122                   'metric': 'bps',
1123                   'bw-max': node1_bw_max})['handle']['id']
1124    defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 1})
1125    defer(_delete_shaper, cfg, nl_shaper, {'scope': 'node', 'id': node1_id})
1126
1127    # Create Node2 under netdev with Q2.
1128    node2_id = nl_shaper.group({
1129                   'ifindex': cfg.ifindex,
1130                   'leaves':[{'handle': {'scope': 'queue', 'id': 2},
1131                              'weight': 1}],
1132                   'handle': {'scope':'node'},
1133                   'metric': 'bps',
1134                   'bw-max': node2_bw_max})['handle']['id']
1135    defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 2})
1136    defer(_delete_shaper, cfg, nl_shaper, {'scope': 'node', 'id': node2_id})
1137
1138    # Create Node3 nested under Node1 with Q3.
1139    node3_id = nl_shaper.group({
1140                   'ifindex': cfg.ifindex,
1141                   'leaves':[{'handle': {'scope': 'queue', 'id': 3},
1142                              'weight': 1}],
1143                   'handle': {'scope':'node'},
1144                   'metric': 'bps',
1145                   'bw-max': node3_bw_max,
1146                   'parent': {'scope': 'node', 'id': node1_id}})['handle']['id']
1147    defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 3})
1148    defer(_delete_shaper, cfg, nl_shaper, {'scope': 'node', 'id': node3_id})
1149
1150    # Reparenting a nested node up to netdev must fail.
1151    with ksft_raises(NlError) as cm:
1152        nl_shaper.group({
1153                   'ifindex': cfg.ifindex,
1154                   'leaves':[{'handle': {'scope': 'queue', 'id': 3},
1155                              'weight': 1}],
1156                   'handle': {'scope':'node', 'id': node3_id},
1157                   'parent': {'scope': 'netdev'}})
1158    if cm.exception:
1159        ksft_eq(cm.exception.error, errno.EOPNOTSUPP)
1160
1161    # Reparenting a node under another node must fail as well.
1162    with ksft_raises(NlError) as cm:
1163        nl_shaper.group({
1164                   'ifindex': cfg.ifindex,
1165                   'leaves':[{'handle': {'scope': 'queue', 'id': 1},
1166                              'weight': 1}],
1167                   'handle': {'scope':'node', 'id': node1_id},
1168                   'parent': {'scope': 'node', 'id': node2_id}})
1169    if cm.exception:
1170        ksft_eq(cm.exception.error, errno.EOPNOTSUPP)
1171
1172    # Updating a node with the same parent must succeed.
1173    nl_shaper.group({
1174                   'ifindex': cfg.ifindex,
1175                   'leaves':[{'handle': {'scope': 'queue', 'id': 1},
1176                              'weight': 5}],
1177                   'handle': {'scope':'node', 'id': node1_id},
1178                   'parent': {'scope': 'netdev'}})
1179
1180    # Updating a node without specifying the parent must succeed.
1181    nl_shaper.group({
1182                   'ifindex': cfg.ifindex,
1183                   'leaves':[{'handle': {'scope': 'queue', 'id': 2},
1184                              'weight': 7}],
1185                   'handle': {'scope':'node', 'id': node2_id}})
1186
1187    # The rejected reparents must have left the hierarchy intact.
1188    shaper = nl_shaper.get({'ifindex': cfg.ifindex,
1189                            'handle': {'scope': 'node', 'id': node1_id}})
1190    ksft_eq(shaper, {'ifindex': cfg.ifindex,
1191                     'handle': {'scope': 'node', 'id': node1_id},
1192                     'parent': {'scope': 'netdev'},
1193                     'metric': 'bps',
1194                     'bw-max': node1_bw_max})
1195    shaper = nl_shaper.get({'ifindex': cfg.ifindex,
1196                            'handle': {'scope': 'node', 'id': node2_id}})
1197    ksft_eq(shaper, {'ifindex': cfg.ifindex,
1198                     'handle': {'scope': 'node', 'id': node2_id},
1199                     'parent': {'scope': 'netdev'},
1200                     'metric': 'bps',
1201                     'bw-max': node2_bw_max})
1202    shaper = nl_shaper.get({'ifindex': cfg.ifindex,
1203                            'handle': {'scope': 'node', 'id': node3_id}})
1204    ksft_eq(shaper, {'ifindex': cfg.ifindex,
1205                     'handle': {'scope': 'node', 'id': node3_id},
1206                     'parent': {'scope': 'node', 'id': node1_id},
1207                     'metric': 'bps',
1208                     'bw-max': node3_bw_max})
1209
1210    # Verify the leaf weights were updated and parents unchanged.
1211    shaper = nl_shaper.get({'ifindex': cfg.ifindex,
1212                            'handle': {'scope': 'queue', 'id': 1}})
1213    ksft_eq(shaper, {'ifindex': cfg.ifindex,
1214                     'parent': {'scope': 'node', 'id': node1_id},
1215                     'handle': {'scope': 'queue', 'id': 1},
1216                     'weight': 5})
1217    shaper = nl_shaper.get({'ifindex': cfg.ifindex,
1218                            'handle': {'scope': 'queue', 'id': 2}})
1219    ksft_eq(shaper, {'ifindex': cfg.ifindex,
1220                     'parent': {'scope': 'node', 'id': node2_id},
1221                     'handle': {'scope': 'queue', 'id': 2},
1222                     'weight': 7})
1223    shaper = nl_shaper.get({'ifindex': cfg.ifindex,
1224                            'handle': {'scope': 'queue', 'id': 3}})
1225    ksft_eq(shaper, {'ifindex': cfg.ifindex,
1226                     'parent': {'scope': 'node', 'id': node3_id},
1227                     'handle': {'scope': 'queue', 'id': 3},
1228                     'weight': 1})
1229
1230    # Cleanup. Delete the nodes explicitly instead of relying on the
1231    # empty-node auto-delete: a kernel that wrongly accepts a reparent may
1232    # mishandle the leaf accounting and leave a node behind. Removing them
1233    # by handle keeps a failing run from leaking state into later tests.
1234    for i in range(1, 4):
1235        _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i})
1236    for nid in (node1_id, node2_id, node3_id):
1237        _delete_shaper(cfg, nl_shaper, {'scope': 'node', 'id': nid})
1238    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
1239    ksft_eq(len(shapers), 0)
1240
1241def queue_update(cfg, nl_shaper) -> None:
1242    nq = _require_queues(cfg, 4)
1243    if not cfg.queues:
1244        raise KsftSkipEx("device does not support queue scope")
1245
1246    netnl = EthtoolFamily()
1247    channels = netnl.channels_get({'header': {'dev-index': cfg.ifindex}})
1248    ch_type = 'combined' if channels['combined-count'] else 'tx'
1249
1250    for i in range(3):
1251        nl_shaper.set({'ifindex': cfg.ifindex,
1252                       'handle': {'scope': 'queue', 'id': i},
1253                       'metric': 'bps',
1254                       'bw-max': (i + 1) * 1000})
1255    defer(cmd, f"ethtool -L {cfg.dev['ifname']} {ch_type} {nq}")
1256
1257    # Delete a channel, with no shapers configured on top of the related
1258    # queue: no changes expected
1259    cmd(f"ethtool -L {cfg.dev['ifname']} {ch_type} 3")
1260    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
1261    ksft_eq(shapers, [{'ifindex': cfg.ifindex,
1262                       'parent': {'scope': 'netdev'},
1263                       'handle': {'scope': 'queue', 'id': 0},
1264                       'metric': 'bps',
1265                       'bw-max': 1000},
1266                      {'ifindex': cfg.ifindex,
1267                       'parent': {'scope': 'netdev'},
1268                       'handle': {'scope': 'queue', 'id': 1},
1269                       'metric': 'bps',
1270                       'bw-max': 2000},
1271                      {'ifindex': cfg.ifindex,
1272                       'parent': {'scope': 'netdev'},
1273                       'handle': {'scope': 'queue', 'id': 2},
1274                       'metric': 'bps',
1275                       'bw-max': 3000}])
1276
1277    # Delete a channel, with a shaper configured on top of the related
1278    # queue: the shaper must be deleted, too
1279    cmd(f"ethtool -L {cfg.dev['ifname']} {ch_type} 2")
1280
1281    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
1282    ksft_eq(shapers, [{'ifindex': cfg.ifindex,
1283                       'parent': {'scope': 'netdev'},
1284                       'handle': {'scope': 'queue', 'id': 0},
1285                       'metric': 'bps',
1286                       'bw-max': 1000},
1287                      {'ifindex': cfg.ifindex,
1288                       'parent': {'scope': 'netdev'},
1289                       'handle': {'scope': 'queue', 'id': 1},
1290                       'metric': 'bps',
1291                       'bw-max': 2000}])
1292
1293    # Restore the original channels number, no expected changes
1294    cmd(f"ethtool -L {cfg.dev['ifname']} {ch_type} {nq}")
1295    shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)
1296    ksft_eq(shapers, [{'ifindex': cfg.ifindex,
1297                       'parent': {'scope': 'netdev'},
1298                       'handle': {'scope': 'queue', 'id': 0},
1299                       'metric': 'bps',
1300                       'bw-max': 1000},
1301                      {'ifindex': cfg.ifindex,
1302                       'parent': {'scope': 'netdev'},
1303                       'handle': {'scope': 'queue', 'id': 1},
1304                       'metric': 'bps',
1305                       'bw-max': 2000}])
1306
1307    # Final cleanup.
1308    for i in range(0, 2):
1309        nl_shaper.delete({'ifindex': cfg.ifindex,
1310                          'handle': {'scope': 'queue', 'id': i}})
1311
1312def dup_leaves(cfg, nl_shaper) -> None:
1313    """ Ensure that the kernel rejects duplicate leaves. """
1314    _require_caps(cfg, nl_shaper, 'node', ['support-bw-max', 'support-metric-bps'],
1315                  "device does not support node scope shapers with bw_max and metric bps")
1316    _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'],
1317                  "device does not support nested queue scope shapers with weight")
1318
1319    node_handle = None
1320    with ksft_raises(NlError) as cm:
1321        node_handle = nl_shaper.group({
1322                   'ifindex': cfg.ifindex,
1323                   'leaves':[{'handle': {'scope': 'queue', 'id': 0},
1324                              'weight': 1},
1325                             {'handle': {'scope': 'queue', 'id': 0},
1326                              'weight': 2}],
1327                   'handle': {'scope':'node'},
1328                   'metric': 'bps',
1329                   'bw-max': 10000})
1330
1331    # Clean up in case the kernel wrongly accepted the request.
1332    if node_handle:
1333        _delete_shaper(cfg, nl_shaper, node_handle['handle'])
1334    _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': 0})
1335
1336    # ksft_raises() has already recorded the failure if nothing was raised.
1337    if cm.exception is None:
1338        return
1339    ksft_eq(cm.exception.error, errno.EINVAL)
1340
1341def main() -> None:
1342    with NetDrvEnv(__file__, queue_count=4) as cfg:
1343        cfg.queues = False
1344        cfg.netdev = False
1345        ksft_run([get_shapers,
1346                  get_caps,
1347                  set_qshapers,
1348                  del_qshapers,
1349                  set_nshapers,
1350                  del_nshapers,
1351                  set_all_supported_attrs,
1352                  invalid_set_preserves_state,
1353                  mixed_parent_group_requires_parent,
1354                  recursive_empty_node_cleanup,
1355                  basic_groups,
1356                  basic_groups_with_rate,
1357                  qgroups,
1358                  set_node_shaper,
1359                  group_update_rate,
1360                  delegation,
1361                  nested_depth_limit,
1362                  delete_child_reparent,
1363                  move_queue_between_nodes,
1364                  reject_reparenting,
1365                  dup_leaves,
1366                  queue_update],
1367                 args=(cfg, NetshaperFamily()))
1368    ksft_exit()
1369
1370
1371if __name__ == "__main__":
1372    main()
1373