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