xref: /openbmc/linux/tools/net/ynl/lib/nlspec.py (revision 1d54134d)
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 SpecFamily(SpecElement):
326    """ Netlink Family Spec class.
327
328    Netlink family information loaded from a spec (e.g. in YAML).
329    Takes care of unfolding implicit information which can be skipped
330    in the spec itself for brevity.
331
332    The class can be used like a dictionary to access the raw spec
333    elements but that's usually a bad idea.
334
335    Attributes:
336        proto     protocol type (e.g. genetlink)
337        msg_id_model   enum-model for operations (unified, directional etc.)
338        license   spec license (loaded from an SPDX tag on the spec)
339
340        attr_sets  dict of attribute sets
341        msgs       dict of all messages (index by name)
342        ops        dict of all valid requests / responses
343        ntfs       dict of all async events
344        consts     dict of all constants/enums
345        fixed_header  string, optional name of family default fixed header struct
346    """
347    def __init__(self, spec_path, schema_path=None, exclude_ops=None):
348        with open(spec_path, "r") as stream:
349            prefix = '# SPDX-License-Identifier: '
350            first = stream.readline().strip()
351            if not first.startswith(prefix):
352                raise Exception('SPDX license tag required in the spec')
353            self.license = first[len(prefix):]
354
355            stream.seek(0)
356            spec = yaml.safe_load(stream)
357
358        self._resolution_list = []
359
360        super().__init__(self, spec)
361
362        self._exclude_ops = exclude_ops if exclude_ops else []
363
364        self.proto = self.yaml.get('protocol', 'genetlink')
365        self.msg_id_model = self.yaml['operations'].get('enum-model', 'unified')
366
367        if schema_path is None:
368            schema_path = os.path.dirname(os.path.dirname(spec_path)) + f'/{self.proto}.yaml'
369        if schema_path:
370            global jsonschema
371
372            with open(schema_path, "r") as stream:
373                schema = yaml.safe_load(stream)
374
375            if jsonschema is None:
376                jsonschema = importlib.import_module("jsonschema")
377
378            jsonschema.validate(self.yaml, schema)
379
380        self.attr_sets = collections.OrderedDict()
381        self.msgs = collections.OrderedDict()
382        self.req_by_value = collections.OrderedDict()
383        self.rsp_by_value = collections.OrderedDict()
384        self.ops = collections.OrderedDict()
385        self.ntfs = collections.OrderedDict()
386        self.consts = collections.OrderedDict()
387
388        last_exception = None
389        while len(self._resolution_list) > 0:
390            resolved = []
391            unresolved = self._resolution_list
392            self._resolution_list = []
393
394            for elem in unresolved:
395                try:
396                    elem.resolve()
397                except (KeyError, AttributeError) as e:
398                    self._resolution_list.append(elem)
399                    last_exception = e
400                    continue
401
402                resolved.append(elem)
403
404            if len(resolved) == 0:
405                raise last_exception
406
407    def new_enum(self, elem):
408        return SpecEnumSet(self, elem)
409
410    def new_attr_set(self, elem):
411        return SpecAttrSet(self, elem)
412
413    def new_struct(self, elem):
414        return SpecStruct(self, elem)
415
416    def new_operation(self, elem, req_val, rsp_val):
417        return SpecOperation(self, elem, req_val, rsp_val)
418
419    def add_unresolved(self, elem):
420        self._resolution_list.append(elem)
421
422    def _dictify_ops_unified(self):
423        self.fixed_header = self.yaml['operations'].get('fixed-header')
424        val = 1
425        for elem in self.yaml['operations']['list']:
426            if 'value' in elem:
427                val = elem['value']
428
429            op = self.new_operation(elem, val, val)
430            val += 1
431
432            self.msgs[op.name] = op
433
434    def _dictify_ops_directional(self):
435        self.fixed_header = self.yaml['operations'].get('fixed-header')
436        req_val = rsp_val = 1
437        for elem in self.yaml['operations']['list']:
438            if 'notify' in elem or 'event' in elem:
439                if 'value' in elem:
440                    rsp_val = elem['value']
441                req_val_next = req_val
442                rsp_val_next = rsp_val + 1
443                req_val = None
444            elif 'do' in elem or 'dump' in elem:
445                mode = elem['do'] if 'do' in elem else elem['dump']
446
447                v = mode.get('request', {}).get('value', None)
448                if v:
449                    req_val = v
450                v = mode.get('reply', {}).get('value', None)
451                if v:
452                    rsp_val = v
453
454                rsp_inc = 1 if 'reply' in mode else 0
455                req_val_next = req_val + 1
456                rsp_val_next = rsp_val + rsp_inc
457            else:
458                raise Exception("Can't parse directional ops")
459
460            if req_val == req_val_next:
461                req_val = None
462            if rsp_val == rsp_val_next:
463                rsp_val = None
464
465            skip = False
466            for exclude in self._exclude_ops:
467                skip |= bool(exclude.match(elem['name']))
468            if not skip:
469                op = self.new_operation(elem, req_val, rsp_val)
470
471            req_val = req_val_next
472            rsp_val = rsp_val_next
473
474            self.msgs[op.name] = op
475
476    def find_operation(self, name):
477      """
478      For a given operation name, find and return operation spec.
479      """
480      for op in self.yaml['operations']['list']:
481        if name == op['name']:
482          return op
483      return None
484
485    def resolve(self):
486        self.resolve_up(super())
487
488        definitions = self.yaml.get('definitions', [])
489        for elem in definitions:
490            if elem['type'] == 'enum' or elem['type'] == 'flags':
491                self.consts[elem['name']] = self.new_enum(elem)
492            elif elem['type'] == 'struct':
493                self.consts[elem['name']] = self.new_struct(elem)
494            else:
495                self.consts[elem['name']] = elem
496
497        for elem in self.yaml['attribute-sets']:
498            attr_set = self.new_attr_set(elem)
499            self.attr_sets[elem['name']] = attr_set
500
501        if self.msg_id_model == 'unified':
502            self._dictify_ops_unified()
503        elif self.msg_id_model == 'directional':
504            self._dictify_ops_directional()
505
506        for op in self.msgs.values():
507            if op.req_value is not None:
508                self.req_by_value[op.req_value] = op
509            if op.rsp_value is not None:
510                self.rsp_by_value[op.rsp_value] = op
511            if not op.is_async and 'attribute-set' in op:
512                self.ops[op.name] = op
513            elif op.is_async:
514                self.ntfs[op.name] = op
515