xref: /linux/tools/net/ynl/pyynl/lib/nlspec.py (revision 90e63d5354951d37fa2b3b91e6f17b95d2bf9bee)
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