summaryrefslogtreecommitdiff
path: root/tools/testing/selftests/net/nl_nlctrl.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/testing/selftests/net/nl_nlctrl.py')
-rwxr-xr-xtools/testing/selftests/net/nl_nlctrl.py213
1 files changed, 213 insertions, 0 deletions
diff --git a/tools/testing/selftests/net/nl_nlctrl.py b/tools/testing/selftests/net/nl_nlctrl.py
new file mode 100755
index 000000000000..237b3d273260
--- /dev/null
+++ b/tools/testing/selftests/net/nl_nlctrl.py
@@ -0,0 +1,213 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+
+"""
+Tests for the nlctrl genetlink family (family info and policy dumps).
+"""
+
+from lib.py import ksft_run, ksft_exit
+from lib.py import ksft_eq, ksft_ge, ksft_true, ksft_in, ksft_not_in
+from lib.py import NetdevFamily, EthtoolFamily, NlctrlFamily
+
+# Families we can expect to always be around, and which between them
+# cover ops with a do, with a dump, and with both.
+FAMILIES = ('nlctrl', 'netdev')
+
+
+def _get_ops(ctrl, name):
+ """Get the ops of a family, keyed by command id."""
+ fam = ctrl.getfamily({'family-name': name})
+ ksft_eq(fam['family-name'], name)
+ ksft_true(fam['family-id'] > 0)
+
+ # The format of ops is quite odd, [{$idx: {"id"...}}, {$idx: {"id"...}}]
+ # Discard the indices and re-key by command id.
+ ops_by_id = {v['id']: v for op in fam['ops'] for v in op.values()}
+ ksft_eq(len(ops_by_id), len(fam['ops']),
+ comment=f"{name} lists a command twice")
+ return ops_by_id
+
+
+def _get_policy_map(ctrl, req):
+ """
+ The policy map in the Netlink replies looks like this:
+
+ [{'family-id': 16, 'op-policy': {'do': 0, 'dump': 0, 'op-id': 3}},
+ {'family-id': 16, 'op-policy': {'dump': 1, 'op-id': 4}}, ...]
+
+ Return the mapping:
+
+ {3:{'do','dump'}, 4:{'dump'}}
+
+ The policy itself is discarded here, only return which command has policy.
+ """
+ pol_map = {}
+ for msg in ctrl.getpolicy(req, dump=True):
+ if 'op-policy' not in msg:
+ continue
+ modes = dict(msg['op-policy'])
+ cmd = modes.pop('op-id')
+ ksft_not_in(cmd, pol_map, comment=f"command {cmd} reported twice")
+ pol_map[cmd] = set(modes.keys())
+ return pol_map
+
+
+def getfamily_do(ctrl) -> None:
+ """Query single families by name and validate their ops."""
+ ops = {name: _get_ops(ctrl, name) for name in FAMILIES}
+
+ for name, ops_by_id in ops.items():
+ for op in ops_by_id.values():
+ # All ops in nlctrl and netdev have a policy
+ ksft_in('cmd-cap-haspol', op['flags'],
+ comment=f"{name} op {op['id']} missing haspol")
+ ksft_true(op['flags'] & {'cmd-cap-do', 'cmd-cap-dump'},
+ comment=f"{name} op {op['id']} has no handler")
+
+ # nlctrl getfamily (id 3) does both, getpolicy (id 10) is dump-only
+ ksft_in('cmd-cap-do', ops['nlctrl'][3]['flags'])
+ ksft_in('cmd-cap-dump', ops['nlctrl'][3]['flags'])
+ ksft_not_in('cmd-cap-do', ops['nlctrl'][10]['flags'])
+ ksft_in('cmd-cap-dump', ops['nlctrl'][10]['flags'])
+
+ netdev = ops['netdev']
+
+ # dev-get (id 1) should support both do and dump
+ ksft_in('cmd-cap-do', netdev[1]['flags'])
+ ksft_in('cmd-cap-dump', netdev[1]['flags'])
+
+ # qstats-get (id 12) is dump-only
+ ksft_not_in('cmd-cap-do', netdev[12]['flags'])
+ ksft_in('cmd-cap-dump', netdev[12]['flags'])
+
+ # napi-set (id 14) is do-only and requires admin
+ ksft_in('cmd-cap-do', netdev[14]['flags'])
+ ksft_not_in('cmd-cap-dump', netdev[14]['flags'])
+ ksft_in('admin-perm', netdev[14]['flags'])
+
+ # Notification-only commands (dev-add/del/change-ntf etc.) must
+ # not appear in the ops list since they have no do/dump handlers.
+ for ntf_id in [2, 3, 4, 6, 7, 8]:
+ ksft_not_in(ntf_id, netdev,
+ comment=f"ntf-only cmd {ntf_id} should not be in ops")
+
+
+def getfamily_dump(ctrl) -> None:
+ """Dump all families and verify expected entries."""
+ families = ctrl.getfamily({}, dump=True)
+ ksft_ge(len(families), 2)
+
+ names = [f['family-name'] for f in families]
+ ksft_in('nlctrl', names, comment="nlctrl not found in family dump")
+ ksft_in('netdev', names, comment="netdev not found in family dump")
+
+
+def getpolicy_dump(_ctrl) -> None:
+ """Dump policies for ops using get_policy() and validate results.
+
+ Test with netdev (split ops) where do and dump can have different
+ policies, and with ethtool (full ops) where they always share one.
+ """
+ # -- netdev (split ops) --
+ ndev = NetdevFamily()
+
+ # dev-get: do has a real policy with ifindex, dump has no policy
+ # (only the reject-all policy with maxattr=0)
+ pol = ndev.get_policy('dev-get', 'do')
+ ksft_in('ifindex', pol, comment="dev-get do policy should have ifindex")
+ ksft_eq(pol['ifindex'].type, 'u32')
+
+ pol_dump = ndev.get_policy('dev-get', 'dump')
+ ksft_eq(len(pol_dump), 0, comment="dev-get should not accept any attrs")
+
+ # napi-get: both do and dump have real policies
+ pol_do = ndev.get_policy('napi-get', 'do')
+ ksft_ge(len(pol_do), 1)
+
+ pol_dump = ndev.get_policy('napi-get', 'dump')
+ ksft_ge(len(pol_dump), 1)
+
+ # -- ethtool (full ops) --
+ et = EthtoolFamily()
+
+ # strset-get (has both do and dump, full ops share policy)
+ pol_do = et.get_policy('strset-get', 'do')
+ ksft_ge(len(pol_do), 1, comment="strset-get should have a do policy")
+
+ pol_dump = et.get_policy('strset-get', 'dump')
+ ksft_ge(len(pol_dump), 1, comment="strset-get should have a dump policy")
+
+ # Same policy means same attribute names
+ ksft_eq(set(pol_do.keys()), set(pol_dump.keys()))
+
+ # linkinfo-set is do-only (SET command), no dump
+ pol_do = et.get_policy('linkinfo-set', 'do')
+ ksft_ge(len(pol_do), 1, comment="linkinfo-set should have a do policy")
+
+ pol_dump = et.get_policy('linkinfo-set', 'dump')
+ ksft_eq(pol_dump, None,
+ comment="linkinfo-set should not have a dump policy")
+
+
+def getpolicy_op_map(ctrl) -> None:
+ """Check the op-to-policy map consistency. Each op with 'haspol' flag
+ has to have a policy. The policy back-references must name only
+ real ops that exist, have given modes (do vs dump) and have 'haspol'.
+ """
+ for name in FAMILIES:
+ ops_by_id = _get_ops(ctrl, name)
+ haspol = {cmd for cmd, op in ops_by_id.items()
+ if 'cmd-cap-haspol' in op['flags']}
+
+ pol_map = _get_policy_map(ctrl, {'family-name': name})
+ ksft_eq(set(pol_map), haspol,
+ comment=f"{name} policy map does not match the op list")
+
+ # Walk the op list rather than the map, the map may be missing
+ # the very op we are after. Asking for a command the family does
+ # not have is an error, so it must not come from the map either.
+ for cmd in sorted(haspol):
+ modes = pol_map.get(cmd, set())
+
+ # The kernel only reports a mode the op actually has.
+ if 'do' in modes:
+ ksft_in('cmd-cap-do', ops_by_id[cmd]['flags'],
+ comment=f"{name} cmd {cmd} has no do")
+ if 'dump' in modes:
+ ksft_in('cmd-cap-dump', ops_by_id[cmd]['flags'],
+ comment=f"{name} cmd {cmd} has no dump")
+
+ # Asking for one op builds the map in a different place in
+ # the kernel, it has to report what the full dump did.
+ single = _get_policy_map(ctrl, {'family-name': name, 'op': cmd})
+ ksft_eq(single, {cmd: modes},
+ comment=f"{name} cmd {cmd} policy differs from the dump")
+
+
+def getpolicy_by_op(_ctrl) -> None:
+ """Query policy for specific ops, check attr names are resolved."""
+ ndev = NetdevFamily()
+
+ # dev-get do policy should have named attributes from the spec
+ pol = ndev.get_policy('dev-get', 'do')
+ ksft_ge(len(pol), 1)
+ # All attr names should be resolved (no 'attr-N' fallbacks)
+ for name in pol:
+ ksft_true(not name.startswith('attr-'),
+ comment=f"unresolved attr name: {name}")
+
+
+def main() -> None:
+ """ Ksft boiler plate main """
+ ctrl = NlctrlFamily()
+ ksft_run([getfamily_do,
+ getfamily_dump,
+ getpolicy_dump,
+ getpolicy_op_map,
+ getpolicy_by_op],
+ args=(ctrl, ))
+ ksft_exit()
+
+
+if __name__ == "__main__":
+ main()