xref: /linux/tools/net/ynl/lib/nlspec.py (revision 621cde16e49b3ecf7d59a8106a20aaebfb4a59a9)
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        modes           supported operation modes (do, dump, event etc.)
339        is_call         bool, whether the operation is a call
340        is_async        bool, whether the operation is a notification
341        is_resv         bool, whether the operation does not exist (it's just a reserved ID)
342        attr_set        attribute set name
343        fixed_header    string, optional name of fixed header struct
344
345        yaml            raw spec as loaded from the spec file
346    """
347    def __init__(self, family, yaml, req_value, rsp_value):
348        super().__init__(family, yaml)
349
350        self.value = req_value if req_value == rsp_value else None
351        self.req_value = req_value
352        self.rsp_value = rsp_value
353
354        self.modes = yaml.keys() & {'do', 'dump', 'event', 'notify'}
355        self.is_call = 'do' in yaml or 'dump' in yaml
356        self.is_async = 'notify' in yaml or 'event' in yaml
357        self.is_resv = not self.is_async and not self.is_call
358        self.fixed_header = self.yaml.get('fixed-header', family.fixed_header)
359
360        # Added by resolve:
361        self.attr_set = None
362        delattr(self, "attr_set")
363
364    def resolve(self):
365        self.resolve_up(super())
366
367        if 'attribute-set' in self.yaml:
368            attr_set_name = self.yaml['attribute-set']
369        elif 'notify' in self.yaml:
370            msg = self.family.msgs[self.yaml['notify']]
371            attr_set_name = msg['attribute-set']
372        elif self.is_resv:
373            attr_set_name = ''
374        else:
375            raise Exception(f"Can't resolve attribute set for op '{self.name}'")
376        if attr_set_name:
377            self.attr_set = self.family.attr_sets[attr_set_name]
378
379
380class SpecMcastGroup(SpecElement):
381    """Netlink Multicast Group
382
383    Information about a multicast group.
384
385    Value is only used for classic netlink families that use the
386    netlink-raw schema. Genetlink families use dynamic ID allocation
387    where the ids of multicast groups get resolved at runtime. Value
388    will be None for genetlink families.
389
390    Attributes:
391        name      name of the mulitcast group
392        value     integer id of this multicast group for netlink-raw or None
393        yaml      raw spec as loaded from the spec file
394    """
395    def __init__(self, family, yaml):
396        super().__init__(family, yaml)
397        self.value = self.yaml.get('value')
398
399
400class SpecFamily(SpecElement):
401    """ Netlink Family Spec class.
402
403    Netlink family information loaded from a spec (e.g. in YAML).
404    Takes care of unfolding implicit information which can be skipped
405    in the spec itself for brevity.
406
407    The class can be used like a dictionary to access the raw spec
408    elements but that's usually a bad idea.
409
410    Attributes:
411        proto     protocol type (e.g. genetlink)
412        msg_id_model   enum-model for operations (unified, directional etc.)
413        license   spec license (loaded from an SPDX tag on the spec)
414
415        attr_sets  dict of attribute sets
416        msgs       dict of all messages (index by name)
417        sub_msgs   dict of all sub messages (index by name)
418        ops        dict of all valid requests / responses
419        ntfs       dict of all async events
420        consts     dict of all constants/enums
421        fixed_header  string, optional name of family default fixed header struct
422        mcast_groups  dict of all multicast groups (index by name)
423        kernel_family   dict of kernel family attributes
424    """
425    def __init__(self, spec_path, schema_path=None, exclude_ops=None):
426        with open(spec_path, "r") as stream:
427            prefix = '# SPDX-License-Identifier: '
428            first = stream.readline().strip()
429            if not first.startswith(prefix):
430                raise Exception('SPDX license tag required in the spec')
431            self.license = first[len(prefix):]
432
433            stream.seek(0)
434            spec = yaml.safe_load(stream)
435
436        self._resolution_list = []
437
438        super().__init__(self, spec)
439
440        self._exclude_ops = exclude_ops if exclude_ops else []
441
442        self.proto = self.yaml.get('protocol', 'genetlink')
443        self.msg_id_model = self.yaml['operations'].get('enum-model', 'unified')
444
445        if schema_path is None:
446            schema_path = os.path.dirname(os.path.dirname(spec_path)) + f'/{self.proto}.yaml'
447        if schema_path:
448            global jsonschema
449
450            with open(schema_path, "r") as stream:
451                schema = yaml.safe_load(stream)
452
453            if jsonschema is None:
454                jsonschema = importlib.import_module("jsonschema")
455
456            jsonschema.validate(self.yaml, schema)
457
458        self.attr_sets = collections.OrderedDict()
459        self.sub_msgs = collections.OrderedDict()
460        self.msgs = collections.OrderedDict()
461        self.req_by_value = collections.OrderedDict()
462        self.rsp_by_value = collections.OrderedDict()
463        self.ops = collections.OrderedDict()
464        self.ntfs = collections.OrderedDict()
465        self.consts = collections.OrderedDict()
466        self.mcast_groups = collections.OrderedDict()
467        self.kernel_family = collections.OrderedDict(self.yaml.get('kernel-family', {}))
468
469        last_exception = None
470        while len(self._resolution_list) > 0:
471            resolved = []
472            unresolved = self._resolution_list
473            self._resolution_list = []
474
475            for elem in unresolved:
476                try:
477                    elem.resolve()
478                except (KeyError, AttributeError) as e:
479                    self._resolution_list.append(elem)
480                    last_exception = e
481                    continue
482
483                resolved.append(elem)
484
485            if len(resolved) == 0:
486                raise last_exception
487
488    def new_enum(self, elem):
489        return SpecEnumSet(self, elem)
490
491    def new_attr_set(self, elem):
492        return SpecAttrSet(self, elem)
493
494    def new_struct(self, elem):
495        return SpecStruct(self, elem)
496
497    def new_sub_message(self, elem):
498        return SpecSubMessage(self, elem);
499
500    def new_operation(self, elem, req_val, rsp_val):
501        return SpecOperation(self, elem, req_val, rsp_val)
502
503    def new_mcast_group(self, elem):
504        return SpecMcastGroup(self, elem)
505
506    def add_unresolved(self, elem):
507        self._resolution_list.append(elem)
508
509    def _dictify_ops_unified(self):
510        self.fixed_header = self.yaml['operations'].get('fixed-header')
511        val = 1
512        for elem in self.yaml['operations']['list']:
513            if 'value' in elem:
514                val = elem['value']
515
516            op = self.new_operation(elem, val, val)
517            val += 1
518
519            self.msgs[op.name] = op
520
521    def _dictify_ops_directional(self):
522        self.fixed_header = self.yaml['operations'].get('fixed-header')
523        req_val = rsp_val = 1
524        for elem in self.yaml['operations']['list']:
525            if 'notify' in elem or 'event' in elem:
526                if 'value' in elem:
527                    rsp_val = elem['value']
528                req_val_next = req_val
529                rsp_val_next = rsp_val + 1
530                req_val = None
531            elif 'do' in elem or 'dump' in elem:
532                mode = elem['do'] if 'do' in elem else elem['dump']
533
534                v = mode.get('request', {}).get('value', None)
535                if v:
536                    req_val = v
537                v = mode.get('reply', {}).get('value', None)
538                if v:
539                    rsp_val = v
540
541                rsp_inc = 1 if 'reply' in mode else 0
542                req_val_next = req_val + 1
543                rsp_val_next = rsp_val + rsp_inc
544            else:
545                raise Exception("Can't parse directional ops")
546
547            if req_val == req_val_next:
548                req_val = None
549            if rsp_val == rsp_val_next:
550                rsp_val = None
551
552            skip = False
553            for exclude in self._exclude_ops:
554                skip |= bool(exclude.match(elem['name']))
555            if not skip:
556                op = self.new_operation(elem, req_val, rsp_val)
557
558            req_val = req_val_next
559            rsp_val = rsp_val_next
560
561            self.msgs[op.name] = op
562
563    def find_operation(self, name):
564      """
565      For a given operation name, find and return operation spec.
566      """
567      for op in self.yaml['operations']['list']:
568        if name == op['name']:
569          return op
570      return None
571
572    def resolve(self):
573        self.resolve_up(super())
574
575        definitions = self.yaml.get('definitions', [])
576        for elem in definitions:
577            if elem['type'] == 'enum' or elem['type'] == 'flags':
578                self.consts[elem['name']] = self.new_enum(elem)
579            elif elem['type'] == 'struct':
580                self.consts[elem['name']] = self.new_struct(elem)
581            else:
582                self.consts[elem['name']] = elem
583
584        for elem in self.yaml['attribute-sets']:
585            attr_set = self.new_attr_set(elem)
586            self.attr_sets[elem['name']] = attr_set
587
588        for elem in self.yaml.get('sub-messages', []):
589            sub_message = self.new_sub_message(elem)
590            self.sub_msgs[sub_message.name] = sub_message
591
592        if self.msg_id_model == 'unified':
593            self._dictify_ops_unified()
594        elif self.msg_id_model == 'directional':
595            self._dictify_ops_directional()
596
597        for op in self.msgs.values():
598            if op.req_value is not None:
599                self.req_by_value[op.req_value] = op
600            if op.rsp_value is not None:
601                self.rsp_by_value[op.rsp_value] = op
602            if not op.is_async and 'attribute-set' in op:
603                self.ops[op.name] = op
604            elif op.is_async:
605                self.ntfs[op.name] = op
606
607        mcgs = self.yaml.get('mcast-groups')
608        if mcgs:
609            for elem in mcgs['list']:
610                mcg = self.new_mcast_group(elem)
611                self.mcast_groups[elem['name']] = mcg
612