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