xref: /linux/tools/net/ynl/lib/nlspec.py (revision 2b0cfa6e49566c8fa6759734cf821aa6e8271a9e)
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    """
422    def __init__(self, spec_path, schema_path=None, exclude_ops=None):
423        with open(spec_path, "r") as stream:
424            prefix = '# SPDX-License-Identifier: '
425            first = stream.readline().strip()
426            if not first.startswith(prefix):
427                raise Exception('SPDX license tag required in the spec')
428            self.license = first[len(prefix):]
429
430            stream.seek(0)
431            spec = yaml.safe_load(stream)
432
433        self._resolution_list = []
434
435        super().__init__(self, spec)
436
437        self._exclude_ops = exclude_ops if exclude_ops else []
438
439        self.proto = self.yaml.get('protocol', 'genetlink')
440        self.msg_id_model = self.yaml['operations'].get('enum-model', 'unified')
441
442        if schema_path is None:
443            schema_path = os.path.dirname(os.path.dirname(spec_path)) + f'/{self.proto}.yaml'
444        if schema_path:
445            global jsonschema
446
447            with open(schema_path, "r") as stream:
448                schema = yaml.safe_load(stream)
449
450            if jsonschema is None:
451                jsonschema = importlib.import_module("jsonschema")
452
453            jsonschema.validate(self.yaml, schema)
454
455        self.attr_sets = collections.OrderedDict()
456        self.sub_msgs = collections.OrderedDict()
457        self.msgs = collections.OrderedDict()
458        self.req_by_value = collections.OrderedDict()
459        self.rsp_by_value = collections.OrderedDict()
460        self.ops = collections.OrderedDict()
461        self.ntfs = collections.OrderedDict()
462        self.consts = collections.OrderedDict()
463        self.mcast_groups = collections.OrderedDict()
464
465        last_exception = None
466        while len(self._resolution_list) > 0:
467            resolved = []
468            unresolved = self._resolution_list
469            self._resolution_list = []
470
471            for elem in unresolved:
472                try:
473                    elem.resolve()
474                except (KeyError, AttributeError) as e:
475                    self._resolution_list.append(elem)
476                    last_exception = e
477                    continue
478
479                resolved.append(elem)
480
481            if len(resolved) == 0:
482                raise last_exception
483
484    def new_enum(self, elem):
485        return SpecEnumSet(self, elem)
486
487    def new_attr_set(self, elem):
488        return SpecAttrSet(self, elem)
489
490    def new_struct(self, elem):
491        return SpecStruct(self, elem)
492
493    def new_sub_message(self, elem):
494        return SpecSubMessage(self, elem);
495
496    def new_operation(self, elem, req_val, rsp_val):
497        return SpecOperation(self, elem, req_val, rsp_val)
498
499    def new_mcast_group(self, elem):
500        return SpecMcastGroup(self, elem)
501
502    def add_unresolved(self, elem):
503        self._resolution_list.append(elem)
504
505    def _dictify_ops_unified(self):
506        self.fixed_header = self.yaml['operations'].get('fixed-header')
507        val = 1
508        for elem in self.yaml['operations']['list']:
509            if 'value' in elem:
510                val = elem['value']
511
512            op = self.new_operation(elem, val, val)
513            val += 1
514
515            self.msgs[op.name] = op
516
517    def _dictify_ops_directional(self):
518        self.fixed_header = self.yaml['operations'].get('fixed-header')
519        req_val = rsp_val = 1
520        for elem in self.yaml['operations']['list']:
521            if 'notify' in elem or 'event' in elem:
522                if 'value' in elem:
523                    rsp_val = elem['value']
524                req_val_next = req_val
525                rsp_val_next = rsp_val + 1
526                req_val = None
527            elif 'do' in elem or 'dump' in elem:
528                mode = elem['do'] if 'do' in elem else elem['dump']
529
530                v = mode.get('request', {}).get('value', None)
531                if v:
532                    req_val = v
533                v = mode.get('reply', {}).get('value', None)
534                if v:
535                    rsp_val = v
536
537                rsp_inc = 1 if 'reply' in mode else 0
538                req_val_next = req_val + 1
539                rsp_val_next = rsp_val + rsp_inc
540            else:
541                raise Exception("Can't parse directional ops")
542
543            if req_val == req_val_next:
544                req_val = None
545            if rsp_val == rsp_val_next:
546                rsp_val = None
547
548            skip = False
549            for exclude in self._exclude_ops:
550                skip |= bool(exclude.match(elem['name']))
551            if not skip:
552                op = self.new_operation(elem, req_val, rsp_val)
553
554            req_val = req_val_next
555            rsp_val = rsp_val_next
556
557            self.msgs[op.name] = op
558
559    def find_operation(self, name):
560      """
561      For a given operation name, find and return operation spec.
562      """
563      for op in self.yaml['operations']['list']:
564        if name == op['name']:
565          return op
566      return None
567
568    def resolve(self):
569        self.resolve_up(super())
570
571        definitions = self.yaml.get('definitions', [])
572        for elem in definitions:
573            if elem['type'] == 'enum' or elem['type'] == 'flags':
574                self.consts[elem['name']] = self.new_enum(elem)
575            elif elem['type'] == 'struct':
576                self.consts[elem['name']] = self.new_struct(elem)
577            else:
578                self.consts[elem['name']] = elem
579
580        for elem in self.yaml['attribute-sets']:
581            attr_set = self.new_attr_set(elem)
582            self.attr_sets[elem['name']] = attr_set
583
584        for elem in self.yaml.get('sub-messages', []):
585            sub_message = self.new_sub_message(elem)
586            self.sub_msgs[sub_message.name] = sub_message
587
588        if self.msg_id_model == 'unified':
589            self._dictify_ops_unified()
590        elif self.msg_id_model == 'directional':
591            self._dictify_ops_directional()
592
593        for op in self.msgs.values():
594            if op.req_value is not None:
595                self.req_by_value[op.req_value] = op
596            if op.rsp_value is not None:
597                self.rsp_by_value[op.rsp_value] = op
598            if not op.is_async and 'attribute-set' in op:
599                self.ops[op.name] = op
600            elif op.is_async:
601                self.ntfs[op.name] = op
602
603        mcgs = self.yaml.get('mcast-groups')
604        if mcgs:
605            for elem in mcgs['list']:
606                mcg = self.new_mcast_group(elem)
607                self.mcast_groups[elem['name']] = mcg
608