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