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