xref: /linux/tools/net/ynl/lib/nlspec.py (revision d7b4e3287ca3a7baf66efd9158498e551a9550da)
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 atttribute 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    """
252    def __init__(self, family, yaml):
253        super().__init__(family, yaml)
254        self.type = yaml['type']
255        self.byte_order = yaml.get('byte-order')
256        self.enum = yaml.get('enum')
257        self.len = yaml.get('len')
258        self.display_hint = yaml.get('display-hint')
259
260
261class SpecStruct(SpecElement):
262    """Netlink struct type
263
264    Represents a C struct definition.
265
266    Attributes:
267        members   ordered list of struct members
268    """
269    def __init__(self, family, yaml):
270        super().__init__(family, yaml)
271
272        self.members = []
273        for member in yaml.get('members', []):
274            self.members.append(self.new_member(family, member))
275
276    def new_member(self, family, elem):
277        return SpecStructMember(family, elem)
278
279    def __iter__(self):
280        yield from self.members
281
282    def items(self):
283        return self.members.items()
284
285
286class SpecSubMessage(SpecElement):
287    """ Netlink sub-message definition
288
289    Represents a set of sub-message formats for polymorphic nlattrs
290    that contain type-specific sub messages.
291
292    Attributes:
293        name     string, name of sub-message definition
294        formats  dict of sub-message formats indexed by match value
295    """
296    def __init__(self, family, yaml):
297        super().__init__(family, yaml)
298
299        self.formats = collections.OrderedDict()
300        for elem in self.yaml['formats']:
301            format = self.new_format(family, elem)
302            self.formats[format.value] = format
303
304    def new_format(self, family, format):
305        return SpecSubMessageFormat(family, format)
306
307
308class SpecSubMessageFormat(SpecElement):
309    """ Netlink sub-message definition
310
311    Represents a set of sub-message formats for polymorphic nlattrs
312    that contain type-specific sub messages.
313
314    Attributes:
315        value         attribute value to match against type selector
316        fixed_header  string, name of fixed header, or None
317        attr_set      string, name of attribute set, or None
318    """
319    def __init__(self, family, yaml):
320        super().__init__(family, yaml)
321
322        self.value = yaml.get('value')
323        self.fixed_header = yaml.get('fixed-header')
324        self.attr_set = yaml.get('attribute-set')
325
326
327class SpecOperation(SpecElement):
328    """Netlink Operation
329
330    Information about a single Netlink operation.
331
332    Attributes:
333        value           numerical ID when serialized, None if req/rsp values differ
334
335        req_value       numerical ID when serialized, user -> kernel
336        rsp_value       numerical ID when serialized, user <- kernel
337        is_call         bool, whether the operation is a call
338        is_async        bool, whether the operation is a notification
339        is_resv         bool, whether the operation does not exist (it's just a reserved ID)
340        attr_set        attribute set name
341        fixed_header    string, optional name of fixed header struct
342
343        yaml            raw spec as loaded from the spec file
344    """
345    def __init__(self, family, yaml, req_value, rsp_value):
346        super().__init__(family, yaml)
347
348        self.value = req_value if req_value == rsp_value else None
349        self.req_value = req_value
350        self.rsp_value = rsp_value
351
352        self.is_call = 'do' in yaml or 'dump' in yaml
353        self.is_async = 'notify' in yaml or 'event' in yaml
354        self.is_resv = not self.is_async and not self.is_call
355        self.fixed_header = self.yaml.get('fixed-header', family.fixed_header)
356
357        # Added by resolve:
358        self.attr_set = None
359        delattr(self, "attr_set")
360
361    def resolve(self):
362        self.resolve_up(super())
363
364        if 'attribute-set' in self.yaml:
365            attr_set_name = self.yaml['attribute-set']
366        elif 'notify' in self.yaml:
367            msg = self.family.msgs[self.yaml['notify']]
368            attr_set_name = msg['attribute-set']
369        elif self.is_resv:
370            attr_set_name = ''
371        else:
372            raise Exception(f"Can't resolve attribute set for op '{self.name}'")
373        if attr_set_name:
374            self.attr_set = self.family.attr_sets[attr_set_name]
375
376
377class SpecMcastGroup(SpecElement):
378    """Netlink Multicast Group
379
380    Information about a multicast group.
381
382    Value is only used for classic netlink families that use the
383    netlink-raw schema. Genetlink families use dynamic ID allocation
384    where the ids of multicast groups get resolved at runtime. Value
385    will be None for genetlink families.
386
387    Attributes:
388        name      name of the mulitcast group
389        value     integer id of this multicast group for netlink-raw or None
390        yaml      raw spec as loaded from the spec file
391    """
392    def __init__(self, family, yaml):
393        super().__init__(family, yaml)
394        self.value = self.yaml.get('value')
395
396
397class SpecFamily(SpecElement):
398    """ Netlink Family Spec class.
399
400    Netlink family information loaded from a spec (e.g. in YAML).
401    Takes care of unfolding implicit information which can be skipped
402    in the spec itself for brevity.
403
404    The class can be used like a dictionary to access the raw spec
405    elements but that's usually a bad idea.
406
407    Attributes:
408        proto     protocol type (e.g. genetlink)
409        msg_id_model   enum-model for operations (unified, directional etc.)
410        license   spec license (loaded from an SPDX tag on the spec)
411
412        attr_sets  dict of attribute sets
413        msgs       dict of all messages (index by name)
414        sub_msgs   dict of all sub messages (index by name)
415        ops        dict of all valid requests / responses
416        ntfs       dict of all async events
417        consts     dict of all constants/enums
418        fixed_header  string, optional name of family default fixed header struct
419        mcast_groups  dict of all multicast groups (index by name)
420    """
421    def __init__(self, spec_path, schema_path=None, exclude_ops=None):
422        with open(spec_path, "r") as stream:
423            prefix = '# SPDX-License-Identifier: '
424            first = stream.readline().strip()
425            if not first.startswith(prefix):
426                raise Exception('SPDX license tag required in the spec')
427            self.license = first[len(prefix):]
428
429            stream.seek(0)
430            spec = yaml.safe_load(stream)
431
432        self._resolution_list = []
433
434        super().__init__(self, spec)
435
436        self._exclude_ops = exclude_ops if exclude_ops else []
437
438        self.proto = self.yaml.get('protocol', 'genetlink')
439        self.msg_id_model = self.yaml['operations'].get('enum-model', 'unified')
440
441        if schema_path is None:
442            schema_path = os.path.dirname(os.path.dirname(spec_path)) + f'/{self.proto}.yaml'
443        if schema_path:
444            global jsonschema
445
446            with open(schema_path, "r") as stream:
447                schema = yaml.safe_load(stream)
448
449            if jsonschema is None:
450                jsonschema = importlib.import_module("jsonschema")
451
452            jsonschema.validate(self.yaml, schema)
453
454        self.attr_sets = collections.OrderedDict()
455        self.sub_msgs = collections.OrderedDict()
456        self.msgs = collections.OrderedDict()
457        self.req_by_value = collections.OrderedDict()
458        self.rsp_by_value = collections.OrderedDict()
459        self.ops = collections.OrderedDict()
460        self.ntfs = collections.OrderedDict()
461        self.consts = collections.OrderedDict()
462        self.mcast_groups = collections.OrderedDict()
463
464        last_exception = None
465        while len(self._resolution_list) > 0:
466            resolved = []
467            unresolved = self._resolution_list
468            self._resolution_list = []
469
470            for elem in unresolved:
471                try:
472                    elem.resolve()
473                except (KeyError, AttributeError) as e:
474                    self._resolution_list.append(elem)
475                    last_exception = e
476                    continue
477
478                resolved.append(elem)
479
480            if len(resolved) == 0:
481                raise last_exception
482
483    def new_enum(self, elem):
484        return SpecEnumSet(self, elem)
485
486    def new_attr_set(self, elem):
487        return SpecAttrSet(self, elem)
488
489    def new_struct(self, elem):
490        return SpecStruct(self, elem)
491
492    def new_sub_message(self, elem):
493        return SpecSubMessage(self, elem);
494
495    def new_operation(self, elem, req_val, rsp_val):
496        return SpecOperation(self, elem, req_val, rsp_val)
497
498    def new_mcast_group(self, elem):
499        return SpecMcastGroup(self, elem)
500
501    def add_unresolved(self, elem):
502        self._resolution_list.append(elem)
503
504    def _dictify_ops_unified(self):
505        self.fixed_header = self.yaml['operations'].get('fixed-header')
506        val = 1
507        for elem in self.yaml['operations']['list']:
508            if 'value' in elem:
509                val = elem['value']
510
511            op = self.new_operation(elem, val, val)
512            val += 1
513
514            self.msgs[op.name] = op
515
516    def _dictify_ops_directional(self):
517        self.fixed_header = self.yaml['operations'].get('fixed-header')
518        req_val = rsp_val = 1
519        for elem in self.yaml['operations']['list']:
520            if 'notify' in elem or 'event' in elem:
521                if 'value' in elem:
522                    rsp_val = elem['value']
523                req_val_next = req_val
524                rsp_val_next = rsp_val + 1
525                req_val = None
526            elif 'do' in elem or 'dump' in elem:
527                mode = elem['do'] if 'do' in elem else elem['dump']
528
529                v = mode.get('request', {}).get('value', None)
530                if v:
531                    req_val = v
532                v = mode.get('reply', {}).get('value', None)
533                if v:
534                    rsp_val = v
535
536                rsp_inc = 1 if 'reply' in mode else 0
537                req_val_next = req_val + 1
538                rsp_val_next = rsp_val + rsp_inc
539            else:
540                raise Exception("Can't parse directional ops")
541
542            if req_val == req_val_next:
543                req_val = None
544            if rsp_val == rsp_val_next:
545                rsp_val = None
546
547            skip = False
548            for exclude in self._exclude_ops:
549                skip |= bool(exclude.match(elem['name']))
550            if not skip:
551                op = self.new_operation(elem, req_val, rsp_val)
552
553            req_val = req_val_next
554            rsp_val = rsp_val_next
555
556            self.msgs[op.name] = op
557
558    def find_operation(self, name):
559      """
560      For a given operation name, find and return operation spec.
561      """
562      for op in self.yaml['operations']['list']:
563        if name == op['name']:
564          return op
565      return None
566
567    def resolve(self):
568        self.resolve_up(super())
569
570        definitions = self.yaml.get('definitions', [])
571        for elem in definitions:
572            if elem['type'] == 'enum' or elem['type'] == 'flags':
573                self.consts[elem['name']] = self.new_enum(elem)
574            elif elem['type'] == 'struct':
575                self.consts[elem['name']] = self.new_struct(elem)
576            else:
577                self.consts[elem['name']] = elem
578
579        for elem in self.yaml['attribute-sets']:
580            attr_set = self.new_attr_set(elem)
581            self.attr_sets[elem['name']] = attr_set
582
583        for elem in self.yaml.get('sub-messages', []):
584            sub_message = self.new_sub_message(elem)
585            self.sub_msgs[sub_message.name] = sub_message
586
587        if self.msg_id_model == 'unified':
588            self._dictify_ops_unified()
589        elif self.msg_id_model == 'directional':
590            self._dictify_ops_directional()
591
592        for op in self.msgs.values():
593            if op.req_value is not None:
594                self.req_by_value[op.req_value] = op
595            if op.rsp_value is not None:
596                self.rsp_by_value[op.rsp_value] = op
597            if not op.is_async and 'attribute-set' in op:
598                self.ops[op.name] = op
599            elif op.is_async:
600                self.ntfs[op.name] = op
601
602        mcgs = self.yaml.get('mcast-groups')
603        if mcgs:
604            for elem in mcgs['list']:
605                mcg = self.new_mcast_group(elem)
606                self.mcast_groups[elem['name']] = mcg
607