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 try: 443 _yaml_loader = pyyaml.CSafeLoader 444 except AttributeError: 445 _yaml_loader = pyyaml.SafeLoader 446 447 def __init__(self, spec_path, schema_path=None, exclude_ops=None): 448 with open(spec_path, "r", encoding='utf-8') as stream: 449 prefix = '# SPDX-License-Identifier: ' 450 first = stream.readline().strip() 451 if not first.startswith(prefix): 452 raise SpecException('SPDX license tag required in the spec') 453 self.license = first[len(prefix):] 454 455 stream.seek(0) 456 spec = pyyaml.load(stream, Loader=self._yaml_loader) 457 458 self.fixed_header = None 459 self._resolution_list = [] 460 461 super().__init__(self, spec) 462 463 self._exclude_ops = exclude_ops if exclude_ops else [] 464 465 self.proto = self.yaml.get('protocol', 'genetlink') 466 self.msg_id_model = self.yaml['operations'].get('enum-model', 'unified') 467 468 if schema_path is None: 469 schema_path = os.path.dirname(os.path.dirname(spec_path)) + f'/{self.proto}.yaml' 470 if schema_path: 471 with open(schema_path, "r", encoding='utf-8') as stream: 472 schema = pyyaml.load(stream, Loader=self._yaml_loader) 473 474 if SpecFamily.jsonschema is None: 475 SpecFamily.jsonschema = importlib.import_module("jsonschema") 476 477 SpecFamily.jsonschema.validate(self.yaml, schema) 478 479 self.attr_sets = collections.OrderedDict() 480 self.sub_msgs = collections.OrderedDict() 481 self.msgs = collections.OrderedDict() 482 self.req_by_value = collections.OrderedDict() 483 self.rsp_by_value = collections.OrderedDict() 484 self.ops = collections.OrderedDict() 485 self.ntfs = collections.OrderedDict() 486 self.consts = collections.OrderedDict() 487 self.mcast_groups = collections.OrderedDict() 488 self.kernel_family = collections.OrderedDict(self.yaml.get('kernel-family', {})) 489 490 last_exception = None 491 while len(self._resolution_list) > 0: 492 resolved = [] 493 unresolved = self._resolution_list 494 self._resolution_list = [] 495 496 for elem in unresolved: 497 try: 498 elem.resolve() 499 except (KeyError, AttributeError) as e: 500 self._resolution_list.append(elem) 501 last_exception = e 502 continue 503 504 resolved.append(elem) 505 506 if len(resolved) == 0: 507 raise last_exception 508 509 def new_enum(self, elem): 510 return SpecEnumSet(self, elem) 511 512 def new_attr_set(self, elem): 513 return SpecAttrSet(self, elem) 514 515 def new_struct(self, elem): 516 return SpecStruct(self, elem) 517 518 def new_sub_message(self, elem): 519 return SpecSubMessage(self, elem) 520 521 def new_operation(self, elem, req_val, rsp_val): 522 return SpecOperation(self, elem, req_val, rsp_val) 523 524 def new_mcast_group(self, elem): 525 return SpecMcastGroup(self, elem) 526 527 def add_unresolved(self, elem): 528 self._resolution_list.append(elem) 529 530 def _dictify_ops_unified(self): 531 self.fixed_header = self.yaml['operations'].get('fixed-header') 532 val = 1 533 for elem in self.yaml['operations']['list']: 534 if 'value' in elem: 535 val = elem['value'] 536 537 op = self.new_operation(elem, val, val) 538 val += 1 539 540 self.msgs[op.name] = op 541 542 def _dictify_ops_directional(self): 543 self.fixed_header = self.yaml['operations'].get('fixed-header') 544 req_val = rsp_val = 1 545 for elem in self.yaml['operations']['list']: 546 if 'notify' in elem or 'event' in elem: 547 if 'value' in elem: 548 rsp_val = elem['value'] 549 req_val_next = req_val 550 rsp_val_next = rsp_val + 1 551 req_val = None 552 elif 'do' in elem or 'dump' in elem: 553 mode = elem['do'] if 'do' in elem else elem['dump'] 554 555 v = mode.get('request', {}).get('value', None) 556 if v: 557 req_val = v 558 v = mode.get('reply', {}).get('value', None) 559 if v: 560 rsp_val = v 561 562 rsp_inc = 1 if 'reply' in mode else 0 563 req_val_next = req_val + 1 564 rsp_val_next = rsp_val + rsp_inc 565 else: 566 raise SpecException("Can't parse directional ops") 567 568 if req_val == req_val_next: 569 req_val = None 570 if rsp_val == rsp_val_next: 571 rsp_val = None 572 573 skip = False 574 for exclude in self._exclude_ops: 575 skip |= bool(exclude.match(elem['name'])) 576 if not skip: 577 op = self.new_operation(elem, req_val, rsp_val) 578 self.msgs[op.name] = op 579 580 req_val = req_val_next 581 rsp_val = rsp_val_next 582 583 def find_operation(self, name): 584 """ 585 For a given operation name, find and return operation spec. 586 """ 587 for op in self.yaml['operations']['list']: 588 if name == op['name']: 589 return op 590 return None 591 592 def resolve(self): 593 self.resolve_up(super()) 594 595 definitions = self.yaml.get('definitions', []) 596 for elem in definitions: 597 if elem['type'] == 'enum' or elem['type'] == 'flags': 598 self.consts[elem['name']] = self.new_enum(elem) 599 elif elem['type'] == 'struct': 600 self.consts[elem['name']] = self.new_struct(elem) 601 else: 602 self.consts[elem['name']] = elem 603 604 for elem in self.yaml['attribute-sets']: 605 attr_set = self.new_attr_set(elem) 606 self.attr_sets[elem['name']] = attr_set 607 608 for elem in self.yaml.get('sub-messages', []): 609 sub_message = self.new_sub_message(elem) 610 self.sub_msgs[sub_message.name] = sub_message 611 612 if self.msg_id_model == 'unified': 613 self._dictify_ops_unified() 614 elif self.msg_id_model == 'directional': 615 self._dictify_ops_directional() 616 617 for op in self.msgs.values(): 618 if op.req_value is not None: 619 self.req_by_value[op.req_value] = op 620 if op.rsp_value is not None: 621 self.rsp_by_value[op.rsp_value] = op 622 if not op.is_async and 'attribute-set' in op: 623 self.ops[op.name] = op 624 elif op.is_async: 625 self.ntfs[op.name] = op 626 627 mcgs = self.yaml.get('mcast-groups') 628 if mcgs: 629 for elem in mcgs['list']: 630 mcg = self.new_mcast_group(elem) 631 self.mcast_groups[elem['name']] = mcg 632