xref: /openbmc/linux/tools/net/ynl/lib/nlspec.py (revision d5a05299306227d73b0febba9cecedf88931c507)
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        byte_order  string or None for native byte order
231        enum        string, name of the enum definition
232    """
233    def __init__(self, family, yaml):
234        super().__init__(family, yaml)
235        self.type = yaml['type']
236        self.byte_order = yaml.get('byte-order')
237        self.enum = yaml.get('enum')
238
239
240class SpecStruct(SpecElement):
241    """Netlink struct type
242
243    Represents a C struct definition.
244
245    Attributes:
246        members   ordered list of struct members
247    """
248    def __init__(self, family, yaml):
249        super().__init__(family, yaml)
250
251        self.members = []
252        for member in yaml.get('members', []):
253            self.members.append(self.new_member(family, member))
254
255    def new_member(self, family, elem):
256        return SpecStructMember(family, elem)
257
258    def __iter__(self):
259        yield from self.members
260
261    def items(self):
262        return self.members.items()
263
264
265class SpecOperation(SpecElement):
266    """Netlink Operation
267
268    Information about a single Netlink operation.
269
270    Attributes:
271        value           numerical ID when serialized, None if req/rsp values differ
272
273        req_value       numerical ID when serialized, user -> kernel
274        rsp_value       numerical ID when serialized, user <- kernel
275        is_call         bool, whether the operation is a call
276        is_async        bool, whether the operation is a notification
277        is_resv         bool, whether the operation does not exist (it's just a reserved ID)
278        attr_set        attribute set name
279        fixed_header    string, optional name of fixed header struct
280
281        yaml            raw spec as loaded from the spec file
282    """
283    def __init__(self, family, yaml, req_value, rsp_value):
284        super().__init__(family, yaml)
285
286        self.value = req_value if req_value == rsp_value else None
287        self.req_value = req_value
288        self.rsp_value = rsp_value
289
290        self.is_call = 'do' in yaml or 'dump' in yaml
291        self.is_async = 'notify' in yaml or 'event' in yaml
292        self.is_resv = not self.is_async and not self.is_call
293        self.fixed_header = self.yaml.get('fixed-header', family.fixed_header)
294
295        # Added by resolve:
296        self.attr_set = None
297        delattr(self, "attr_set")
298
299    def resolve(self):
300        self.resolve_up(super())
301
302        if 'attribute-set' in self.yaml:
303            attr_set_name = self.yaml['attribute-set']
304        elif 'notify' in self.yaml:
305            msg = self.family.msgs[self.yaml['notify']]
306            attr_set_name = msg['attribute-set']
307        elif self.is_resv:
308            attr_set_name = ''
309        else:
310            raise Exception(f"Can't resolve attribute set for op '{self.name}'")
311        if attr_set_name:
312            self.attr_set = self.family.attr_sets[attr_set_name]
313
314
315class SpecFamily(SpecElement):
316    """ Netlink Family Spec class.
317
318    Netlink family information loaded from a spec (e.g. in YAML).
319    Takes care of unfolding implicit information which can be skipped
320    in the spec itself for brevity.
321
322    The class can be used like a dictionary to access the raw spec
323    elements but that's usually a bad idea.
324
325    Attributes:
326        proto     protocol type (e.g. genetlink)
327        msg_id_model   enum-model for operations (unified, directional etc.)
328        license   spec license (loaded from an SPDX tag on the spec)
329
330        attr_sets  dict of attribute sets
331        msgs       dict of all messages (index by name)
332        ops        dict of all valid requests / responses
333        ntfs       dict of all async events
334        consts     dict of all constants/enums
335        fixed_header  string, optional name of family default fixed header struct
336    """
337    def __init__(self, spec_path, schema_path=None, exclude_ops=None):
338        with open(spec_path, "r") as stream:
339            prefix = '# SPDX-License-Identifier: '
340            first = stream.readline().strip()
341            if not first.startswith(prefix):
342                raise Exception('SPDX license tag required in the spec')
343            self.license = first[len(prefix):]
344
345            stream.seek(0)
346            spec = yaml.safe_load(stream)
347
348        self._resolution_list = []
349
350        super().__init__(self, spec)
351
352        self._exclude_ops = exclude_ops if exclude_ops else []
353
354        self.proto = self.yaml.get('protocol', 'genetlink')
355        self.msg_id_model = self.yaml['operations'].get('enum-model', 'unified')
356
357        if schema_path is None:
358            schema_path = os.path.dirname(os.path.dirname(spec_path)) + f'/{self.proto}.yaml'
359        if schema_path:
360            global jsonschema
361
362            with open(schema_path, "r") as stream:
363                schema = yaml.safe_load(stream)
364
365            if jsonschema is None:
366                jsonschema = importlib.import_module("jsonschema")
367
368            jsonschema.validate(self.yaml, schema)
369
370        self.attr_sets = collections.OrderedDict()
371        self.msgs = collections.OrderedDict()
372        self.req_by_value = collections.OrderedDict()
373        self.rsp_by_value = collections.OrderedDict()
374        self.ops = collections.OrderedDict()
375        self.ntfs = collections.OrderedDict()
376        self.consts = collections.OrderedDict()
377
378        last_exception = None
379        while len(self._resolution_list) > 0:
380            resolved = []
381            unresolved = self._resolution_list
382            self._resolution_list = []
383
384            for elem in unresolved:
385                try:
386                    elem.resolve()
387                except (KeyError, AttributeError) as e:
388                    self._resolution_list.append(elem)
389                    last_exception = e
390                    continue
391
392                resolved.append(elem)
393
394            if len(resolved) == 0:
395                raise last_exception
396
397    def new_enum(self, elem):
398        return SpecEnumSet(self, elem)
399
400    def new_attr_set(self, elem):
401        return SpecAttrSet(self, elem)
402
403    def new_struct(self, elem):
404        return SpecStruct(self, elem)
405
406    def new_operation(self, elem, req_val, rsp_val):
407        return SpecOperation(self, elem, req_val, rsp_val)
408
409    def add_unresolved(self, elem):
410        self._resolution_list.append(elem)
411
412    def _dictify_ops_unified(self):
413        self.fixed_header = self.yaml['operations'].get('fixed-header')
414        val = 1
415        for elem in self.yaml['operations']['list']:
416            if 'value' in elem:
417                val = elem['value']
418
419            op = self.new_operation(elem, val, val)
420            val += 1
421
422            self.msgs[op.name] = op
423
424    def _dictify_ops_directional(self):
425        self.fixed_header = self.yaml['operations'].get('fixed-header')
426        req_val = rsp_val = 1
427        for elem in self.yaml['operations']['list']:
428            if 'notify' in elem or 'event' in elem:
429                if 'value' in elem:
430                    rsp_val = elem['value']
431                req_val_next = req_val
432                rsp_val_next = rsp_val + 1
433                req_val = None
434            elif 'do' in elem or 'dump' in elem:
435                mode = elem['do'] if 'do' in elem else elem['dump']
436
437                v = mode.get('request', {}).get('value', None)
438                if v:
439                    req_val = v
440                v = mode.get('reply', {}).get('value', None)
441                if v:
442                    rsp_val = v
443
444                rsp_inc = 1 if 'reply' in mode else 0
445                req_val_next = req_val + 1
446                rsp_val_next = rsp_val + rsp_inc
447            else:
448                raise Exception("Can't parse directional ops")
449
450            if req_val == req_val_next:
451                req_val = None
452            if rsp_val == rsp_val_next:
453                rsp_val = None
454
455            skip = False
456            for exclude in self._exclude_ops:
457                skip |= bool(exclude.match(elem['name']))
458            if not skip:
459                op = self.new_operation(elem, req_val, rsp_val)
460
461            req_val = req_val_next
462            rsp_val = rsp_val_next
463
464            self.msgs[op.name] = op
465
466    def find_operation(self, name):
467      """
468      For a given operation name, find and return operation spec.
469      """
470      for op in self.yaml['operations']['list']:
471        if name == op['name']:
472          return op
473      return None
474
475    def resolve(self):
476        self.resolve_up(super())
477
478        definitions = self.yaml.get('definitions', [])
479        for elem in definitions:
480            if elem['type'] == 'enum' or elem['type'] == 'flags':
481                self.consts[elem['name']] = self.new_enum(elem)
482            elif elem['type'] == 'struct':
483                self.consts[elem['name']] = self.new_struct(elem)
484            else:
485                self.consts[elem['name']] = elem
486
487        for elem in self.yaml['attribute-sets']:
488            attr_set = self.new_attr_set(elem)
489            self.attr_sets[elem['name']] = attr_set
490
491        if self.msg_id_model == 'unified':
492            self._dictify_ops_unified()
493        elif self.msg_id_model == 'directional':
494            self._dictify_ops_directional()
495
496        for op in self.msgs.values():
497            if op.req_value is not None:
498                self.req_by_value[op.req_value] = op
499            if op.rsp_value is not None:
500                self.rsp_by_value[op.rsp_value] = op
501            if not op.is_async and 'attribute-set' in op:
502                self.ops[op.name] = op
503            elif op.is_async:
504                self.ntfs[op.name] = op
505