xref: /linux/tools/net/ynl/lib/nlspec.py (revision b4db9f840283caca0d904436f187ef56a9126eaa)
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        for entry in self.entries.values():
135            if entry.has_doc():
136                return True
137        return False
138
139    def get_mask(self, as_flags=None):
140        mask = 0
141        for e in self.entries.values():
142            mask += e.user_value(as_flags)
143        return mask
144
145
146class SpecAttr(SpecElement):
147    """ Single Netlink attribute type
148
149    Represents a single attribute type within an attr space.
150
151    Attributes:
152        type          string, attribute type
153        value         numerical ID when serialized
154        attr_set      Attribute Set containing this attr
155        is_multi      bool, attr may repeat multiple times
156        struct_name   string, name of struct definition
157        sub_type      string, name of sub type
158        len           integer, optional byte length of binary types
159        display_hint  string, hint to help choose format specifier
160                      when displaying the value
161        sub_message   string, name of sub message type
162        selector      string, name of attribute used to select
163                      sub-message type
164
165        is_auto_scalar bool, attr is a variable-size scalar
166    """
167    def __init__(self, family, attr_set, yaml, value):
168        super().__init__(family, yaml)
169
170        self.type = yaml['type']
171        self.value = value
172        self.attr_set = attr_set
173        self.is_multi = yaml.get('multi-attr', False)
174        self.struct_name = yaml.get('struct')
175        self.sub_type = yaml.get('sub-type')
176        self.byte_order = yaml.get('byte-order')
177        self.len = yaml.get('len')
178        self.display_hint = yaml.get('display-hint')
179        self.sub_message = yaml.get('sub-message')
180        self.selector = yaml.get('selector')
181
182        self.is_auto_scalar = self.type == "sint" or self.type == "uint"
183
184
185class SpecAttrSet(SpecElement):
186    """ Netlink Attribute Set class.
187
188    Represents a ID space of attributes within Netlink.
189
190    Note that unlike other elements, which expose contents of the raw spec
191    via the dictionary interface Attribute Set exposes attributes by name.
192
193    Attributes:
194        attrs      ordered dict of all attributes (indexed by name)
195        attrs_by_val  ordered dict of all attributes (indexed by value)
196        subset_of  parent set if this is a subset, otherwise None
197    """
198    def __init__(self, family, yaml):
199        super().__init__(family, yaml)
200
201        self.subset_of = self.yaml.get('subset-of', None)
202
203        self.attrs = collections.OrderedDict()
204        self.attrs_by_val = collections.OrderedDict()
205
206        if self.subset_of is None:
207            val = 1
208            for elem in self.yaml['attributes']:
209                if 'value' in elem:
210                    val = elem['value']
211
212                attr = self.new_attr(elem, val)
213                self.attrs[attr.name] = attr
214                self.attrs_by_val[attr.value] = attr
215                val += 1
216        else:
217            real_set = family.attr_sets[self.subset_of]
218            for elem in self.yaml['attributes']:
219                attr = real_set[elem['name']]
220                self.attrs[attr.name] = attr
221                self.attrs_by_val[attr.value] = attr
222
223    def new_attr(self, elem, value):
224        return SpecAttr(self.family, self, elem, value)
225
226    def __getitem__(self, key):
227        return self.attrs[key]
228
229    def __contains__(self, key):
230        return key in self.attrs
231
232    def __iter__(self):
233        yield from self.attrs
234
235    def items(self):
236        return self.attrs.items()
237
238
239class SpecStructMember(SpecElement):
240    """Struct member attribute
241
242    Represents a single struct member attribute.
243
244    Attributes:
245        type        string, type of the member attribute
246        byte_order  string or None for native byte order
247        enum        string, name of the enum definition
248        len         integer, optional byte length of binary types
249        display_hint  string, hint to help choose format specifier
250                      when displaying the value
251        struct      string, name of nested struct type
252    """
253    def __init__(self, family, yaml):
254        super().__init__(family, yaml)
255        self.type = yaml['type']
256        self.byte_order = yaml.get('byte-order')
257        self.enum = yaml.get('enum')
258        self.len = yaml.get('len')
259        self.display_hint = yaml.get('display-hint')
260        self.struct = yaml.get('struct')
261
262
263class SpecStruct(SpecElement):
264    """Netlink struct type
265
266    Represents a C struct definition.
267
268    Attributes:
269        members   ordered list of struct members
270    """
271    def __init__(self, family, yaml):
272        super().__init__(family, yaml)
273
274        self.members = []
275        for member in yaml.get('members', []):
276            self.members.append(self.new_member(family, member))
277
278    def new_member(self, family, elem):
279        return SpecStructMember(family, elem)
280
281    def __iter__(self):
282        yield from self.members
283
284    def items(self):
285        return self.members.items()
286
287
288class SpecSubMessage(SpecElement):
289    """ Netlink sub-message definition
290
291    Represents a set of sub-message formats for polymorphic nlattrs
292    that contain type-specific sub messages.
293
294    Attributes:
295        name     string, name of sub-message definition
296        formats  dict of sub-message formats indexed by match value
297    """
298    def __init__(self, family, yaml):
299        super().__init__(family, yaml)
300
301        self.formats = collections.OrderedDict()
302        for elem in self.yaml['formats']:
303            format = self.new_format(family, elem)
304            self.formats[format.value] = format
305
306    def new_format(self, family, format):
307        return SpecSubMessageFormat(family, format)
308
309
310class SpecSubMessageFormat(SpecElement):
311    """ Netlink sub-message format definition
312
313    Represents a single format for a sub-message.
314
315    Attributes:
316        value         attribute value to match against type selector
317        fixed_header  string, name of fixed header, or None
318        attr_set      string, name of attribute set, or None
319    """
320    def __init__(self, family, yaml):
321        super().__init__(family, yaml)
322
323        self.value = yaml.get('value')
324        self.fixed_header = yaml.get('fixed-header')
325        self.attr_set = yaml.get('attribute-set')
326
327
328class SpecOperation(SpecElement):
329    """Netlink Operation
330
331    Information about a single Netlink operation.
332
333    Attributes:
334        value           numerical ID when serialized, None if req/rsp values differ
335
336        req_value       numerical ID when serialized, user -> kernel
337        rsp_value       numerical ID when serialized, user <- kernel
338        is_call         bool, whether the operation is a call
339        is_async        bool, whether the operation is a notification
340        is_resv         bool, whether the operation does not exist (it's just a reserved ID)
341        attr_set        attribute set name
342        fixed_header    string, optional name of fixed header struct
343
344        yaml            raw spec as loaded from the spec file
345    """
346    def __init__(self, family, yaml, req_value, rsp_value):
347        super().__init__(family, yaml)
348
349        self.value = req_value if req_value == rsp_value else None
350        self.req_value = req_value
351        self.rsp_value = rsp_value
352
353        self.is_call = 'do' in yaml or 'dump' in yaml
354        self.is_async = 'notify' in yaml or 'event' in yaml
355        self.is_resv = not self.is_async and not self.is_call
356        self.fixed_header = self.yaml.get('fixed-header', family.fixed_header)
357
358        # Added by resolve:
359        self.attr_set = None
360        delattr(self, "attr_set")
361
362    def resolve(self):
363        self.resolve_up(super())
364
365        if 'attribute-set' in self.yaml:
366            attr_set_name = self.yaml['attribute-set']
367        elif 'notify' in self.yaml:
368            msg = self.family.msgs[self.yaml['notify']]
369            attr_set_name = msg['attribute-set']
370        elif self.is_resv:
371            attr_set_name = ''
372        else:
373            raise Exception(f"Can't resolve attribute set for op '{self.name}'")
374        if attr_set_name:
375            self.attr_set = self.family.attr_sets[attr_set_name]
376
377
378class SpecMcastGroup(SpecElement):
379    """Netlink Multicast Group
380
381    Information about a multicast group.
382
383    Value is only used for classic netlink families that use the
384    netlink-raw schema. Genetlink families use dynamic ID allocation
385    where the ids of multicast groups get resolved at runtime. Value
386    will be None for genetlink families.
387
388    Attributes:
389        name      name of the mulitcast group
390        value     integer id of this multicast group for netlink-raw or None
391        yaml      raw spec as loaded from the spec file
392    """
393    def __init__(self, family, yaml):
394        super().__init__(family, yaml)
395        self.value = self.yaml.get('value')
396
397
398class SpecFamily(SpecElement):
399    """ Netlink Family Spec class.
400
401    Netlink family information loaded from a spec (e.g. in YAML).
402    Takes care of unfolding implicit information which can be skipped
403    in the spec itself for brevity.
404
405    The class can be used like a dictionary to access the raw spec
406    elements but that's usually a bad idea.
407
408    Attributes:
409        proto     protocol type (e.g. genetlink)
410        msg_id_model   enum-model for operations (unified, directional etc.)
411        license   spec license (loaded from an SPDX tag on the spec)
412
413        attr_sets  dict of attribute sets
414        msgs       dict of all messages (index by name)
415        sub_msgs   dict of all sub messages (index by name)
416        ops        dict of all valid requests / responses
417        ntfs       dict of all async events
418        consts     dict of all constants/enums
419        fixed_header  string, optional name of family default fixed header struct
420        mcast_groups  dict of all multicast groups (index by name)
421        kernel_family   dict of kernel family attributes
422    """
423    def __init__(self, spec_path, schema_path=None, exclude_ops=None):
424        with open(spec_path, "r") as stream:
425            prefix = '# SPDX-License-Identifier: '
426            first = stream.readline().strip()
427            if not first.startswith(prefix):
428                raise Exception('SPDX license tag required in the spec')
429            self.license = first[len(prefix):]
430
431            stream.seek(0)
432            spec = yaml.safe_load(stream)
433
434        self._resolution_list = []
435
436        super().__init__(self, spec)
437
438        self._exclude_ops = exclude_ops if exclude_ops else []
439
440        self.proto = self.yaml.get('protocol', 'genetlink')
441        self.msg_id_model = self.yaml['operations'].get('enum-model', 'unified')
442
443        if schema_path is None:
444            schema_path = os.path.dirname(os.path.dirname(spec_path)) + f'/{self.proto}.yaml'
445        if schema_path:
446            global jsonschema
447
448            with open(schema_path, "r") as stream:
449                schema = yaml.safe_load(stream)
450
451            if jsonschema is None:
452                jsonschema = importlib.import_module("jsonschema")
453
454            jsonschema.validate(self.yaml, schema)
455
456        self.attr_sets = collections.OrderedDict()
457        self.sub_msgs = collections.OrderedDict()
458        self.msgs = collections.OrderedDict()
459        self.req_by_value = collections.OrderedDict()
460        self.rsp_by_value = collections.OrderedDict()
461        self.ops = collections.OrderedDict()
462        self.ntfs = collections.OrderedDict()
463        self.consts = collections.OrderedDict()
464        self.mcast_groups = collections.OrderedDict()
465        self.kernel_family = collections.OrderedDict(self.yaml.get('kernel-family', {}))
466
467        last_exception = None
468        while len(self._resolution_list) > 0:
469            resolved = []
470            unresolved = self._resolution_list
471            self._resolution_list = []
472
473            for elem in unresolved:
474                try:
475                    elem.resolve()
476                except (KeyError, AttributeError) as e:
477                    self._resolution_list.append(elem)
478                    last_exception = e
479                    continue
480
481                resolved.append(elem)
482
483            if len(resolved) == 0:
484                raise last_exception
485
486    def new_enum(self, elem):
487        return SpecEnumSet(self, elem)
488
489    def new_attr_set(self, elem):
490        return SpecAttrSet(self, elem)
491
492    def new_struct(self, elem):
493        return SpecStruct(self, elem)
494
495    def new_sub_message(self, elem):
496        return SpecSubMessage(self, elem);
497
498    def new_operation(self, elem, req_val, rsp_val):
499        return SpecOperation(self, elem, req_val, rsp_val)
500
501    def new_mcast_group(self, elem):
502        return SpecMcastGroup(self, elem)
503
504    def add_unresolved(self, elem):
505        self._resolution_list.append(elem)
506
507    def _dictify_ops_unified(self):
508        self.fixed_header = self.yaml['operations'].get('fixed-header')
509        val = 1
510        for elem in self.yaml['operations']['list']:
511            if 'value' in elem:
512                val = elem['value']
513
514            op = self.new_operation(elem, val, val)
515            val += 1
516
517            self.msgs[op.name] = op
518
519    def _dictify_ops_directional(self):
520        self.fixed_header = self.yaml['operations'].get('fixed-header')
521        req_val = rsp_val = 1
522        for elem in self.yaml['operations']['list']:
523            if 'notify' in elem or 'event' in elem:
524                if 'value' in elem:
525                    rsp_val = elem['value']
526                req_val_next = req_val
527                rsp_val_next = rsp_val + 1
528                req_val = None
529            elif 'do' in elem or 'dump' in elem:
530                mode = elem['do'] if 'do' in elem else elem['dump']
531
532                v = mode.get('request', {}).get('value', None)
533                if v:
534                    req_val = v
535                v = mode.get('reply', {}).get('value', None)
536                if v:
537                    rsp_val = v
538
539                rsp_inc = 1 if 'reply' in mode else 0
540                req_val_next = req_val + 1
541                rsp_val_next = rsp_val + rsp_inc
542            else:
543                raise Exception("Can't parse directional ops")
544
545            if req_val == req_val_next:
546                req_val = None
547            if rsp_val == rsp_val_next:
548                rsp_val = None
549
550            skip = False
551            for exclude in self._exclude_ops:
552                skip |= bool(exclude.match(elem['name']))
553            if not skip:
554                op = self.new_operation(elem, req_val, rsp_val)
555
556            req_val = req_val_next
557            rsp_val = rsp_val_next
558
559            self.msgs[op.name] = op
560
561    def find_operation(self, name):
562      """
563      For a given operation name, find and return operation spec.
564      """
565      for op in self.yaml['operations']['list']:
566        if name == op['name']:
567          return op
568      return None
569
570    def resolve(self):
571        self.resolve_up(super())
572
573        definitions = self.yaml.get('definitions', [])
574        for elem in definitions:
575            if elem['type'] == 'enum' or elem['type'] == 'flags':
576                self.consts[elem['name']] = self.new_enum(elem)
577            elif elem['type'] == 'struct':
578                self.consts[elem['name']] = self.new_struct(elem)
579            else:
580                self.consts[elem['name']] = elem
581
582        for elem in self.yaml['attribute-sets']:
583            attr_set = self.new_attr_set(elem)
584            self.attr_sets[elem['name']] = attr_set
585
586        for elem in self.yaml.get('sub-messages', []):
587            sub_message = self.new_sub_message(elem)
588            self.sub_msgs[sub_message.name] = sub_message
589
590        if self.msg_id_model == 'unified':
591            self._dictify_ops_unified()
592        elif self.msg_id_model == 'directional':
593            self._dictify_ops_directional()
594
595        for op in self.msgs.values():
596            if op.req_value is not None:
597                self.req_by_value[op.req_value] = op
598            if op.rsp_value is not None:
599                self.rsp_by_value[op.rsp_value] = op
600            if not op.is_async and 'attribute-set' in op:
601                self.ops[op.name] = op
602            elif op.is_async:
603                self.ntfs[op.name] = op
604
605        mcgs = self.yaml.get('mcast-groups')
606        if mcgs:
607            for elem in mcgs['list']:
608                mcg = self.new_mcast_group(elem)
609                self.mcast_groups[elem['name']] = mcg
610