xref: /linux/tools/testing/selftests/net/nl_nlctrl.py (revision f2c53ea949c5048f96b3dbb5a5ee7131ce4ff2de)
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0
3
4"""
5Tests for the nlctrl genetlink family (family info and policy dumps).
6"""
7
8from lib.py import ksft_run, ksft_exit
9from lib.py import ksft_eq, ksft_ge, ksft_true, ksft_in, ksft_not_in
10from lib.py import NetdevFamily, EthtoolFamily, NlctrlFamily
11
12# Families we can expect to always be around, and which between them
13# cover ops with a do, with a dump, and with both.
14FAMILIES = ('nlctrl', 'netdev')
15
16
17def _get_ops(ctrl, name):
18    """Get the ops of a family, keyed by command id."""
19    fam = ctrl.getfamily({'family-name': name})
20    ksft_eq(fam['family-name'], name)
21    ksft_true(fam['family-id'] > 0)
22
23    # The format of ops is quite odd, [{$idx: {"id"...}}, {$idx: {"id"...}}]
24    # Discard the indices and re-key by command id.
25    ops_by_id = {v['id']: v for op in fam['ops'] for v in op.values()}
26    ksft_eq(len(ops_by_id), len(fam['ops']),
27            comment=f"{name} lists a command twice")
28    return ops_by_id
29
30
31def _get_policy_map(ctrl, req):
32    """
33    The policy map in the Netlink replies looks like this:
34
35         [{'family-id': 16, 'op-policy': {'do': 0, 'dump': 0, 'op-id': 3}},
36          {'family-id': 16, 'op-policy': {'dump': 1, 'op-id': 4}}, ...]
37
38    Return the mapping:
39
40         {3:{'do','dump'}, 4:{'dump'}}
41
42    The policy itself is discarded here, only return which command has policy.
43    """
44    pol_map = {}
45    for msg in ctrl.getpolicy(req, dump=True):
46        if 'op-policy' not in msg:
47            continue
48        modes = dict(msg['op-policy'])
49        cmd = modes.pop('op-id')
50        ksft_not_in(cmd, pol_map, comment=f"command {cmd} reported twice")
51        pol_map[cmd] = set(modes.keys())
52    return pol_map
53
54
55def getfamily_do(ctrl) -> None:
56    """Query single families by name and validate their ops."""
57    ops = {name: _get_ops(ctrl, name) for name in FAMILIES}
58
59    for name, ops_by_id in ops.items():
60        for op in ops_by_id.values():
61            # All ops in nlctrl and netdev have a policy
62            ksft_in('cmd-cap-haspol', op['flags'],
63                    comment=f"{name} op {op['id']} missing haspol")
64            ksft_true(op['flags'] & {'cmd-cap-do', 'cmd-cap-dump'},
65                      comment=f"{name} op {op['id']} has no handler")
66
67    # nlctrl getfamily (id 3) does both, getpolicy (id 10) is dump-only
68    ksft_in('cmd-cap-do', ops['nlctrl'][3]['flags'])
69    ksft_in('cmd-cap-dump', ops['nlctrl'][3]['flags'])
70    ksft_not_in('cmd-cap-do', ops['nlctrl'][10]['flags'])
71    ksft_in('cmd-cap-dump', ops['nlctrl'][10]['flags'])
72
73    netdev = ops['netdev']
74
75    # dev-get (id 1) should support both do and dump
76    ksft_in('cmd-cap-do', netdev[1]['flags'])
77    ksft_in('cmd-cap-dump', netdev[1]['flags'])
78
79    # qstats-get (id 12) is dump-only
80    ksft_not_in('cmd-cap-do', netdev[12]['flags'])
81    ksft_in('cmd-cap-dump', netdev[12]['flags'])
82
83    # napi-set (id 14) is do-only and requires admin
84    ksft_in('cmd-cap-do', netdev[14]['flags'])
85    ksft_not_in('cmd-cap-dump', netdev[14]['flags'])
86    ksft_in('admin-perm', netdev[14]['flags'])
87
88    # Notification-only commands (dev-add/del/change-ntf etc.) must
89    # not appear in the ops list since they have no do/dump handlers.
90    for ntf_id in [2, 3, 4, 6, 7, 8]:
91        ksft_not_in(ntf_id, netdev,
92                    comment=f"ntf-only cmd {ntf_id} should not be in ops")
93
94
95def getfamily_dump(ctrl) -> None:
96    """Dump all families and verify expected entries."""
97    families = ctrl.getfamily({}, dump=True)
98    ksft_ge(len(families), 2)
99
100    names = [f['family-name'] for f in families]
101    ksft_in('nlctrl', names, comment="nlctrl not found in family dump")
102    ksft_in('netdev', names, comment="netdev not found in family dump")
103
104
105def getpolicy_dump(_ctrl) -> None:
106    """Dump policies for ops using get_policy() and validate results.
107
108    Test with netdev (split ops) where do and dump can have different
109    policies, and with ethtool (full ops) where they always share one.
110    """
111    # -- netdev (split ops) --
112    ndev = NetdevFamily()
113
114    # dev-get: do has a real policy with ifindex, dump has no policy
115    # (only the reject-all policy with maxattr=0)
116    pol = ndev.get_policy('dev-get', 'do')
117    ksft_in('ifindex', pol, comment="dev-get do policy should have ifindex")
118    ksft_eq(pol['ifindex'].type, 'u32')
119
120    pol_dump = ndev.get_policy('dev-get', 'dump')
121    ksft_eq(len(pol_dump), 0, comment="dev-get should not accept any attrs")
122
123    # napi-get: both do and dump have real policies
124    pol_do = ndev.get_policy('napi-get', 'do')
125    ksft_ge(len(pol_do), 1)
126
127    pol_dump = ndev.get_policy('napi-get', 'dump')
128    ksft_ge(len(pol_dump), 1)
129
130    # -- ethtool (full ops) --
131    et = EthtoolFamily()
132
133    # strset-get (has both do and dump, full ops share policy)
134    pol_do = et.get_policy('strset-get', 'do')
135    ksft_ge(len(pol_do), 1, comment="strset-get should have a do policy")
136
137    pol_dump = et.get_policy('strset-get', 'dump')
138    ksft_ge(len(pol_dump), 1, comment="strset-get should have a dump policy")
139
140    # Same policy means same attribute names
141    ksft_eq(set(pol_do.keys()), set(pol_dump.keys()))
142
143    # linkinfo-set is do-only (SET command), no dump
144    pol_do = et.get_policy('linkinfo-set', 'do')
145    ksft_ge(len(pol_do), 1, comment="linkinfo-set should have a do policy")
146
147    pol_dump = et.get_policy('linkinfo-set', 'dump')
148    ksft_eq(pol_dump, None,
149            comment="linkinfo-set should not have a dump policy")
150
151
152def getpolicy_op_map(ctrl) -> None:
153    """Check the op-to-policy map consistency. Each op with 'haspol' flag
154    has to have a policy. The policy back-references must name only
155    real ops that exist, have given modes (do vs dump) and have 'haspol'.
156    """
157    for name in FAMILIES:
158        ops_by_id = _get_ops(ctrl, name)
159        haspol = {cmd for cmd, op in ops_by_id.items()
160                  if 'cmd-cap-haspol' in op['flags']}
161
162        pol_map = _get_policy_map(ctrl, {'family-name': name})
163        ksft_eq(set(pol_map), haspol,
164                comment=f"{name} policy map does not match the op list")
165
166        # Walk the op list rather than the map, the map may be missing
167        # the very op we are after. Asking for a command the family does
168        # not have is an error, so it must not come from the map either.
169        for cmd in sorted(haspol):
170            modes = pol_map.get(cmd, set())
171
172            # The kernel only reports a mode the op actually has.
173            if 'do' in modes:
174                ksft_in('cmd-cap-do', ops_by_id[cmd]['flags'],
175                        comment=f"{name} cmd {cmd} has no do")
176            if 'dump' in modes:
177                ksft_in('cmd-cap-dump', ops_by_id[cmd]['flags'],
178                        comment=f"{name} cmd {cmd} has no dump")
179
180            # Asking for one op builds the map in a different place in
181            # the kernel, it has to report what the full dump did.
182            single = _get_policy_map(ctrl, {'family-name': name, 'op': cmd})
183            ksft_eq(single, {cmd: modes},
184                    comment=f"{name} cmd {cmd} policy differs from the dump")
185
186
187def getpolicy_by_op(_ctrl) -> None:
188    """Query policy for specific ops, check attr names are resolved."""
189    ndev = NetdevFamily()
190
191    # dev-get do policy should have named attributes from the spec
192    pol = ndev.get_policy('dev-get', 'do')
193    ksft_ge(len(pol), 1)
194    # All attr names should be resolved (no 'attr-N' fallbacks)
195    for name in pol:
196        ksft_true(not name.startswith('attr-'),
197                  comment=f"unresolved attr name: {name}")
198
199
200def main() -> None:
201    """ Ksft boiler plate main """
202    ctrl = NlctrlFamily()
203    ksft_run([getfamily_do,
204              getfamily_dump,
205              getpolicy_dump,
206              getpolicy_op_map,
207              getpolicy_by_op],
208             args=(ctrl, ))
209    ksft_exit()
210
211
212if __name__ == "__main__":
213    main()
214