1# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause 2# 3# pylint: disable=missing-function-docstring, too-many-instance-attributes, too-many-branches 4 5""" 6The nlspec is a python library for parsing and using YNL netlink 7specifications. 8""" 9 10import collections 11import importlib 12import os 13import yaml as pyyaml 14 15 16class SpecException(Exception): 17 """Netlink spec exception. 18 """ 19 20 21class SpecElement: 22 """Netlink spec element. 23 24 Abstract element of the Netlink spec. Implements the dictionary interface 25 for access to the raw spec. Supports iterative resolution of dependencies 26 across elements and class inheritance levels. The elements of the spec 27 may refer to each other, and although loops should be very rare, having 28 to maintain correct ordering of instantiation is painful, so the resolve() 29 method should be used to perform parts of init which require access to 30 other parts of the spec. 31 32 Attributes: 33 yaml raw spec as loaded from the spec file 34 family back reference to the full family 35 36 name name of the entity as listed in the spec (optional) 37 ident_name name which can be safely used as identifier in code (optional) 38 """ 39 def __init__(self, family, yaml): 40 self.yaml = yaml 41 self.family = family 42 43 if 'name' in self.yaml: 44 self.name = self.yaml['name'] 45 self.ident_name = self.name.replace('-', '_') 46 47 self._super_resolved = False 48 family.add_unresolved(self) 49 50 def __getitem__(self, key): 51 return self.yaml[key] 52 53 def __contains__(self, key): 54 return key in self.yaml 55 56 def get(self, key, default=None): 57 return self.yaml.get(key, default) 58 59 def resolve_up(self, up): 60 if not self._super_resolved: 61 up.resolve() 62 self._super_resolved = True 63 64 def resolve(self): 65 pass 66 67 68class SpecEnumEntry(SpecElement): 69 """ Entry within an enum declared in the Netlink spec. 70 71 Attributes: 72 doc documentation string 73 enum_set back reference to the enum 74 value numerical value of this enum (use accessors in most situations!) 75 76 Methods: 77 raw_value raw value, i.e. the id in the enum, unlike user value which is a mask for flags 78 user_value user value, same as raw value for enums, for flags it's the mask 79 """ 80 def __init__(self, enum_set, yaml, prev, value_start): 81 if isinstance(yaml, str): 82 yaml = {'name': yaml} 83 super().__init__(enum_set.family, yaml) 84 85 self.doc = yaml.get('doc', '') 86 self.enum_set = enum_set 87 88 if 'value' in yaml: 89 self.value = yaml['value'] 90 elif prev: 91 self.value = prev.value + 1 92 else: 93 self.value = value_start 94 95 def has_doc(self): 96 return bool(self.doc) 97 98 def raw_value(self): 99 return self.value 100 101 def user_value(self, as_flags=None): 102 if self.enum_set['type'] == 'flags' or as_flags: 103 return 1 << self.value 104 return self.value 105 106 107class SpecEnumSet(SpecElement): 108 """ Enum type 109 110 Represents an enumeration (list of numerical constants) 111 as declared in the "definitions" section of the spec. 112 113 Attributes: 114 type enum or flags 115 entries entries by name 116 entries_by_val entries by value 117 Methods: 118 get_mask for flags compute the mask of all defined values 119 """ 120 def __init__(self, family, yaml): 121 super().__init__(family, yaml) 122 123 self.type = yaml['type'] 124 125 prev_entry = None 126 value_start = self.yaml.get('value-start', 0) 127 self.entries = {} 128 self.entries_by_val = {} 129 for entry in self.yaml['entries']: 130 e = self.new_entry(entry, prev_entry, value_start) 131 self.entries[e.name] = e 132 self.entries_by_val[e.raw_value()] = e 133 prev_entry = e 134 135 def new_entry(self, entry, prev_entry, value_start): 136 return SpecEnumEntry(self, entry, prev_entry, value_start) 137 138 def has_doc(self): 139 if 'doc' in self.yaml: 140 return True 141 return self.has_entry_doc() 142 143 def has_entry_doc(self): 144 for entry in self.entries.values(): 145 if entry.has_doc(): 146 return True 147 return False 148 149 def get_mask(self, as_flags=None): 150 mask = 0 151 for e in self.entries.values(): 152 mask += e.user_value(as_flags) 153 return mask 154 155 156class SpecAttr(SpecElement): 157 """ Single Netlink attribute type 158 159 Represents a single attribute type within an attr space. 160 161 Attributes: 162 type string, attribute type 163 value numerical ID when serialized 164 attr_set Attribute Set containing this attr 165 is_multi bool, attr may repeat multiple times 166 struct_name string, name of struct definition 167 sub_type string, name of sub type 168 len integer, optional byte length of binary types 169 display_hint string, hint to help choose format specifier 170 when displaying the value 171 sub_message string, name of sub message type 172 selector string, name of attribute used to select 173 sub-message type 174 175 is_auto_scalar bool, attr is a variable-size scalar 176 """ 177 def __init__(self, family, attr_set, yaml, value): 178 super().__init__(family, yaml) 179 180 self.type = yaml['type'] 181 self.value = value 182 self.attr_set = attr_set 183 self.is_multi = yaml.get('multi-attr', False) 184 self.struct_name = yaml.get('struct') 185 self.sub_type = yaml.get('sub-type') 186 self.byte_order = yaml.get('byte-order') 187 self.len = yaml.get('len') 188 self.display_hint = yaml.get('display-hint') 189 self.sub_message = yaml.get('sub-message') 190 self.selector = yaml.get('selector') 191 192 self.is_auto_scalar = self.type in ("sint", "uint") 193 194 195class SpecAttrSet(SpecElement): 196 """ Netlink Attribute Set class. 197 198 Represents a ID space of attributes within Netlink. 199 200 Note that unlike other elements, which expose contents of the raw spec 201 via the dictionary interface Attribute Set exposes attributes by name. 202 203 Attributes: 204 attrs ordered dict of all attributes (indexed by name) 205 attrs_by_val ordered dict of all attributes (indexed by value) 206 subset_of parent set if this is a subset, otherwise None 207 """ 208 def __init__(self, family, yaml): 209 super().__init__(family, yaml) 210 211 self.subset_of = self.yaml.get('subset-of', None) 212 213 self.attrs = collections.OrderedDict() 214 self.attrs_by_val = collections.OrderedDict() 215 216 if self.subset_of is None: 217 val = 1 218 for elem in self.yaml['attributes']: 219 if 'value' in elem: 220 val = elem['value'] 221 222 attr = self.new_attr(elem, val) 223 self.attrs[attr.name] = attr 224 self.attrs_by_val[attr.value] = attr 225 val += 1 226 else: 227 real_set = family.attr_sets[self.subset_of] 228 for elem in self.yaml['attributes']: 229 real_attr = real_set[elem['name']] 230 combined_elem = real_attr.yaml | elem 231 attr = self.new_attr(combined_elem, real_attr.value) 232 233 self.attrs[attr.name] = attr 234 self.attrs_by_val[attr.value] = attr 235 236 def new_attr(self, elem, value): 237 return SpecAttr(self.family, self, elem, value) 238 239 def __getitem__(self, key): 240 return self.attrs[key] 241 242 def __contains__(self, key): 243 return key in self.attrs 244 245 def __iter__(self): 246 yield from self.attrs 247 248 def items(self): 249 return self.attrs.items() 250 251 252class SpecStructMember(SpecElement): 253 """Struct member attribute 254 255 Represents a single struct member attribute. 256 257 Attributes: 258 type string, type of the member attribute 259 byte_order string or None for native byte order 260 enum string, name of the enum definition 261 len integer, optional byte length of binary types 262 display_hint string, hint to help choose format specifier 263 when displaying the value 264 struct string, name of nested struct type 265 """ 266 def __init__(self, family, yaml): 267 super().__init__(family, yaml) 268 self.type = yaml['type'] 269 self.byte_order = yaml.get('byte-order') 270 self.enum = yaml.get('enum') 271 self.len = yaml.get('len') 272 self.display_hint = yaml.get('display-hint') 273 self.struct = yaml.get('struct') 274 275 276class SpecStruct(SpecElement): 277 """Netlink struct type 278 279 Represents a C struct definition. 280 281 Attributes: 282 members ordered list of struct members 283 """ 284 def __init__(self, family, yaml): 285 super().__init__(family, yaml) 286 287 self.members = [] 288 for member in yaml.get('members', []): 289 self.members.append(self.new_member(family, member)) 290 291 def new_member(self, family, elem): 292 return SpecStructMember(family, elem) 293 294 def __iter__(self): 295 yield from self.members 296 297 def items(self): 298 return self.members 299 300 301class SpecSubMessage(SpecElement): 302 """ Netlink sub-message definition 303 304 Represents a set of sub-message formats for polymorphic nlattrs 305 that contain type-specific sub messages. 306 307 Attributes: 308 name string, name of sub-message definition 309 formats dict of sub-message formats indexed by match value 310 """ 311 def __init__(self, family, yaml): 312 super().__init__(family, yaml) 313 314 self.formats = collections.OrderedDict() 315 for elem in self.yaml['formats']: 316 msg_format = self.new_format(family, elem) 317 self.formats[msg_format.value] = msg_format 318 319 def new_format(self, family, msg_format): 320 return SpecSubMessageFormat(family, msg_format) 321 322 323class SpecSubMessageFormat(SpecElement): 324 """ Netlink sub-message format definition 325 326 Represents a single format for a sub-message. 327 328 Attributes: 329 value attribute value to match against type selector 330 fixed_header string, name of fixed header, or None 331 attr_set string, name of attribute set, or None 332 """ 333 def __init__(self, family, yaml): 334 super().__init__(family, yaml) 335 336 self.value = yaml.get('value') 337 self.fixed_header = yaml.get('fixed-header') 338 self.attr_set = yaml.get('attribute-set') 339 340 341class SpecOperation(SpecElement): 342 """Netlink Operation 343 344 Information about a single Netlink operation. 345 346 Attributes: 347 value numerical ID when serialized, None if req/rsp values differ 348 349 req_value numerical ID when serialized, user -> kernel 350 rsp_value numerical ID when serialized, user <- kernel 351 modes supported operation modes (do, dump, event etc.) 352 is_call bool, whether the operation is a call 353 is_async bool, whether the operation is a notification 354 is_resv bool, whether the operation does not exist (it's just a reserved ID) 355 attr_set attribute set name 356 fixed_header string, optional name of fixed header struct 357 358 yaml raw spec as loaded from the spec file 359 """ 360 def __init__(self, family, yaml, req_value, rsp_value): 361 super().__init__(family, yaml) 362 363 self.value = req_value if req_value == rsp_value else None 364 self.req_value = req_value 365 self.rsp_value = rsp_value 366 367 self.modes = yaml.keys() & {'do', 'dump', 'event', 'notify'} 368 self.is_call = 'do' in yaml or 'dump' in yaml 369 self.is_async = 'notify' in yaml or 'event' in yaml 370 self.is_resv = not self.is_async and not self.is_call 371 self.fixed_header = self.yaml.get('fixed-header', family.fixed_header) 372 373 # Added by resolve: 374 self.attr_set = None 375 delattr(self, "attr_set") 376 377 def resolve(self): 378 self.resolve_up(super()) 379 380 if 'attribute-set' in self.yaml: 381 attr_set_name = self.yaml['attribute-set'] 382 elif 'notify' in self.yaml: 383 msg = self.family.msgs[self.yaml['notify']] 384 attr_set_name = msg['attribute-set'] 385 elif self.is_resv: 386 attr_set_name = '' 387 else: 388 raise SpecException(f"Can't resolve attribute set for op '{self.name}'") 389 if attr_set_name: 390 self.attr_set = self.family.attr_sets[attr_set_name] 391 392 393class SpecMcastGroup(SpecElement): 394 """Netlink Multicast Group 395 396 Information about a multicast group. 397 398 Value is only used for classic netlink families that use the 399 netlink-raw schema. Genetlink families use dynamic ID allocation 400 where the ids of multicast groups get resolved at runtime. Value 401 will be None for genetlink families. 402 403 Attributes: 404 name name of the mulitcast group 405 value integer id of this multicast group for netlink-raw or None 406 yaml raw spec as loaded from the spec file 407 """ 408 def __init__(self, family, yaml): 409 super().__init__(family, yaml) 410 self.value = self.yaml.get('value') 411 412 413class SpecFamily(SpecElement): 414 """ Netlink Family Spec class. 415 416 Netlink family information loaded from a spec (e.g. in YAML). 417 Takes care of unfolding implicit information which can be skipped 418 in the spec itself for brevity. 419 420 The class can be used like a dictionary to access the raw spec 421 elements but that's usually a bad idea. 422 423 Attributes: 424 proto protocol type (e.g. genetlink) 425 msg_id_model enum-model for operations (unified, directional etc.) 426 license spec license (loaded from an SPDX tag on the spec) 427 428 attr_sets dict of attribute sets 429 msgs dict of all messages (index by name) 430 sub_msgs dict of all sub messages (index by name) 431 ops dict of all valid requests / responses 432 ntfs dict of all async events 433 consts dict of all constants/enums 434 fixed_header string, optional name of family default fixed header struct 435 mcast_groups dict of all multicast groups (index by name) 436 kernel_family dict of kernel family attributes 437 """ 438 439 # To be loaded dynamically as needed 440 jsonschema = None 441 442 def __init__(self, spec_path, schema_path=None, exclude_ops=None): 443 with open(spec_path, "r", encoding='utf-8') as stream: 444 prefix = '# SPDX-License-Identifier: ' 445 first = stream.readline().strip() 446 if not first.startswith(prefix): 447 raise SpecException('SPDX license tag required in the spec') 448 self.license = first[len(prefix):] 449 450 stream.seek(0) 451 spec = pyyaml.safe_load(stream) 452 453 self.fixed_header = None 454 self._resolution_list = [] 455 456 super().__init__(self, spec) 457 458 self._exclude_ops = exclude_ops if exclude_ops else [] 459 460 self.proto = self.yaml.get('protocol', 'genetlink') 461 self.msg_id_model = self.yaml['operations'].get('enum-model', 'unified') 462 463 if schema_path is None: 464 schema_path = os.path.dirname(os.path.dirname(spec_path)) + f'/{self.proto}.yaml' 465 if schema_path: 466 with open(schema_path, "r", encoding='utf-8') as stream: 467 schema = pyyaml.safe_load(stream) 468 469 if SpecFamily.jsonschema is None: 470 SpecFamily.jsonschema = importlib.import_module("jsonschema") 471 472 SpecFamily.jsonschema.validate(self.yaml, schema) 473 474 self.attr_sets = collections.OrderedDict() 475 self.sub_msgs = collections.OrderedDict() 476 self.msgs = collections.OrderedDict() 477 self.req_by_value = collections.OrderedDict() 478 self.rsp_by_value = collections.OrderedDict() 479 self.ops = collections.OrderedDict() 480 self.ntfs = collections.OrderedDict() 481 self.consts = collections.OrderedDict() 482 self.mcast_groups = collections.OrderedDict() 483 self.kernel_family = collections.OrderedDict(self.yaml.get('kernel-family', {})) 484 485 last_exception = None 486 while len(self._resolution_list) > 0: 487 resolved = [] 488 unresolved = self._resolution_list 489 self._resolution_list = [] 490 491 for elem in unresolved: 492 try: 493 elem.resolve() 494 except (KeyError, AttributeError) as e: 495 self._resolution_list.append(elem) 496 last_exception = e 497 continue 498 499 resolved.append(elem) 500 501 if len(resolved) == 0: 502 raise last_exception 503 504 def new_enum(self, elem): 505 return SpecEnumSet(self, elem) 506 507 def new_attr_set(self, elem): 508 return SpecAttrSet(self, elem) 509 510 def new_struct(self, elem): 511 return SpecStruct(self, elem) 512 513 def new_sub_message(self, elem): 514 return SpecSubMessage(self, elem) 515 516 def new_operation(self, elem, req_val, rsp_val): 517 return SpecOperation(self, elem, req_val, rsp_val) 518 519 def new_mcast_group(self, elem): 520 return SpecMcastGroup(self, elem) 521 522 def add_unresolved(self, elem): 523 self._resolution_list.append(elem) 524 525 def _dictify_ops_unified(self): 526 self.fixed_header = self.yaml['operations'].get('fixed-header') 527 val = 1 528 for elem in self.yaml['operations']['list']: 529 if 'value' in elem: 530 val = elem['value'] 531 532 op = self.new_operation(elem, val, val) 533 val += 1 534 535 self.msgs[op.name] = op 536 537 def _dictify_ops_directional(self): 538 self.fixed_header = self.yaml['operations'].get('fixed-header') 539 req_val = rsp_val = 1 540 for elem in self.yaml['operations']['list']: 541 if 'notify' in elem or 'event' in elem: 542 if 'value' in elem: 543 rsp_val = elem['value'] 544 req_val_next = req_val 545 rsp_val_next = rsp_val + 1 546 req_val = None 547 elif 'do' in elem or 'dump' in elem: 548 mode = elem['do'] if 'do' in elem else elem['dump'] 549 550 v = mode.get('request', {}).get('value', None) 551 if v: 552 req_val = v 553 v = mode.get('reply', {}).get('value', None) 554 if v: 555 rsp_val = v 556 557 rsp_inc = 1 if 'reply' in mode else 0 558 req_val_next = req_val + 1 559 rsp_val_next = rsp_val + rsp_inc 560 else: 561 raise SpecException("Can't parse directional ops") 562 563 if req_val == req_val_next: 564 req_val = None 565 if rsp_val == rsp_val_next: 566 rsp_val = None 567 568 skip = False 569 for exclude in self._exclude_ops: 570 skip |= bool(exclude.match(elem['name'])) 571 if not skip: 572 op = self.new_operation(elem, req_val, rsp_val) 573 self.msgs[op.name] = op 574 575 req_val = req_val_next 576 rsp_val = rsp_val_next 577 578 def find_operation(self, name): 579 """ 580 For a given operation name, find and return operation spec. 581 """ 582 for op in self.yaml['operations']['list']: 583 if name == op['name']: 584 return op 585 return None 586 587 def resolve(self): 588 self.resolve_up(super()) 589 590 definitions = self.yaml.get('definitions', []) 591 for elem in definitions: 592 if elem['type'] == 'enum' or elem['type'] == 'flags': 593 self.consts[elem['name']] = self.new_enum(elem) 594 elif elem['type'] == 'struct': 595 self.consts[elem['name']] = self.new_struct(elem) 596 else: 597 self.consts[elem['name']] = elem 598 599 for elem in self.yaml['attribute-sets']: 600 attr_set = self.new_attr_set(elem) 601 self.attr_sets[elem['name']] = attr_set 602 603 for elem in self.yaml.get('sub-messages', []): 604 sub_message = self.new_sub_message(elem) 605 self.sub_msgs[sub_message.name] = sub_message 606 607 if self.msg_id_model == 'unified': 608 self._dictify_ops_unified() 609 elif self.msg_id_model == 'directional': 610 self._dictify_ops_directional() 611 612 for op in self.msgs.values(): 613 if op.req_value is not None: 614 self.req_by_value[op.req_value] = op 615 if op.rsp_value is not None: 616 self.rsp_by_value[op.rsp_value] = op 617 if not op.is_async and 'attribute-set' in op: 618 self.ops[op.name] = op 619 elif op.is_async: 620 self.ntfs[op.name] = op 621 622 mcgs = self.yaml.get('mcast-groups') 623 if mcgs: 624 for elem in mcgs['list']: 625 mcg = self.new_mcast_group(elem) 626 self.mcast_groups[elem['name']] = mcg 627