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