| #!/usr/bin/env python3 | |
| # SPDX-License-Identifier: GPL-2.0 | |
| # pylint: disable=too-many-lines | |
| import errno | |
| import glob | |
| from lib.py import ksft_run, ksft_exit | |
| from lib.py import ksft_eq, ksft_true, ksft_raises, KsftSkipEx | |
| from lib.py import EthtoolFamily, NetshaperFamily | |
| from lib.py import NetDrvEnv | |
| from lib.py import NlError | |
| from lib.py import cmd, defer | |
| def _delete_shaper(cfg, nl_shaper, handle) -> None: | |
| """ Delete the shaper identified by handle, ignoring a missing-shaper error. """ | |
| try: | |
| nl_shaper.delete({'ifindex': cfg.ifindex, | |
| 'handle': handle}) | |
| except NlError as e: | |
| if e.error != errno.ENOENT: | |
| raise | |
| def _require_queues(cfg, count): | |
| """ Return the netdev TX queue count, skipping the test if fewer than count exist. """ | |
| qcnt = len(glob.glob(f"/sys/class/net/{cfg.ifname}/queues/tx-*")) | |
| if qcnt < count: | |
| raise KsftSkipEx(f"netdev has {qcnt} queues, {count} required") | |
| return qcnt | |
| def _cap_get(cfg, nl_shaper, scope): | |
| """ Return the shaper capabilities for the given scope, caching them on cfg. """ | |
| if not hasattr(cfg, 'cap_cache'): | |
| cfg.cap_cache = {} | |
| if scope not in cfg.cap_cache: | |
| cfg.cap_cache[scope] = nl_shaper.cap_get({'ifindex': cfg.ifindex, | |
| 'scope': scope}) | |
| return cfg.cap_cache[scope] | |
| def _require_caps(cfg, nl_shaper, scope, caps, msg) -> None: | |
| """ Skip the test unless the given scope advertises all the required caps. """ | |
| try: | |
| supported = _cap_get(cfg, nl_shaper, scope) | |
| except NlError as e: | |
| if e.error == errno.EOPNOTSUPP: | |
| raise KsftSkipEx(f"{scope} scope shapers not supported by the device") | |
| raise | |
| if not set(caps).issubset(supported): | |
| raise KsftSkipEx(msg) | |
| def get_shapers(cfg, nl_shaper) -> None: | |
| try: | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| except NlError as e: | |
| if e.error == 95: | |
| raise KsftSkipEx("shapers not supported by the device") | |
| raise | |
| # Default configuration: no shapers configured. | |
| ksft_eq(len(shapers), 0) | |
| def get_caps(cfg, nl_shaper) -> None: | |
| try: | |
| caps = nl_shaper.cap_get({'ifindex': cfg.ifindex}, dump=True) | |
| except NlError as e: | |
| if e.error == 95: | |
| raise KsftSkipEx("shapers not supported by the device") | |
| raise | |
| # Each device implementing shaper support must support some | |
| # features in at least a scope. | |
| ksft_true(len(caps)> 0) | |
| def set_qshapers(cfg, nl_shaper) -> None: | |
| try: | |
| caps = nl_shaper.cap_get({'ifindex': cfg.ifindex, | |
| 'scope':'queue'}) | |
| except NlError as e: | |
| if e.error == 95: | |
| raise KsftSkipEx("shapers not supported by the device") | |
| raise | |
| if not 'support-bw-max' in caps or not 'support-metric-bps' in caps: | |
| raise KsftSkipEx("device does not support queue scope shapers with bw_max and metric bps") | |
| _require_queues(cfg, 3) | |
| cfg.queues = True | |
| nl_shaper.set({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 1}, | |
| 'metric': 'bps', | |
| 'bw-max': 10000}) | |
| nl_shaper.set({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 2}, | |
| 'metric': 'bps', | |
| 'bw-max': 20000}) | |
| # Querying a specific shaper not yet configured must fail. | |
| raised = False | |
| try: | |
| shaper_q0 = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 0}}) | |
| except (NlError): | |
| raised = True | |
| ksft_eq(raised, True) | |
| shaper_q1 = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 1}}) | |
| ksft_eq(shaper_q1, {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'netdev'}, | |
| 'handle': {'scope': 'queue', 'id': 1}, | |
| 'metric': 'bps', | |
| 'bw-max': 10000}) | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(shapers, [{'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'netdev'}, | |
| 'handle': {'scope': 'queue', 'id': 1}, | |
| 'metric': 'bps', | |
| 'bw-max': 10000}, | |
| {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'netdev'}, | |
| 'handle': {'scope': 'queue', 'id': 2}, | |
| 'metric': 'bps', | |
| 'bw-max': 20000}]) | |
| def del_qshapers(cfg, nl_shaper) -> None: | |
| if not cfg.queues: | |
| raise KsftSkipEx("queue shapers not supported by device, skipping delete") | |
| nl_shaper.delete({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 2}}) | |
| nl_shaper.delete({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 1}}) | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(len(shapers), 0) | |
| def set_nshapers(cfg, nl_shaper) -> None: | |
| # Check required features. | |
| try: | |
| caps = nl_shaper.cap_get({'ifindex': cfg.ifindex, | |
| 'scope':'netdev'}) | |
| except NlError as e: | |
| if e.error == 95: | |
| raise KsftSkipEx("shapers not supported by the device") | |
| raise | |
| if not 'support-bw-max' in caps or not 'support-metric-bps' in caps: | |
| raise KsftSkipEx("device does not support nested netdev scope shapers with weight") | |
| cfg.netdev = True; | |
| nl_shaper.set({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'netdev', 'id': 0}, | |
| 'bw-max': 100000}) | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(shapers, [{'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'netdev'}, | |
| 'metric': 'bps', | |
| 'bw-max': 100000}]) | |
| def del_nshapers(cfg, nl_shaper) -> None: | |
| if not cfg.netdev: | |
| raise KsftSkipEx("netdev shaper not supported by device, skipping delete") | |
| nl_shaper.delete({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'netdev'}}) | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(len(shapers), 0) | |
| def set_all_supported_attrs(cfg, nl_shaper) -> None: | |
| """ Set every queue-scope attribute the device advertises and verify the read-back. """ | |
| _require_queues(cfg, 1) | |
| _require_caps(cfg, nl_shaper, 'queue', [], | |
| "queue scope shapers not supported by the device") | |
| caps = _cap_get(cfg, nl_shaper, 'queue') | |
| attrs = {'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 0}} | |
| expected = {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'netdev'}, | |
| 'handle': {'scope': 'queue', 'id': 0}} | |
| rate_attrs = {'support-bw-min': ('bw-min', 10000, 100), | |
| 'support-bw-max': ('bw-max', 20000, 200), | |
| 'support-burst': ('burst', 3000, 30)} | |
| rate_attr_supported = any(cap in caps for cap in rate_attrs) | |
| bps_supported = 'support-metric-bps' in caps | |
| pps_supported = 'support-metric-pps' in caps | |
| def add_rate_attrs(metric, value_idx) -> None: | |
| attrs['metric'] = metric | |
| expected['metric'] = metric | |
| for cap, (attr, bps_value, pps_value) in rate_attrs.items(): | |
| if cap not in caps: | |
| continue | |
| value = bps_value if value_idx == 0 else pps_value | |
| attrs[attr] = value | |
| expected[attr] = value | |
| if rate_attr_supported: | |
| if bps_supported: | |
| add_rate_attrs('bps', 0) | |
| elif pps_supported: | |
| add_rate_attrs('pps', 1) | |
| if 'support-priority' in caps: | |
| attrs['priority'] = 1 | |
| expected['priority'] = 1 | |
| if 'support-weight' in caps: | |
| attrs['weight'] = 2 | |
| expected['weight'] = 2 | |
| if len(attrs) == 2: | |
| raise KsftSkipEx("device does not advertise any supported queue shaper attributes") | |
| nl_shaper.set(attrs) | |
| defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 0}) | |
| shaper = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 0}}) | |
| ksft_eq(shaper, expected) | |
| if rate_attr_supported and bps_supported and pps_supported: | |
| add_rate_attrs('pps', 1) | |
| nl_shaper.set(attrs) | |
| shaper = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 0}}) | |
| ksft_eq(shaper, expected) | |
| _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': 0}) | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(len(shapers), 0) | |
| def invalid_set_preserves_state(cfg, nl_shaper) -> None: | |
| """ Verify a rejected .set leaves the existing shaper configuration unchanged. """ | |
| nq = _require_queues(cfg, 1) | |
| _require_caps(cfg, nl_shaper, 'queue', | |
| ['support-bw-max', 'support-metric-bps'], | |
| "device does not support queue scope bw_max with bps metric") | |
| initial = {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'netdev'}, | |
| 'handle': {'scope': 'queue', 'id': 0}, | |
| 'metric': 'bps', | |
| 'bw-max': 10000} | |
| nl_shaper.set({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 0}, | |
| 'metric': 'bps', | |
| 'bw-max': 10000}) | |
| defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 0}) | |
| with ksft_raises(NlError): | |
| nl_shaper.set({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': 0}, | |
| 'metric': 'bps', | |
| 'bw-max': 20000}) | |
| shaper = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 0}}) | |
| ksft_eq(shaper, initial) | |
| with ksft_raises(NlError): | |
| nl_shaper.set({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': nq}, | |
| 'metric': 'bps', | |
| 'bw-max': 20000}) | |
| shaper = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 0}}) | |
| ksft_eq(shaper, initial) | |
| _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': 0}) | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(len(shapers), 0) | |
| def mixed_parent_group_requires_parent(cfg, nl_shaper) -> None: | |
| r"""Grouping leaves from different nodes requires an explicit parent. | |
| netdev netdev | |
| / \ parent=netdev | |
| N1 N2 group N | |
| | | {Q0,Q1} / \ | |
| Q0 Q1 -------> Q0 Q1 | |
| Without an explicit parent the group is rejected; parent=netdev | |
| collapses the leaves into one new node. | |
| """ | |
| _require_queues(cfg, 2) | |
| _require_caps(cfg, nl_shaper, 'node', | |
| ['support-bw-max', 'support-metric-bps'], | |
| "device does not support node scope shapers with bw_max and metric bps") | |
| _require_caps(cfg, nl_shaper, 'queue', | |
| ['support-nesting', 'support-weight'], | |
| "device does not support nested queue scope shapers with weight") | |
| n1_handle = nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, | |
| 'weight': 1}], | |
| 'handle': {'scope':'node'}, | |
| 'metric': 'bps', | |
| 'bw-max': 10000}) | |
| n1_id = n1_handle['handle']['id'] | |
| defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 0}) | |
| n2_handle = nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 2}], | |
| 'handle': {'scope':'node'}, | |
| 'metric': 'bps', | |
| 'bw-max': 20000}) | |
| n2_id = n2_handle['handle']['id'] | |
| defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 1}) | |
| with ksft_raises(NlError): | |
| nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, | |
| 'weight': 3}, | |
| {'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 4}], | |
| 'handle': {'scope':'node'}, | |
| 'metric': 'bps', | |
| 'bw-max': 30000}) | |
| shaper_q0 = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 0}}) | |
| ksft_eq(shaper_q0, {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': n1_id}, | |
| 'handle': {'scope': 'queue', 'id': 0}, | |
| 'weight': 1}) | |
| shaper_q1 = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 1}}) | |
| ksft_eq(shaper_q1, {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': n2_id}, | |
| 'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 2}) | |
| node_handle = nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, | |
| 'weight': 3}, | |
| {'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 4}], | |
| 'handle': {'scope':'node'}, | |
| 'parent': {'scope': 'netdev'}, | |
| 'metric': 'bps', | |
| 'bw-max': 30000}) | |
| node_id = node_handle['handle']['id'] | |
| for old_id in (n1_id, n2_id): | |
| with ksft_raises(NlError): | |
| nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': old_id}}) | |
| shaper_q0 = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 0}}) | |
| ksft_eq(shaper_q0, {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': node_id}, | |
| 'handle': {'scope': 'queue', 'id': 0}, | |
| 'weight': 3}) | |
| shaper_q1 = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 1}}) | |
| ksft_eq(shaper_q1, {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': node_id}, | |
| 'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 4}) | |
| for i in range(2): | |
| _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i}) | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(len(shapers), 0) | |
| def recursive_empty_node_cleanup(cfg, nl_shaper) -> None: | |
| r"""Deleting the last leaf recursively removes the emptied ancestors. | |
| netdev netdev | |
| | del Q0 | |
| N1 ------> (N1 and N2 removed too) | |
| | | |
| N2 | |
| | | |
| Q0 | |
| """ | |
| _require_queues(cfg, 1) | |
| _require_caps(cfg, nl_shaper, 'node', | |
| ['support-bw-max', 'support-metric-bps', 'support-nesting'], | |
| "device does not support nested node scope shapers") | |
| _require_caps(cfg, nl_shaper, 'queue', | |
| ['support-nesting', 'support-weight'], | |
| "device does not support nested queue scope shapers with weight") | |
| n1_handle = nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, | |
| 'weight': 1}], | |
| 'handle': {'scope':'node'}, | |
| 'metric': 'bps', | |
| 'bw-max': 10000}) | |
| n1_id = n1_handle['handle']['id'] | |
| defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 0}) | |
| n2_handle = nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, | |
| 'weight': 1}], | |
| 'handle': {'scope':'node'}, | |
| 'parent': {'scope': 'node', 'id': n1_id}, | |
| 'metric': 'bps', | |
| 'bw-max': 5000}) | |
| n2_id = n2_handle['handle']['id'] | |
| shaper_q0 = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 0}}) | |
| ksft_eq(shaper_q0, {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': n2_id}, | |
| 'handle': {'scope': 'queue', 'id': 0}, | |
| 'weight': 1}) | |
| nl_shaper.delete({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 0}}) | |
| for handle in ({'scope': 'queue', 'id': 0}, | |
| {'scope': 'node', 'id': n2_id}, | |
| {'scope': 'node', 'id': n1_id}): | |
| with ksft_raises(NlError): | |
| nl_shaper.get({'ifindex': cfg.ifindex, 'handle': handle}) | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(len(shapers), 0) | |
| def _group_under_netdev(cfg, nl_shaper, bw_max=None): | |
| r"""Group queues under a netdev-scope node; caller owns node teardown. | |
| netdev netdev | |
| / \ del Q1,Q2 | |
| Q1 Q2 -------> (netdev node persists) | |
| """ | |
| group_args = { | |
| 'ifindex': cfg.ifindex, | |
| 'leaves': [{'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 1}, | |
| {'handle': {'scope': 'queue', 'id': 2}, | |
| 'weight': 2}], | |
| 'handle': {'scope': 'netdev'}} | |
| if bw_max: | |
| group_args['metric'] = 'bps' | |
| group_args['bw-max'] = bw_max | |
| node_handle = nl_shaper.group(group_args) | |
| ksft_eq(node_handle, {'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'netdev'}}) | |
| del_node = defer(_delete_shaper, cfg, nl_shaper, {'scope': 'netdev'}) | |
| del_queues = [defer(_delete_shaper, cfg, nl_shaper, | |
| {'scope': 'queue', 'id': qid}) | |
| for qid in (1, 2)] | |
| shaper = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 1}}) | |
| ksft_eq(shaper, {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'netdev'}, | |
| 'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 1}) | |
| for dq in del_queues: | |
| dq.exec() | |
| # Caller owns the node teardown so it can verify the netdev-scope node | |
| # survives leaf deletion before removing it. | |
| return del_node | |
| def basic_groups(cfg, nl_shaper) -> None: | |
| r"""Group queues under a netdev-scope node, then tear it down. | |
| netdev | |
| / \ | |
| Q1 Q2 | |
| """ | |
| _require_queues(cfg, 3) | |
| _require_caps(cfg, nl_shaper, 'netdev', [], "netdev scope not supported by the device") | |
| _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], | |
| "queue scope not supported with nesting and weight") | |
| del_node = _group_under_netdev(cfg, nl_shaper) | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(shapers, [{'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'netdev'}}]) | |
| del_node.exec() | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(len(shapers), 0) | |
| def basic_groups_with_rate(cfg, nl_shaper) -> None: | |
| r"""Rate-limited netdev-scope node outlives deletion of its leaves. | |
| netdev[10kbps] netdev[10kbps] | |
| / \ del Q1,Q2 | |
| Q1 Q2 -------> (node persists) | |
| """ | |
| bw_max = 10000 | |
| _require_queues(cfg, 3) | |
| _require_caps(cfg, nl_shaper, 'netdev', ['support-bw-max', 'support-metric-bps'], | |
| "device does not support netdev scope rate limiting") | |
| _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], | |
| "device does not support queue scope shapers with nesting and weight") | |
| del_node = _group_under_netdev(cfg, nl_shaper, bw_max=bw_max) | |
| # Deleting all the leaves shaper does not affect the node one | |
| # when the latter has 'netdev' scope. | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(shapers, [{'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'netdev'}, | |
| 'metric': 'bps', | |
| 'bw-max': bw_max}]) | |
| del_node.exec() | |
| def qgroups(cfg, nl_shaper) -> None: | |
| _require_queues(cfg, 4) | |
| _require_caps(cfg, nl_shaper, 'node', | |
| ['support-bw-max', 'support-metric-bps'], | |
| "device does not support node scope shapers with bw_max and metric bps") | |
| _require_caps(cfg, nl_shaper, 'queue', | |
| ['support-nesting', 'support-weight'], | |
| "device does not support nested queue scope shapers with weight") | |
| node_handle = nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 3}, | |
| {'handle': {'scope': 'queue', 'id': 2}, | |
| 'weight': 2}], | |
| 'handle': {'scope':'node'}, | |
| 'metric': 'bps', | |
| 'bw-max': 10000}) | |
| node_id = node_handle['handle']['id'] | |
| shaper = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 1}}) | |
| ksft_eq(shaper, {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': node_id}, | |
| 'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 3}) | |
| shaper = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': node_id}}) | |
| ksft_eq(shaper, {'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': node_id}, | |
| 'parent': {'scope': 'netdev'}, | |
| 'metric': 'bps', | |
| 'bw-max': 10000}) | |
| # Grouping to a specified, not existing node scope shaper must fail | |
| raised = False | |
| try: | |
| nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 3}, | |
| 'weight': 3}], | |
| 'handle': {'scope':'node', 'id': node_id + 1}, | |
| 'metric': 'bps', | |
| 'bw-max': 10000}) | |
| except (NlError): | |
| raised = True | |
| ksft_eq(raised, True) | |
| # Add to an existing node | |
| node_handle = nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 3}, | |
| 'weight': 4}], | |
| 'handle': {'scope':'node', 'id': node_id}}) | |
| ksft_eq(node_handle, {'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': node_id}}) | |
| shaper = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 3}}) | |
| ksft_eq(shaper, {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': node_id}, | |
| 'handle': {'scope': 'queue', 'id': 3}, | |
| 'weight': 4}) | |
| nl_shaper.delete({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 2}}) | |
| nl_shaper.delete({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 1}}) | |
| # Deleting a non empty node will move the leaves downstream. | |
| nl_shaper.delete({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': node_id}}) | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(shapers, [{'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'netdev'}, | |
| 'handle': {'scope': 'queue', 'id': 3}, | |
| 'weight': 4}]) | |
| # Finish and verify the complete cleanup. | |
| nl_shaper.delete({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 3}}) | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(len(shapers), 0) | |
| def set_node_shaper(cfg, nl_shaper) -> None: | |
| """ Verify a node-scope shaper rate can be updated via .set. """ | |
| _require_queues(cfg, 2) | |
| _require_caps(cfg, nl_shaper, 'node', ['support-bw-max', 'support-metric-bps'], | |
| "device does not support node scope shapers with bw_max and metric bps") | |
| _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], | |
| "device does not support nested queue scope shapers with weight") | |
| node_handle = nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 1}], | |
| 'handle': {'scope':'node'}, | |
| 'metric': 'bps', | |
| 'bw-max': 10000}) | |
| node_id = node_handle['handle']['id'] | |
| defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 1}) | |
| # Update the node's rate via .set | |
| nl_shaper.set({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': node_id}, | |
| 'metric': 'bps', | |
| 'bw-max': 20000}) | |
| shaper = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': node_id}}) | |
| ksft_eq(shaper, {'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': node_id}, | |
| 'parent': {'scope': 'netdev'}, | |
| 'metric': 'bps', | |
| 'bw-max': 20000}) | |
| # Cleanup | |
| _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': 1}) | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(len(shapers), 0) | |
| def group_update_rate(cfg, nl_shaper) -> None: | |
| """ Verify re-grouping a node updates its rate while leaving the leaves untouched. """ | |
| _require_queues(cfg, 3) | |
| _require_caps(cfg, nl_shaper, 'node', ['support-bw-max', 'support-metric-bps'], | |
| "device does not support node scope shapers with bw_max and metric bps") | |
| _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], | |
| "device does not support nested queue scope shapers with weight") | |
| # Create node with Q1, Q2 at bw_max=10000 | |
| node_handle = nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 1}, | |
| {'handle': {'scope': 'queue', 'id': 2}, | |
| 'weight': 1}], | |
| 'handle': {'scope':'node'}, | |
| 'metric': 'bps', | |
| 'bw-max': 10000}) | |
| node_id = node_handle['handle']['id'] | |
| for i in range(1, 3): | |
| defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': i}) | |
| # Update rate via .group on the same node | |
| nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 1}, | |
| {'handle': {'scope': 'queue', 'id': 2}, | |
| 'weight': 1}], | |
| 'handle': {'scope':'node', 'id': node_id}, | |
| 'metric': 'bps', | |
| 'bw-max': 50000}) | |
| # Verify rate updated | |
| shaper = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': node_id}}) | |
| ksft_eq(shaper, {'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': node_id}, | |
| 'parent': {'scope': 'netdev'}, | |
| 'metric': 'bps', | |
| 'bw-max': 50000}) | |
| # Verify leaves unchanged | |
| shaper_q1 = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 1}}) | |
| ksft_eq(shaper_q1, {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': node_id}, | |
| 'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 1}) | |
| shaper_q2 = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 2}}) | |
| ksft_eq(shaper_q2, {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': node_id}, | |
| 'handle': {'scope': 'queue', 'id': 2}, | |
| 'weight': 1}) | |
| # Make sure we only have 3 shapers including 2 queues and the node | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(len(shapers), 3) | |
| # Cleanup | |
| for i in range(1, 3): | |
| _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i}) | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(len(shapers), 0) | |
| def delegation(cfg, nl_shaper) -> None: | |
| _require_queues(cfg, 4) | |
| _require_caps(cfg, nl_shaper, 'node', | |
| ['support-bw-max', 'support-metric-bps', 'support-nesting'], | |
| "device does not support node scope shapers with bw_max, metric bps and nesting") | |
| _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], | |
| "device does not support nested queue scope shapers with weight") | |
| node_handle = nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 3}, | |
| {'handle': {'scope': 'queue', 'id': 2}, | |
| 'weight': 2}, | |
| {'handle': {'scope': 'queue', 'id': 3}, | |
| 'weight': 1}], | |
| 'handle': {'scope':'node'}, | |
| 'metric': 'bps', | |
| 'bw-max': 10000}) | |
| node_id = node_handle['handle']['id'] | |
| # Create the nested node and validate the hierarchy | |
| nested_node_handle = nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 3}, | |
| {'handle': {'scope': 'queue', 'id': 2}, | |
| 'weight': 2}], | |
| 'handle': {'scope':'node'}, | |
| 'metric': 'bps', | |
| 'bw-max': 5000}) | |
| nested_node_id = nested_node_handle['handle']['id'] | |
| ksft_true(nested_node_id != node_id) | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(shapers, [{'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': nested_node_id}, | |
| 'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 3}, | |
| {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': nested_node_id}, | |
| 'handle': {'scope': 'queue', 'id': 2}, | |
| 'weight': 2}, | |
| {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': node_id}, | |
| 'handle': {'scope': 'queue', 'id': 3}, | |
| 'weight': 1}, | |
| {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'netdev'}, | |
| 'handle': {'scope': 'node', 'id': node_id}, | |
| 'metric': 'bps', | |
| 'bw-max': 10000}, | |
| {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': node_id}, | |
| 'handle': {'scope': 'node', 'id': nested_node_id}, | |
| 'metric': 'bps', | |
| 'bw-max': 5000}]) | |
| # Deleting a non empty node will move the leaves downstream. | |
| nl_shaper.delete({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': nested_node_id}}) | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(shapers, [{'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': node_id}, | |
| 'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 3}, | |
| {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': node_id}, | |
| 'handle': {'scope': 'queue', 'id': 2}, | |
| 'weight': 2}, | |
| {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': node_id}, | |
| 'handle': {'scope': 'queue', 'id': 3}, | |
| 'weight': 1}, | |
| {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'netdev'}, | |
| 'handle': {'scope': 'node', 'id': node_id}, | |
| 'metric': 'bps', | |
| 'bw-max': 10000}]) | |
| # Final cleanup. | |
| for i in range(1, 4): | |
| nl_shaper.delete({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': i}}) | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(len(shapers), 0) | |
| def nested_depth_limit(cfg, nl_shaper) -> None: | |
| r"""Nest nodes as deep as the device allows to find the max depth. | |
| netdev | |
| | | |
| N1 -- Q1 | |
| | | |
| N2 -- Q2 | |
| | | |
| N3 -- Q3 | |
| : (deepen until the driver rejects) | |
| """ | |
| bw_max = 10000 | |
| _require_caps(cfg, nl_shaper, 'node', | |
| ['support-bw-max', 'support-metric-bps', 'support-nesting'], | |
| "device does not support node scope shapers with bw_max, metric bps and nesting") | |
| _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], | |
| "device does not support nested queue scope shapers with weight") | |
| nq = _require_queues(cfg, 3) | |
| node_ids = [] | |
| cleanups = [] | |
| queue_id = 1 | |
| max_depth = 0 | |
| limit_err = None | |
| # Create initial node with a queue leaf | |
| node_id = nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves': [{'handle': {'scope': 'queue', 'id': queue_id}, | |
| 'weight': 1}], | |
| 'handle': {'scope': 'node'}, | |
| 'metric': 'bps', | |
| 'bw-max': bw_max})['handle']['id'] | |
| node_ids.append(node_id) | |
| cleanups.append(defer(_delete_shaper, cfg, nl_shaper, | |
| {'scope': 'node', 'id': node_id})) | |
| cleanups.append(defer(_delete_shaper, cfg, nl_shaper, | |
| {'scope': 'queue', 'id': queue_id})) | |
| max_depth = 1 | |
| shaper = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': node_id}}) | |
| ksft_eq(shaper, {'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': node_id}, | |
| 'parent': {'scope': 'netdev'}, | |
| 'metric': 'bps', | |
| 'bw-max': bw_max}) | |
| shaper = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': queue_id}}) | |
| ksft_eq(shaper, {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': node_id}, | |
| 'handle': {'scope': 'queue', 'id': queue_id}, | |
| 'weight': 1}) | |
| queue_id += 1 | |
| # Keep nesting deeper until the driver rejects or queues run out. | |
| while queue_id < nq: | |
| parent_id = node_ids[-1] | |
| try: | |
| node_id = nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves': [{'handle': {'scope': 'queue', | |
| 'id': queue_id}, | |
| 'weight': 1}], | |
| 'handle': {'scope': 'node'}, | |
| 'parent': {'scope': 'node', | |
| 'id': parent_id}, | |
| 'metric': 'bps', | |
| 'bw-max': bw_max})['handle']['id'] | |
| except NlError as e: | |
| # Only treat "cannot nest deeper" errors as the depth limit; | |
| # drivers report it differently (EOPNOTSUPP/ENOSPC/E2BIG/EINVAL). | |
| # Anything else (ENOMEM, EIO, EPERM, driver bug) is a real failure. | |
| if e.error not in (errno.EOPNOTSUPP, errno.ENOSPC, | |
| errno.E2BIG, errno.EINVAL): | |
| raise | |
| limit_err = e | |
| break | |
| node_ids.append(node_id) | |
| cleanups.append(defer(_delete_shaper, cfg, nl_shaper, | |
| {'scope': 'node', 'id': node_id})) | |
| cleanups.append(defer(_delete_shaper, cfg, nl_shaper, | |
| {'scope': 'queue', 'id': queue_id})) | |
| max_depth += 1 | |
| shaper = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': node_id}}) | |
| ksft_eq(shaper, {'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': node_id}, | |
| 'parent': {'scope': 'node', 'id': parent_id}, | |
| 'metric': 'bps', | |
| 'bw-max': bw_max}) | |
| shaper = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', | |
| 'id': queue_id}}) | |
| ksft_eq(shaper, {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': node_id}, | |
| 'handle': {'scope': 'queue', 'id': queue_id}, | |
| 'weight': 1}) | |
| queue_id += 1 | |
| if limit_err: | |
| print(f"# max nesting depth supported: {max_depth} (errno {limit_err.error})") | |
| else: | |
| print(f"# max nesting depth tested: {max_depth}") | |
| ksft_true(max_depth >= 2, | |
| f"max nesting depth: {max_depth}") | |
| # Cleanup: exec the deferred deletes in reverse creation order, so each | |
| # queue leaf and deeper node is removed before its parent node. | |
| for cleanup in reversed(cleanups): | |
| cleanup.exec() | |
| ksft_eq(len(nl_shaper.get({'ifindex': cfg.ifindex}, dump=True)), 0) | |
| def delete_child_reparent(cfg, nl_shaper) -> None: | |
| r"""Deleting a child node reparents its queue leaf to the parent. | |
| netdev netdev | |
| | | | |
| N1 del N2 N1 | |
| / | \ -----> / | \ | |
| Q1 Q2 N2 Q1 Q2 Q3 | |
| | | |
| Q3 | |
| """ | |
| n1_bw_max = 10000 | |
| n2_bw_max = 5000 | |
| _require_caps(cfg, nl_shaper, 'node', | |
| ['support-bw-max', 'support-metric-bps', 'support-nesting'], | |
| "device does not support node scope shapers with bw_max, metric bps and nesting") | |
| _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], | |
| "device does not support nested queue scope shapers with weight") | |
| _require_queues(cfg, 4) | |
| # Create parent node N1 with Q1, Q2 | |
| n1_handle = nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 1}, | |
| {'handle': {'scope': 'queue', 'id': 2}, | |
| 'weight': 1}], | |
| 'handle': {'scope':'node'}, | |
| 'metric': 'bps', | |
| 'bw-max': n1_bw_max}) | |
| n1_id = n1_handle['handle']['id'] | |
| for i in range(1, 3): | |
| defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': i}) | |
| # Create child node N2 under N1 with Q3 | |
| n2_handle = nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 3}, | |
| 'weight': 1}], | |
| 'handle': {'scope':'node'}, | |
| 'parent': {'scope': 'node', 'id': n1_id}, | |
| 'metric': 'bps', | |
| 'bw-max': n2_bw_max}) | |
| n2_id = n2_handle['handle']['id'] | |
| defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 3}) | |
| # Delete child N2 - Q3 should reparent to N1 | |
| nl_shaper.delete({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': n2_id}}) | |
| with ksft_raises(NlError): | |
| nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': n2_id}}) | |
| shaper_n1 = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': n1_id}}) | |
| ksft_eq(shaper_n1, {'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': n1_id}, | |
| 'parent': {'scope': 'netdev'}, | |
| 'metric': 'bps', | |
| 'bw-max': n1_bw_max}) | |
| shaper_q3 = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 3}}) | |
| ksft_eq(shaper_q3, {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': n1_id}, | |
| 'handle': {'scope': 'queue', 'id': 3}, | |
| 'weight': 1}) | |
| # Cleanup | |
| for i in range(1, 4): | |
| _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i}) | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(len(shapers), 0) | |
| def move_queue_between_nodes(cfg, nl_shaper) -> None: | |
| r"""Move a queue between nodes by re-grouping the destination node. | |
| netdev netdev | |
| / \ .group N2 / \ | |
| N1 N2 {Q1,Q3} N1 N2 | |
| / \ | -------> | / \ | |
| Q1 Q2 Q3 Q2 Q1 Q3 | |
| """ | |
| n1_bw_max = 10000 | |
| n2_bw_max = 20000 | |
| _require_caps(cfg, nl_shaper, 'node', | |
| ['support-bw-max', 'support-metric-bps', 'support-nesting'], | |
| "device does not support node scope shapers with bw_max, metric bps and nesting") | |
| _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], | |
| "device does not support nested queue scope shapers with weight") | |
| _require_queues(cfg, 4) | |
| # Create N1 with Q1, Q2 | |
| n1_handle = nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 1}, | |
| {'handle': {'scope': 'queue', 'id': 2}, | |
| 'weight': 1}], | |
| 'handle': {'scope':'node'}, | |
| 'metric': 'bps', | |
| 'bw-max': n1_bw_max}) | |
| n1_id = n1_handle['handle']['id'] | |
| for i in range(1, 3): | |
| defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': i}) | |
| # Create N2 with Q3 | |
| n2_handle = nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 3}, | |
| 'weight': 1}], | |
| 'handle': {'scope':'node'}, | |
| 'metric': 'bps', | |
| 'bw-max': n2_bw_max}) | |
| n2_id = n2_handle['handle']['id'] | |
| defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 3}) | |
| # Move Q1 from N1 to N2 by re-grouping N2 with Q1, Q3 | |
| nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 2}, | |
| {'handle': {'scope': 'queue', 'id': 3}, | |
| 'weight': 1}], | |
| 'handle': {'scope':'node', 'id': n2_id}, | |
| 'metric': 'bps', | |
| 'bw-max': n2_bw_max}) | |
| shaper_n1 = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': n1_id}}) | |
| ksft_eq(shaper_n1, {'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': n1_id}, | |
| 'parent': {'scope': 'netdev'}, | |
| 'metric': 'bps', | |
| 'bw-max': n1_bw_max}) | |
| shaper_n2 = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': n2_id}}) | |
| ksft_eq(shaper_n2, {'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': n2_id}, | |
| 'parent': {'scope': 'netdev'}, | |
| 'metric': 'bps', | |
| 'bw-max': n2_bw_max}) | |
| # Verify Q1 moved to N2 | |
| shaper_q1 = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 1}}) | |
| ksft_eq(shaper_q1, {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': n2_id}, | |
| 'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 2}) | |
| # Verify Q2 still under N1 | |
| shaper_q2 = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 2}}) | |
| ksft_eq(shaper_q2, {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': n1_id}, | |
| 'handle': {'scope': 'queue', 'id': 2}, | |
| 'weight': 1}) | |
| # Verify Q3 remained under N2 | |
| shaper_q3 = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 3}}) | |
| ksft_eq(shaper_q3, {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': n2_id}, | |
| 'handle': {'scope': 'queue', 'id': 3}, | |
| 'weight': 1}) | |
| # Cleanup | |
| for i in range(1, 4): | |
| _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i}) | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(len(shapers), 0) | |
| def reject_reparenting(cfg, nl_shaper) -> None: | |
| r"""Reject reparenting an existing node; the hierarchy stays intact. | |
| netdev | |
| / \ rejected: N3 -> netdev | |
| N1 N2 rejected: N1 -> N2 | |
| / \ | (both EOPNOTSUPP) | |
| Q1 N3 Q2 | |
| | | |
| Q3 | |
| """ | |
| node1_bw_max = 10000 | |
| node2_bw_max = 5000 | |
| node3_bw_max = 20000 | |
| _require_caps(cfg, nl_shaper, 'node', | |
| ['support-bw-max', 'support-metric-bps', 'support-nesting'], | |
| "device does not support node scope shapers with bw_max, metric bps and nesting") | |
| _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], | |
| "device does not support nested queue scope shapers with weight") | |
| _require_queues(cfg, 4) | |
| # Create Node1 under netdev with Q1. | |
| node1_id = nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 1}], | |
| 'handle': {'scope':'node'}, | |
| 'metric': 'bps', | |
| 'bw-max': node1_bw_max})['handle']['id'] | |
| defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 1}) | |
| defer(_delete_shaper, cfg, nl_shaper, {'scope': 'node', 'id': node1_id}) | |
| # Create Node2 under netdev with Q2. | |
| node2_id = nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 2}, | |
| 'weight': 1}], | |
| 'handle': {'scope':'node'}, | |
| 'metric': 'bps', | |
| 'bw-max': node2_bw_max})['handle']['id'] | |
| defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 2}) | |
| defer(_delete_shaper, cfg, nl_shaper, {'scope': 'node', 'id': node2_id}) | |
| # Create Node3 nested under Node1 with Q3. | |
| node3_id = nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 3}, | |
| 'weight': 1}], | |
| 'handle': {'scope':'node'}, | |
| 'metric': 'bps', | |
| 'bw-max': node3_bw_max, | |
| 'parent': {'scope': 'node', 'id': node1_id}})['handle']['id'] | |
| defer(_delete_shaper, cfg, nl_shaper, {'scope': 'queue', 'id': 3}) | |
| defer(_delete_shaper, cfg, nl_shaper, {'scope': 'node', 'id': node3_id}) | |
| # Reparenting a nested node up to netdev must fail. | |
| with ksft_raises(NlError) as cm: | |
| nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 3}, | |
| 'weight': 1}], | |
| 'handle': {'scope':'node', 'id': node3_id}, | |
| 'parent': {'scope': 'netdev'}}) | |
| if cm.exception: | |
| ksft_eq(cm.exception.error, errno.EOPNOTSUPP) | |
| # Reparenting a node under another node must fail as well. | |
| with ksft_raises(NlError) as cm: | |
| nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 1}], | |
| 'handle': {'scope':'node', 'id': node1_id}, | |
| 'parent': {'scope': 'node', 'id': node2_id}}) | |
| if cm.exception: | |
| ksft_eq(cm.exception.error, errno.EOPNOTSUPP) | |
| # Updating a node with the same parent must succeed. | |
| nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 5}], | |
| 'handle': {'scope':'node', 'id': node1_id}, | |
| 'parent': {'scope': 'netdev'}}) | |
| # Updating a node without specifying the parent must succeed. | |
| nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 2}, | |
| 'weight': 7}], | |
| 'handle': {'scope':'node', 'id': node2_id}}) | |
| # The rejected reparents must have left the hierarchy intact. | |
| shaper = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': node1_id}}) | |
| ksft_eq(shaper, {'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': node1_id}, | |
| 'parent': {'scope': 'netdev'}, | |
| 'metric': 'bps', | |
| 'bw-max': node1_bw_max}) | |
| shaper = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': node2_id}}) | |
| ksft_eq(shaper, {'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': node2_id}, | |
| 'parent': {'scope': 'netdev'}, | |
| 'metric': 'bps', | |
| 'bw-max': node2_bw_max}) | |
| shaper = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': node3_id}}) | |
| ksft_eq(shaper, {'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'node', 'id': node3_id}, | |
| 'parent': {'scope': 'node', 'id': node1_id}, | |
| 'metric': 'bps', | |
| 'bw-max': node3_bw_max}) | |
| # Verify the leaf weights were updated and parents unchanged. | |
| shaper = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 1}}) | |
| ksft_eq(shaper, {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': node1_id}, | |
| 'handle': {'scope': 'queue', 'id': 1}, | |
| 'weight': 5}) | |
| shaper = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 2}}) | |
| ksft_eq(shaper, {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': node2_id}, | |
| 'handle': {'scope': 'queue', 'id': 2}, | |
| 'weight': 7}) | |
| shaper = nl_shaper.get({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': 3}}) | |
| ksft_eq(shaper, {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'node', 'id': node3_id}, | |
| 'handle': {'scope': 'queue', 'id': 3}, | |
| 'weight': 1}) | |
| # Cleanup. Delete the nodes explicitly instead of relying on the | |
| # empty-node auto-delete: a kernel that wrongly accepts a reparent may | |
| # mishandle the leaf accounting and leave a node behind. Removing them | |
| # by handle keeps a failing run from leaking state into later tests. | |
| for i in range(1, 4): | |
| _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': i}) | |
| for nid in (node1_id, node2_id, node3_id): | |
| _delete_shaper(cfg, nl_shaper, {'scope': 'node', 'id': nid}) | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(len(shapers), 0) | |
| def queue_update(cfg, nl_shaper) -> None: | |
| nq = _require_queues(cfg, 4) | |
| if not cfg.queues: | |
| raise KsftSkipEx("device does not support queue scope") | |
| netnl = EthtoolFamily() | |
| channels = netnl.channels_get({'header': {'dev-index': cfg.ifindex}}) | |
| ch_type = 'combined' if channels['combined-count'] else 'tx' | |
| for i in range(3): | |
| nl_shaper.set({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': i}, | |
| 'metric': 'bps', | |
| 'bw-max': (i + 1) * 1000}) | |
| defer(cmd, f"ethtool -L {cfg.dev['ifname']} {ch_type} {nq}") | |
| # Delete a channel, with no shapers configured on top of the related | |
| # queue: no changes expected | |
| cmd(f"ethtool -L {cfg.dev['ifname']} {ch_type} 3") | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(shapers, [{'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'netdev'}, | |
| 'handle': {'scope': 'queue', 'id': 0}, | |
| 'metric': 'bps', | |
| 'bw-max': 1000}, | |
| {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'netdev'}, | |
| 'handle': {'scope': 'queue', 'id': 1}, | |
| 'metric': 'bps', | |
| 'bw-max': 2000}, | |
| {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'netdev'}, | |
| 'handle': {'scope': 'queue', 'id': 2}, | |
| 'metric': 'bps', | |
| 'bw-max': 3000}]) | |
| # Delete a channel, with a shaper configured on top of the related | |
| # queue: the shaper must be deleted, too | |
| cmd(f"ethtool -L {cfg.dev['ifname']} {ch_type} 2") | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(shapers, [{'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'netdev'}, | |
| 'handle': {'scope': 'queue', 'id': 0}, | |
| 'metric': 'bps', | |
| 'bw-max': 1000}, | |
| {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'netdev'}, | |
| 'handle': {'scope': 'queue', 'id': 1}, | |
| 'metric': 'bps', | |
| 'bw-max': 2000}]) | |
| # Restore the original channels number, no expected changes | |
| cmd(f"ethtool -L {cfg.dev['ifname']} {ch_type} {nq}") | |
| shapers = nl_shaper.get({'ifindex': cfg.ifindex}, dump=True) | |
| ksft_eq(shapers, [{'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'netdev'}, | |
| 'handle': {'scope': 'queue', 'id': 0}, | |
| 'metric': 'bps', | |
| 'bw-max': 1000}, | |
| {'ifindex': cfg.ifindex, | |
| 'parent': {'scope': 'netdev'}, | |
| 'handle': {'scope': 'queue', 'id': 1}, | |
| 'metric': 'bps', | |
| 'bw-max': 2000}]) | |
| # Final cleanup. | |
| for i in range(0, 2): | |
| nl_shaper.delete({'ifindex': cfg.ifindex, | |
| 'handle': {'scope': 'queue', 'id': i}}) | |
| def dup_leaves(cfg, nl_shaper) -> None: | |
| """ Ensure that the kernel rejects duplicate leaves. """ | |
| _require_caps(cfg, nl_shaper, 'node', ['support-bw-max', 'support-metric-bps'], | |
| "device does not support node scope shapers with bw_max and metric bps") | |
| _require_caps(cfg, nl_shaper, 'queue', ['support-nesting', 'support-weight'], | |
| "device does not support nested queue scope shapers with weight") | |
| node_handle = None | |
| with ksft_raises(NlError) as cm: | |
| node_handle = nl_shaper.group({ | |
| 'ifindex': cfg.ifindex, | |
| 'leaves':[{'handle': {'scope': 'queue', 'id': 0}, | |
| 'weight': 1}, | |
| {'handle': {'scope': 'queue', 'id': 0}, | |
| 'weight': 2}], | |
| 'handle': {'scope':'node'}, | |
| 'metric': 'bps', | |
| 'bw-max': 10000}) | |
| # Clean up in case the kernel wrongly accepted the request. | |
| if node_handle: | |
| _delete_shaper(cfg, nl_shaper, node_handle['handle']) | |
| _delete_shaper(cfg, nl_shaper, {'scope': 'queue', 'id': 0}) | |
| # ksft_raises() has already recorded the failure if nothing was raised. | |
| if cm.exception is None: | |
| return | |
| ksft_eq(cm.exception.error, errno.EINVAL) | |
| def main() -> None: | |
| with NetDrvEnv(__file__, queue_count=4) as cfg: | |
| cfg.queues = False | |
| cfg.netdev = False | |
| ksft_run([get_shapers, | |
| get_caps, | |
| set_qshapers, | |
| del_qshapers, | |
| set_nshapers, | |
| del_nshapers, | |
| set_all_supported_attrs, | |
| invalid_set_preserves_state, | |
| mixed_parent_group_requires_parent, | |
| recursive_empty_node_cleanup, | |
| basic_groups, | |
| basic_groups_with_rate, | |
| qgroups, | |
| set_node_shaper, | |
| group_update_rate, | |
| delegation, | |
| nested_depth_limit, | |
| delete_child_reparent, | |
| move_queue_between_nodes, | |
| reject_reparenting, | |
| dup_leaves, | |
| queue_update], | |
| args=(cfg, NetshaperFamily())) | |
| ksft_exit() | |
| if __name__ == "__main__": | |
| main() |