Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

tools: ynl: rework policy access to support recursion

Donald points out that the current naive implementation using dicts
breaks if policy is recursive (child nest uses policy idx already
used by its parent).

Lean more into the NlPolicy class. This lets us "render" the policy
on demand, when user accesses it. If someone wants to do an infinite
walk that's on them :) Show policy info as attributes of the class
and use dict format to descend into sub-policies for extra neatness.

Reviewed-by: Donald Hunter <donald.hunter@gmail.com>
Link: https://patch.msgid.link/20260313232047.2068518-1-kuba@kernel.org
Signed-off-by: Jakub Kicinski <kuba@kernel.org>

+120 -62
+2 -2
tools/net/ynl/pyynl/cli.py
··· 313 313 if args.policy: 314 314 if args.do: 315 315 pol = ynl.get_policy(args.do, 'do') 316 - output(pol.attrs if pol else None) 316 + output(pol.to_dict() if pol else None) 317 317 args.do = None 318 318 if args.dump: 319 319 pol = ynl.get_policy(args.dump, 'dump') 320 - output(pol.attrs if pol else None) 320 + output(pol.to_dict() if pol else None) 321 321 args.dump = None 322 322 323 323 if args.ntf:
+107 -45
tools/net/ynl/pyynl/lib/ynl.py
··· 143 143 pass 144 144 145 145 146 - # pylint: disable=too-few-public-methods 147 146 class NlPolicy: 148 147 """Kernel policy for one mode (do or dump) of one operation. 149 148 150 - Returned by YnlFamily.get_policy(). Contains a dict of attributes 151 - the kernel accepts, with their validation constraints. 149 + Returned by YnlFamily.get_policy(). Attributes of the policy 150 + are accessible as attributes of the object. Nested policies 151 + can be accessed indexing the object like a dictionary:: 152 152 153 - Attributes: 154 - attrs: dict mapping attribute names to policy dicts, e.g. 155 - page-pool-stats-get do policy:: 153 + pol = ynl.get_policy('page-pool-stats-get', 'do') 154 + pol['info'].type # 'nested' 155 + pol['info']['id'].type # 'uint' 156 + pol['info']['id'].min_value # 1 156 157 157 - { 158 - 'info': {'type': 'nested', 'policy': { 159 - 'id': {'type': 'uint', 'min-value': 1, 160 - 'max-value': 4294967295}, 161 - 'ifindex': {'type': 'u32', 'min-value': 1, 162 - 'max-value': 2147483647}, 163 - }}, 164 - } 158 + Each policy entry always has a 'type' attribute (e.g. u32, string, 159 + nested). Optional attributes depending on the 'type': min-value, 160 + max-value, min-length, max-length, mask. 165 161 166 - Each policy dict always contains 'type' (e.g. u32, string, 167 - nested). Optional keys: min-value, max-value, min-length, 168 - max-length, mask, policy. 162 + Policies can form infinite nesting loops. These loops are trimmed 163 + when policy is converted to a dict with pol.to_dict(). 169 164 """ 170 - def __init__(self, attrs): 171 - self.attrs = attrs 165 + def __init__(self, ynl, policy_idx, policy_table, attr_set, props=None): 166 + self._policy_idx = policy_idx 167 + self._policy_table = policy_table 168 + self._ynl = ynl 169 + self._props = props or {} 170 + self._entries = {} 171 + self._cache = {} 172 + if policy_idx is not None and policy_idx in policy_table: 173 + for attr_id, decoded in policy_table[policy_idx].items(): 174 + if attr_set and attr_id in attr_set.attrs_by_val: 175 + spec = attr_set.attrs_by_val[attr_id] 176 + name = spec['name'] 177 + else: 178 + spec = None 179 + name = f'attr-{attr_id}' 180 + self._entries[name] = (spec, decoded) 181 + 182 + def __getitem__(self, name): 183 + """Descend into a nested policy by attribute name.""" 184 + if name not in self._cache: 185 + spec, decoded = self._entries[name] 186 + props = dict(decoded) 187 + child_idx = None 188 + child_set = None 189 + if 'policy-idx' in props: 190 + child_idx = props.pop('policy-idx') 191 + if spec and 'nested-attributes' in spec.yaml: 192 + child_set = self._ynl.attr_sets[spec.yaml['nested-attributes']] 193 + self._cache[name] = NlPolicy(self._ynl, child_idx, 194 + self._policy_table, 195 + child_set, props) 196 + return self._cache[name] 197 + 198 + def __getattr__(self, name): 199 + """Access this policy entry's own properties (type, min-value, etc.). 200 + 201 + Underscores in the name are converted to dashes, so that 202 + pol.min_value looks up "min-value". 203 + """ 204 + key = name.replace('_', '-') 205 + try: 206 + # Hack for level-0 which we still want to have .type but we don't 207 + # want type to pointlessly show up in the dict / JSON form. 208 + if not self._props and name == "type": 209 + return "nested" 210 + return self._props[key] 211 + except KeyError: 212 + raise AttributeError(name) 213 + 214 + def get(self, name, default=None): 215 + """Look up a child policy entry by attribute name, with a default.""" 216 + try: 217 + return self[name] 218 + except KeyError: 219 + return default 220 + 221 + def __contains__(self, name): 222 + return name in self._entries 223 + 224 + def __len__(self): 225 + return len(self._entries) 226 + 227 + def __iter__(self): 228 + return iter(self._entries) 229 + 230 + def keys(self): 231 + """Return attribute names accepted by this policy.""" 232 + return self._entries.keys() 233 + 234 + def to_dict(self, seen=None): 235 + """Convert to a plain dict, suitable for JSON serialization. 236 + 237 + Nested NlPolicy objects are expanded recursively. Cyclic 238 + references are trimmed (resolved to just {"type": "nested"}). 239 + """ 240 + if seen is None: 241 + seen = set() 242 + result = dict(self._props) 243 + if self._policy_idx is not None: 244 + if self._policy_idx not in seen: 245 + seen = seen | {self._policy_idx} 246 + children = {} 247 + for name in self: 248 + children[name] = self[name].to_dict(seen) 249 + if self._props: 250 + result['policy'] = children 251 + else: 252 + result = children 253 + return result 254 + 255 + def __repr__(self): 256 + return repr(self.to_dict()) 172 257 173 258 174 259 class NlAttr: ··· 1393 1308 def do_multi(self, ops): 1394 1309 return self._ops(ops) 1395 1310 1396 - def _resolve_policy(self, policy_idx, policy_table, attr_set): 1397 - attrs = {} 1398 - if policy_idx not in policy_table: 1399 - return attrs 1400 - for attr_id, decoded in policy_table[policy_idx].items(): 1401 - if attr_set and attr_id in attr_set.attrs_by_val: 1402 - spec = attr_set.attrs_by_val[attr_id] 1403 - name = spec['name'] 1404 - else: 1405 - spec = None 1406 - name = f'attr-{attr_id}' 1407 - if 'policy-idx' in decoded: 1408 - sub_set = None 1409 - if spec and 'nested-attributes' in spec.yaml: 1410 - sub_set = self.attr_sets[spec.yaml['nested-attributes']] 1411 - nested = self._resolve_policy(decoded['policy-idx'], 1412 - policy_table, sub_set) 1413 - del decoded['policy-idx'] 1414 - decoded['policy'] = nested 1415 - attrs[name] = decoded 1416 - return attrs 1417 - 1418 1311 def get_policy(self, op_name, mode): 1419 1312 """Query running kernel for the Netlink policy of an operation. 1420 1313 ··· 1404 1341 mode: 'do' or 'dump' 1405 1342 1406 1343 Returns: 1407 - NlPolicy with an attrs dict mapping attribute names to 1408 - their policy properties (type, min/max, nested, etc.), 1344 + NlPolicy acting as a read-only dict mapping attribute names 1345 + to their policy properties (type, min/max, nested, etc.), 1409 1346 or None if the operation has no policy for the given mode. 1410 1347 Empty policy usually implies that the operation rejects 1411 1348 all attributes. ··· 1416 1353 if mode not in op_policy: 1417 1354 return None 1418 1355 policy_idx = op_policy[mode] 1419 - attrs = self._resolve_policy(policy_idx, policy_table, op.attr_set) 1420 - return NlPolicy(attrs) 1356 + return NlPolicy(self, policy_idx, policy_table, op.attr_set)
+11 -15
tools/testing/selftests/net/nl_nlctrl.py
··· 68 68 # dev-get: do has a real policy with ifindex, dump has no policy 69 69 # (only the reject-all policy with maxattr=0) 70 70 pol = ndev.get_policy('dev-get', 'do') 71 - ksft_in('ifindex', pol.attrs, 72 - comment="dev-get do policy should have ifindex") 73 - ksft_eq(pol.attrs.get('ifindex', {}).get('type'), 'u32') 71 + ksft_in('ifindex', pol, comment="dev-get do policy should have ifindex") 72 + ksft_eq(pol['ifindex'].type, 'u32') 74 73 75 74 pol_dump = ndev.get_policy('dev-get', 'dump') 76 - ksft_eq(len(pol_dump.attrs), 0, 77 - comment="dev-get should not accept any attrs") 75 + ksft_eq(len(pol_dump), 0, comment="dev-get should not accept any attrs") 78 76 79 77 # napi-get: both do and dump have real policies 80 78 pol_do = ndev.get_policy('napi-get', 'do') 81 - ksft_ge(len(pol_do.attrs), 1) 79 + ksft_ge(len(pol_do), 1) 82 80 83 81 pol_dump = ndev.get_policy('napi-get', 'dump') 84 - ksft_ge(len(pol_dump.attrs), 1) 82 + ksft_ge(len(pol_dump), 1) 85 83 86 84 # -- ethtool (full ops) -- 87 85 et = EthtoolFamily() 88 86 89 87 # strset-get (has both do and dump, full ops share policy) 90 88 pol_do = et.get_policy('strset-get', 'do') 91 - ksft_ge(len(pol_do.attrs), 1, comment="strset-get should have a do policy") 89 + ksft_ge(len(pol_do), 1, comment="strset-get should have a do policy") 92 90 93 91 pol_dump = et.get_policy('strset-get', 'dump') 94 - ksft_ge(len(pol_dump.attrs), 1, 95 - comment="strset-get should have a dump policy") 92 + ksft_ge(len(pol_dump), 1, comment="strset-get should have a dump policy") 96 93 97 94 # Same policy means same attribute names 98 - ksft_eq(set(pol_do.attrs.keys()), set(pol_dump.attrs.keys())) 95 + ksft_eq(set(pol_do.keys()), set(pol_dump.keys())) 99 96 100 97 # linkinfo-set is do-only (SET command), no dump 101 98 pol_do = et.get_policy('linkinfo-set', 'do') 102 - ksft_ge(len(pol_do.attrs), 1, 103 - comment="linkinfo-set should have a do policy") 99 + ksft_ge(len(pol_do), 1, comment="linkinfo-set should have a do policy") 104 100 105 101 pol_dump = et.get_policy('linkinfo-set', 'dump') 106 102 ksft_eq(pol_dump, None, ··· 109 113 110 114 # dev-get do policy should have named attributes from the spec 111 115 pol = ndev.get_policy('dev-get', 'do') 112 - ksft_ge(len(pol.attrs), 1) 116 + ksft_ge(len(pol), 1) 113 117 # All attr names should be resolved (no 'attr-N' fallbacks) 114 - for name in pol.attrs: 118 + for name in pol: 115 119 ksft_true(not name.startswith('attr-'), 116 120 comment=f"unresolved attr name: {name}") 117 121