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