diff options
Diffstat (limited to 'tools/net/ynl/lib')
| -rw-r--r-- | tools/net/ynl/lib/__init__.py | 9 | ||||
| -rw-r--r-- | tools/net/ynl/lib/nlspec.py | 218 | ||||
| -rw-r--r-- | tools/net/ynl/lib/ynl.py | 137 |
3 files changed, 310 insertions, 54 deletions
diff --git a/tools/net/ynl/lib/__init__.py b/tools/net/ynl/lib/__init__.py index 3c73f59eabab..4b3797fe784b 100644 --- a/tools/net/ynl/lib/__init__.py +++ b/tools/net/ynl/lib/__init__.py @@ -1,7 +1,8 @@ -# SPDX-License-Identifier: BSD-3-Clause +# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause -from .nlspec import SpecAttr, SpecAttrSet, SpecFamily, SpecOperation +from .nlspec import SpecAttr, SpecAttrSet, SpecEnumEntry, SpecEnumSet, \ + SpecFamily, SpecOperation from .ynl import YnlFamily -__all__ = ["SpecAttr", "SpecAttrSet", "SpecFamily", "SpecOperation", - "YnlFamily"] +__all__ = ["SpecAttr", "SpecAttrSet", "SpecEnumEntry", "SpecEnumSet", + "SpecFamily", "SpecOperation", "YnlFamily"] diff --git a/tools/net/ynl/lib/nlspec.py b/tools/net/ynl/lib/nlspec.py index 71da568e2c28..a0241add3839 100644 --- a/tools/net/ynl/lib/nlspec.py +++ b/tools/net/ynl/lib/nlspec.py @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: BSD-3-Clause +# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause import collections import importlib @@ -57,14 +57,103 @@ class SpecElement: pass +class SpecEnumEntry(SpecElement): + """ Entry within an enum declared in the Netlink spec. + + Attributes: + doc documentation string + enum_set back reference to the enum + value numerical value of this enum (use accessors in most situations!) + + Methods: + raw_value raw value, i.e. the id in the enum, unlike user value which is a mask for flags + user_value user value, same as raw value for enums, for flags it's the mask + """ + def __init__(self, enum_set, yaml, prev, value_start): + if isinstance(yaml, str): + yaml = {'name': yaml} + super().__init__(enum_set.family, yaml) + + self.doc = yaml.get('doc', '') + self.enum_set = enum_set + + if 'value' in yaml: + self.value = yaml['value'] + elif prev: + self.value = prev.value + 1 + else: + self.value = value_start + + def has_doc(self): + return bool(self.doc) + + def raw_value(self): + return self.value + + def user_value(self, as_flags=None): + if self.enum_set['type'] == 'flags' or as_flags: + return 1 << self.value + else: + return self.value + + +class SpecEnumSet(SpecElement): + """ Enum type + + Represents an enumeration (list of numerical constants) + as declared in the "definitions" section of the spec. + + Attributes: + type enum or flags + entries entries by name + entries_by_val entries by value + Methods: + get_mask for flags compute the mask of all defined values + """ + def __init__(self, family, yaml): + super().__init__(family, yaml) + + self.type = yaml['type'] + + prev_entry = None + value_start = self.yaml.get('value-start', 0) + self.entries = dict() + self.entries_by_val = dict() + for entry in self.yaml['entries']: + e = self.new_entry(entry, prev_entry, value_start) + self.entries[e.name] = e + self.entries_by_val[e.raw_value()] = e + prev_entry = e + + def new_entry(self, entry, prev_entry, value_start): + return SpecEnumEntry(self, entry, prev_entry, value_start) + + def has_doc(self): + if 'doc' in self.yaml: + return True + for entry in self.entries.values(): + if entry.has_doc(): + return True + return False + + def get_mask(self, as_flags=None): + mask = 0 + for e in self.entries.values(): + mask += e.user_value(as_flags) + return mask + + class SpecAttr(SpecElement): """ Single Netlink atttribute type Represents a single attribute type within an attr space. Attributes: - value numerical ID when serialized - attr_set Attribute Set containing this attr + value numerical ID when serialized + attr_set Attribute Set containing this attr + is_multi bool, attr may repeat multiple times + struct_name string, name of struct definition + sub_type string, name of sub type """ def __init__(self, family, attr_set, yaml, value): super().__init__(family, yaml) @@ -72,6 +161,9 @@ class SpecAttr(SpecElement): self.value = value self.attr_set = attr_set self.is_multi = yaml.get('multi-attr', False) + self.struct_name = yaml.get('struct') + self.sub_type = yaml.get('sub-type') + self.byte_order = yaml.get('byte-order') class SpecAttrSet(SpecElement): @@ -95,15 +187,22 @@ class SpecAttrSet(SpecElement): self.attrs = collections.OrderedDict() self.attrs_by_val = collections.OrderedDict() - val = 0 - for elem in self.yaml['attributes']: - if 'value' in elem: - val = elem['value'] + if self.subset_of is None: + val = 1 + for elem in self.yaml['attributes']: + if 'value' in elem: + val = elem['value'] - attr = self.new_attr(elem, val) - self.attrs[attr.name] = attr - self.attrs_by_val[attr.value] = attr - val += 1 + attr = self.new_attr(elem, val) + self.attrs[attr.name] = attr + self.attrs_by_val[attr.value] = attr + val += 1 + else: + real_set = family.attr_sets[self.subset_of] + for elem in self.yaml['attributes']: + attr = real_set[elem['name']] + self.attrs[attr.name] = attr + self.attrs_by_val[attr.value] = attr def new_attr(self, elem, value): return SpecAttr(self.family, self, elem, value) @@ -121,22 +220,61 @@ class SpecAttrSet(SpecElement): return self.attrs.items() +class SpecStructMember(SpecElement): + """Struct member attribute + + Represents a single struct member attribute. + + Attributes: + type string, type of the member attribute + """ + def __init__(self, family, yaml): + super().__init__(family, yaml) + self.type = yaml['type'] + + +class SpecStruct(SpecElement): + """Netlink struct type + + Represents a C struct definition. + + Attributes: + members ordered list of struct members + """ + def __init__(self, family, yaml): + super().__init__(family, yaml) + + self.members = [] + for member in yaml.get('members', []): + self.members.append(self.new_member(family, member)) + + def new_member(self, family, elem): + return SpecStructMember(family, elem) + + def __iter__(self): + yield from self.members + + def items(self): + return self.members.items() + + class SpecOperation(SpecElement): """Netlink Operation Information about a single Netlink operation. Attributes: - value numerical ID when serialized, None if req/rsp values differ + value numerical ID when serialized, None if req/rsp values differ - req_value numerical ID when serialized, user -> kernel - rsp_value numerical ID when serialized, user <- kernel - is_call bool, whether the operation is a call - is_async bool, whether the operation is a notification - is_resv bool, whether the operation does not exist (it's just a reserved ID) - attr_set attribute set name + req_value numerical ID when serialized, user -> kernel + rsp_value numerical ID when serialized, user <- kernel + is_call bool, whether the operation is a call + is_async bool, whether the operation is a notification + is_resv bool, whether the operation does not exist (it's just a reserved ID) + attr_set attribute set name + fixed_header string, optional name of fixed header struct - yaml raw spec as loaded from the spec file + yaml raw spec as loaded from the spec file """ def __init__(self, family, yaml, req_value, rsp_value): super().__init__(family, yaml) @@ -148,6 +286,7 @@ class SpecOperation(SpecElement): self.is_call = 'do' in yaml or 'dump' in yaml self.is_async = 'notify' in yaml or 'event' in yaml self.is_resv = not self.is_async and not self.is_call + self.fixed_header = self.yaml.get('fixed-header', family.fixed_header) # Added by resolve: self.attr_set = None @@ -181,14 +320,24 @@ class SpecFamily(SpecElement): Attributes: proto protocol type (e.g. genetlink) + license spec license (loaded from an SPDX tag on the spec) attr_sets dict of attribute sets msgs dict of all messages (index by name) msgs_by_value dict of all messages (indexed by name) ops dict of all valid requests / responses + consts dict of all constants/enums + fixed_header string, optional name of family default fixed header struct """ def __init__(self, spec_path, schema_path=None): with open(spec_path, "r") as stream: + prefix = '# SPDX-License-Identifier: ' + first = stream.readline().strip() + if not first.startswith(prefix): + raise Exception('SPDX license tag required in the spec') + self.license = first[len(prefix):] + + stream.seek(0) spec = yaml.safe_load(stream) self._resolution_list = [] @@ -215,6 +364,7 @@ class SpecFamily(SpecElement): self.req_by_value = collections.OrderedDict() self.rsp_by_value = collections.OrderedDict() self.ops = collections.OrderedDict() + self.consts = collections.OrderedDict() last_exception = None while len(self._resolution_list) > 0: @@ -235,9 +385,15 @@ class SpecFamily(SpecElement): if len(resolved) == 0: raise last_exception + def new_enum(self, elem): + return SpecEnumSet(self, elem) + def new_attr_set(self, elem): return SpecAttrSet(self, elem) + def new_struct(self, elem): + return SpecStruct(self, elem) + def new_operation(self, elem, req_val, rsp_val): return SpecOperation(self, elem, req_val, rsp_val) @@ -245,7 +401,8 @@ class SpecFamily(SpecElement): self._resolution_list.append(elem) def _dictify_ops_unified(self): - val = 0 + self.fixed_header = self.yaml['operations'].get('fixed-header') + val = 1 for elem in self.yaml['operations']['list']: if 'value' in elem: val = elem['value'] @@ -256,7 +413,8 @@ class SpecFamily(SpecElement): self.msgs[op.name] = op def _dictify_ops_directional(self): - req_val = rsp_val = 0 + self.fixed_header = self.yaml['operations'].get('fixed-header') + req_val = rsp_val = 1 for elem in self.yaml['operations']['list']: if 'notify' in elem: if 'value' in elem: @@ -286,9 +444,27 @@ class SpecFamily(SpecElement): self.msgs[op.name] = op + def find_operation(self, name): + """ + For a given operation name, find and return operation spec. + """ + for op in self.yaml['operations']['list']: + if name == op['name']: + return op + return None + def resolve(self): self.resolve_up(super()) + definitions = self.yaml.get('definitions', []) + for elem in definitions: + if elem['type'] == 'enum' or elem['type'] == 'flags': + self.consts[elem['name']] = self.new_enum(elem) + elif elem['type'] == 'struct': + self.consts[elem['name']] = self.new_struct(elem) + else: + self.consts[elem['name']] = elem + for elem in self.yaml['attribute-sets']: attr_set = self.new_attr_set(elem) self.attr_sets[elem['name']] = attr_set diff --git a/tools/net/ynl/lib/ynl.py b/tools/net/ynl/lib/ynl.py index 1c7411ee04dc..aa77bcae4807 100644 --- a/tools/net/ynl/lib/ynl.py +++ b/tools/net/ynl/lib/ynl.py @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: BSD-3-Clause +# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause import functools import os @@ -67,7 +67,20 @@ class Netlink: NLMSGERR_ATTR_MISS_NEST = 6 +class NlError(Exception): + def __init__(self, nl_msg): + self.nl_msg = nl_msg + + def __str__(self): + return f"Netlink error: {os.strerror(-self.nl_msg.error)}\n{self.nl_msg}" + + class NlAttr: + type_formats = { 'u8' : ('B', 1), 's8' : ('b', 1), + 'u16': ('H', 2), 's16': ('h', 2), + 'u32': ('I', 4), 's32': ('i', 4), + 'u64': ('Q', 8), 's64': ('q', 8) } + def __init__(self, raw, offset): self._len, self._type = struct.unpack("HH", raw[offset:offset + 4]) self.type = self._type & ~Netlink.NLA_TYPE_MASK @@ -75,17 +88,25 @@ class NlAttr: self.full_len = (self.payload_len + 3) & ~3 self.raw = raw[offset + 4:offset + self.payload_len] + def format_byte_order(byte_order): + if byte_order: + return ">" if byte_order == "big-endian" else "<" + return "" + def as_u8(self): return struct.unpack("B", self.raw)[0] - def as_u16(self): - return struct.unpack("H", self.raw)[0] + def as_u16(self, byte_order=None): + endian = NlAttr.format_byte_order(byte_order) + return struct.unpack(f"{endian}H", self.raw)[0] - def as_u32(self): - return struct.unpack("I", self.raw)[0] + def as_u32(self, byte_order=None): + endian = NlAttr.format_byte_order(byte_order) + return struct.unpack(f"{endian}I", self.raw)[0] - def as_u64(self): - return struct.unpack("Q", self.raw)[0] + def as_u64(self, byte_order=None): + endian = NlAttr.format_byte_order(byte_order) + return struct.unpack(f"{endian}Q", self.raw)[0] def as_strz(self): return self.raw.decode('ascii')[:-1] @@ -93,6 +114,21 @@ class NlAttr: def as_bin(self): return self.raw + def as_c_array(self, type): + format, _ = self.type_formats[type] + return list({ x[0] for x in struct.iter_unpack(format, self.raw) }) + + def as_struct(self, members): + value = dict() + offset = 0 + for m in members: + # TODO: handle non-scalar members + format, size = self.type_formats[m.type] + decoded = struct.unpack_from(format, self.raw, offset) + offset += size + value[m.name] = decoded[0] + return value + def __repr__(self): return f"[type:{self.type} len:{self._len}] {self.raw}" @@ -200,7 +236,7 @@ def _genl_msg(nl_type, nl_flags, genl_cmd, genl_version, seq=None): if seq is None: seq = random.randint(1, 1024) nlmsg = struct.pack("HHII", nl_type, nl_flags, seq, 0) - genlmsg = struct.pack("bbH", genl_cmd, genl_version, 0) + genlmsg = struct.pack("BBH", genl_cmd, genl_version, 0) return nlmsg + genlmsg @@ -258,14 +294,22 @@ def _genl_load_families(): class GenlMsg: - def __init__(self, nl_msg): + def __init__(self, nl_msg, fixed_header_members=[]): self.nl = nl_msg self.hdr = nl_msg.raw[0:4] - self.raw = nl_msg.raw[4:] + offset = 4 - self.genl_cmd, self.genl_version, _ = struct.unpack("bbH", self.hdr) + self.genl_cmd, self.genl_version, _ = struct.unpack("BBH", self.hdr) + self.fixed_header_attrs = dict() + for m in fixed_header_members: + format, size = NlAttr.type_formats[m.type] + decoded = struct.unpack_from(format, nl_msg.raw, offset) + offset += size + self.fixed_header_attrs[m.name] = decoded[0] + + self.raw = nl_msg.raw[offset:] self.raw_attrs = NlAttrs(self.raw) def __repr__(self): @@ -303,11 +347,6 @@ class YnlFamily(SpecFamily): self.sock.setsockopt(Netlink.SOL_NETLINK, Netlink.NETLINK_CAP_ACK, 1) self.sock.setsockopt(Netlink.SOL_NETLINK, Netlink.NETLINK_EXT_ACK, 1) - self._types = dict() - - for elem in self.yaml.get('definitions', []): - self._types[elem['name']] = elem - self.async_msg_ids = set() self.async_msg_queue = [] @@ -319,7 +358,10 @@ class YnlFamily(SpecFamily): bound_f = functools.partial(self._op, op_name) setattr(self, op.ident_name, bound_f) - self.family = GenlFamily(self.yaml['name']) + try: + self.family = GenlFamily(self.yaml['name']) + except KeyError: + raise Exception(f"Family '{self.yaml['name']}' not supported by the kernel") def ntf_subscribe(self, mcast_name): if mcast_name not in self.family.genl_family['mcast']: @@ -339,8 +381,17 @@ class YnlFamily(SpecFamily): attr_payload += self._add_attr(attr['nested-attributes'], subname, subvalue) elif attr["type"] == 'flag': attr_payload = b'' + elif attr["type"] == 'u8': + attr_payload = struct.pack("B", int(value)) + elif attr["type"] == 'u16': + endian = NlAttr.format_byte_order(attr.byte_order) + attr_payload = struct.pack(f"{endian}H", int(value)) elif attr["type"] == 'u32': - attr_payload = struct.pack("I", int(value)) + endian = NlAttr.format_byte_order(attr.byte_order) + attr_payload = struct.pack(f"{endian}I", int(value)) + elif attr["type"] == 'u64': + endian = NlAttr.format_byte_order(attr.byte_order) + attr_payload = struct.pack(f"{endian}Q", int(value)) elif attr["type"] == 'string': attr_payload = str(value).encode('ascii') + b'\x00' elif attr["type"] == 'binary': @@ -353,19 +404,28 @@ class YnlFamily(SpecFamily): def _decode_enum(self, rsp, attr_spec): raw = rsp[attr_spec['name']] - enum = self._types[attr_spec['enum']] + enum = self.consts[attr_spec['enum']] i = attr_spec.get('value-start', 0) if 'enum-as-flags' in attr_spec and attr_spec['enum-as-flags']: value = set() while raw: if raw & 1: - value.add(enum['entries'][i]) + value.add(enum.entries_by_val[i].name) raw >>= 1 i += 1 else: - value = enum['entries'][raw - i] + value = enum.entries_by_val[raw - i].name rsp[attr_spec['name']] = value + def _decode_binary(self, attr, attr_spec): + if attr_spec.struct_name: + decoded = attr.as_struct(self.consts[attr_spec.struct_name]) + elif attr_spec.sub_type: + decoded = attr.as_c_array(attr_spec.sub_type) + else: + decoded = attr.as_bin() + return decoded + def _decode(self, attrs, space): attr_space = self.attr_sets[space] rsp = dict() @@ -376,14 +436,16 @@ class YnlFamily(SpecFamily): decoded = subdict elif attr_spec['type'] == 'u8': decoded = attr.as_u8() + elif attr_spec['type'] == 'u16': + decoded = attr.as_u16(attr_spec.byte_order) elif attr_spec['type'] == 'u32': - decoded = attr.as_u32() + decoded = attr.as_u32(attr_spec.byte_order) elif attr_spec['type'] == 'u64': - decoded = attr.as_u64() + decoded = attr.as_u64(attr_spec.byte_order) elif attr_spec["type"] == 'string': decoded = attr.as_strz() elif attr_spec["type"] == 'binary': - decoded = attr.as_bin() + decoded = self._decode_binary(attr, attr_spec) elif attr_spec["type"] == 'flag': decoded = True else: @@ -468,6 +530,17 @@ class YnlFamily(SpecFamily): self.handle_ntf(nl_msg, gm) + def operation_do_attributes(self, name): + """ + For a given operation name, find and return a supported + set of attributes (as a dict). + """ + op = self.find_operation(name) + if not op: + return None + + return op['do']['request']['attributes'].copy() + def _op(self, method, vals, dump=False): op = self.ops[method] @@ -477,6 +550,13 @@ class YnlFamily(SpecFamily): req_seq = random.randint(1024, 65535) msg = _genl_msg(self.family.family_id, nl_flags, op.req_value, 1, req_seq) + fixed_header_members = [] + if op.fixed_header: + fixed_header_members = self.consts[op.fixed_header].members + for m in fixed_header_members: + value = vals.pop(m.name) + format, _ = NlAttr.type_formats[m.type] + msg += struct.pack(format, value) for name, value in vals.items(): msg += self._add_attr(op.attr_set.name, name, value) msg = _genl_msg_finalize(msg) @@ -493,9 +573,7 @@ class YnlFamily(SpecFamily): self._decode_extack(msg, op.attr_set, nl_msg.extack) if nl_msg.error: - print("Netlink error:", os.strerror(-nl_msg.error)) - print(nl_msg) - return + raise NlError(nl_msg) if nl_msg.done: if nl_msg.extack: print("Netlink warning:") @@ -503,7 +581,7 @@ class YnlFamily(SpecFamily): done = True break - gm = GenlMsg(nl_msg) + gm = GenlMsg(nl_msg, fixed_header_members) # Check if this is a reply to our request if nl_msg.nl_seq != req_seq or gm.genl_cmd != op.rsp_value: if gm.genl_cmd in self.async_msg_ids: @@ -513,7 +591,8 @@ class YnlFamily(SpecFamily): print('Unexpected message: ' + repr(gm)) continue - rsp.append(self._decode(gm.raw_attrs, op.attr_set.name)) + rsp.append(self._decode(gm.raw_attrs, op.attr_set.name) + | gm.fixed_header_attrs) if not rsp: return None |