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