From 758d29fb3a8b3c756b4e4e0aa9b32ca8cfaf3feb Mon Sep 17 00:00:00 2001 From: Donald Hunter Date: Sun, 19 Mar 2023 19:37:58 +0000 Subject: tools: ynl: Fix genlmsg header encoding formats The pack strings use 'b' signed char for cmd and version but struct genlmsghdr defines them as unsigned char. Use 'B' instead. Fixes: 4e4480e89c47 ("tools: ynl: move the cli and netlink code around") Signed-off-by: Donald Hunter Link: https://lore.kernel.org/r/20230319193803.97453-1-donald.hunter@gmail.com Signed-off-by: Jakub Kicinski --- tools/net/ynl/lib/ynl.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'tools/net/ynl/lib/ynl.py') diff --git a/tools/net/ynl/lib/ynl.py b/tools/net/ynl/lib/ynl.py index 90764a83c646..32536e1f9064 100644 --- a/tools/net/ynl/lib/ynl.py +++ b/tools/net/ynl/lib/ynl.py @@ -200,7 +200,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 @@ -264,7 +264,7 @@ class GenlMsg: self.hdr = nl_msg.raw[0:4] self.raw = nl_msg.raw[4:] - self.genl_cmd, self.genl_version, _ = struct.unpack("bbH", self.hdr) + self.genl_cmd, self.genl_version, _ = struct.unpack("BBH", self.hdr) self.raw_attrs = NlAttrs(self.raw) @@ -358,7 +358,7 @@ class YnlFamily(SpecFamily): 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(self, attrs, space): -- cgit From 8da3a5598f75cd0361e11b6d74697084380eb4b0 Mon Sep 17 00:00:00 2001 From: Jiri Pirko Date: Wed, 22 Mar 2023 16:42:42 +0100 Subject: ynl: allow to encode u8 attr Playing with dpll netlink, I came across following issue: $ sudo ./tools/net/ynl/cli.py --spec Documentation/netlink/specs/dpll.yaml --do pin-set --json '{"id": 0, "pin-idx": 1, "pin-state": 1}' Traceback (most recent call last): File "tools/net/ynl/cli.py", line 52, in main() File "tools/net/ynl/cli.py", line 40, in main reply = ynl.do(args.do, attrs) File "tools/net/ynl/lib/ynl.py", line 520, in do return self._op(method, vals) File "tools/net/ynl/lib/ynl.py", line 476, in _op msg += self._add_attr(op.attr_set.name, name, value) File "tools/net/ynl/lib/ynl.py", line 344, in _add_attr raise Exception(f'Unknown type at {space} {name} {value} {attr["type"]}') Exception: Unknown type at dpll pin-state 1 u8 I'm not that familiar with ynl code, but from a quick peek, I suspect that couple other types are missing for both encoding and decoding. Ignoring those here as I'm scratching my local itch only. Fix the issue by adding u8 attr packing. Signed-off-by: Jiri Pirko Link: https://lore.kernel.org/r/20230322154242.1739136-1-jiri@resnulli.us Signed-off-by: Jakub Kicinski --- tools/net/ynl/lib/ynl.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'tools/net/ynl/lib/ynl.py') diff --git a/tools/net/ynl/lib/ynl.py b/tools/net/ynl/lib/ynl.py index 90764a83c646..bcb798c7734d 100644 --- a/tools/net/ynl/lib/ynl.py +++ b/tools/net/ynl/lib/ynl.py @@ -334,6 +334,8 @@ 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"] == 'u32': attr_payload = struct.pack("I", int(value)) elif attr["type"] == 'string': -- cgit From dd3a7d58dcc2434632bed31683ff134b7d6d1da4 Mon Sep 17 00:00:00 2001 From: Michal Michalik Date: Fri, 24 Mar 2023 18:52:58 +0100 Subject: tools: ynl: Add missing types to encode/decode While testing the tool I noticed we miss the u16 type on payload create. On the code inspection it turned out we miss also u64 - add them. We also miss the decoding of u16 despite the fact `NlAttr` class supports it - add it. Signed-off-by: Michal Michalik Acked-by: Jakub Kicinski Signed-off-by: David S. Miller --- tools/net/ynl/lib/ynl.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'tools/net/ynl/lib/ynl.py') diff --git a/tools/net/ynl/lib/ynl.py b/tools/net/ynl/lib/ynl.py index 98ecfdb44a83..7eaf066b115e 100644 --- a/tools/net/ynl/lib/ynl.py +++ b/tools/net/ynl/lib/ynl.py @@ -336,8 +336,12 @@ class YnlFamily(SpecFamily): attr_payload = b'' elif attr["type"] == 'u8': attr_payload = struct.pack("B", int(value)) + elif attr["type"] == 'u16': + attr_payload = struct.pack("H", int(value)) elif attr["type"] == 'u32': attr_payload = struct.pack("I", int(value)) + elif attr["type"] == 'u64': + attr_payload = struct.pack("Q", int(value)) elif attr["type"] == 'string': attr_payload = str(value).encode('ascii') + b'\x00' elif attr["type"] == 'binary': @@ -373,6 +377,8 @@ class YnlFamily(SpecFamily): decoded = subdict elif attr_spec['type'] == 'u8': decoded = attr.as_u8() + elif attr_spec['type'] == 'u16': + decoded = attr.as_u16() elif attr_spec['type'] == 'u32': decoded = attr.as_u32() elif attr_spec['type'] == 'u64': -- cgit From b423c3c86325192259380ac870aafd370a683e73 Mon Sep 17 00:00:00 2001 From: Donald Hunter Date: Mon, 27 Mar 2023 09:31:33 +0100 Subject: tools: ynl: Add C array attribute decoding to ynl Add support for decoding C arrays from binay blobs in genetlink-legacy messages. Signed-off-by: Donald Hunter Reviewed-by: Jakub Kicinski Signed-off-by: Jakub Kicinski --- tools/net/ynl/lib/ynl.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) (limited to 'tools/net/ynl/lib/ynl.py') diff --git a/tools/net/ynl/lib/ynl.py b/tools/net/ynl/lib/ynl.py index 7eaf066b115e..eada229402fa 100644 --- a/tools/net/ynl/lib/ynl.py +++ b/tools/net/ynl/lib/ynl.py @@ -68,6 +68,11 @@ class Netlink: 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 @@ -93,6 +98,10 @@ 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 __repr__(self): return f"[type:{self.type} len:{self._len}] {self.raw}" @@ -367,6 +376,13 @@ class YnlFamily(SpecFamily): value = enum.entries_by_val[raw - i].name rsp[attr_spec['name']] = value + def _decode_binary(self, attr, attr_spec): + if 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() @@ -386,7 +402,7 @@ class YnlFamily(SpecFamily): 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: -- cgit From 2607191395bd4db544db05452625cd7e98bc0848 Mon Sep 17 00:00:00 2001 From: Donald Hunter Date: Mon, 27 Mar 2023 09:31:34 +0100 Subject: tools: ynl: Add struct attr decoding to ynl Add support for decoding attributes that contain C structs. Signed-off-by: Donald Hunter Signed-off-by: Jakub Kicinski --- tools/net/ynl/lib/ynl.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) (limited to 'tools/net/ynl/lib/ynl.py') diff --git a/tools/net/ynl/lib/ynl.py b/tools/net/ynl/lib/ynl.py index eada229402fa..63af3bd9787d 100644 --- a/tools/net/ynl/lib/ynl.py +++ b/tools/net/ynl/lib/ynl.py @@ -102,6 +102,17 @@ class NlAttr: 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}" @@ -377,7 +388,9 @@ class YnlFamily(SpecFamily): rsp[attr_spec['name']] = value def _decode_binary(self, attr, attr_spec): - if attr_spec.sub_type: + 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() -- cgit From f036d936ca57e8bc1f39b92cadfbac27095dc4e7 Mon Sep 17 00:00:00 2001 From: Donald Hunter Date: Mon, 27 Mar 2023 09:31:35 +0100 Subject: tools: ynl: Add fixed-header support to ynl Add support for netlink families that add an optional fixed header structure after the genetlink header and before any attributes. The fixed-header can be specified on a per op basis, or once for all operations, which serves as a default value that can be overridden. Signed-off-by: Donald Hunter Signed-off-by: Jakub Kicinski --- tools/net/ynl/lib/ynl.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) (limited to 'tools/net/ynl/lib/ynl.py') diff --git a/tools/net/ynl/lib/ynl.py b/tools/net/ynl/lib/ynl.py index 63af3bd9787d..ec40918152e1 100644 --- a/tools/net/ynl/lib/ynl.py +++ b/tools/net/ynl/lib/ynl.py @@ -278,14 +278,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.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): @@ -509,6 +517,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) @@ -535,7 +550,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: @@ -545,7 +560,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 -- cgit From 9f7cc57fe5508c495d3a75efd7f942aeec0013e0 Mon Sep 17 00:00:00 2001 From: Stanislav Fomichev Date: Wed, 29 Mar 2023 15:16:52 -0700 Subject: tools: ynl: support byte-order in cli Used by ethtool spec. Signed-off-by: Stanislav Fomichev Signed-off-by: Jakub Kicinski --- tools/net/ynl/lib/ynl.py | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) (limited to 'tools/net/ynl/lib/ynl.py') diff --git a/tools/net/ynl/lib/ynl.py b/tools/net/ynl/lib/ynl.py index ec40918152e1..8778994d40c0 100644 --- a/tools/net/ynl/lib/ynl.py +++ b/tools/net/ynl/lib/ynl.py @@ -80,17 +80,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] @@ -365,11 +373,14 @@ class YnlFamily(SpecFamily): elif attr["type"] == 'u8': attr_payload = struct.pack("B", int(value)) elif attr["type"] == 'u16': - attr_payload = struct.pack("H", int(value)) + 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': - attr_payload = struct.pack("Q", int(value)) + 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': @@ -415,11 +426,11 @@ class YnlFamily(SpecFamily): elif attr_spec['type'] == 'u8': decoded = attr.as_u8() elif attr_spec['type'] == 'u16': - decoded = attr.as_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': -- cgit From 48993e22d23ae1bda1db3616f5d9baa4e7b18d35 Mon Sep 17 00:00:00 2001 From: Stanislav Fomichev Date: Wed, 29 Mar 2023 15:16:54 -0700 Subject: tools: ynl: replace print with NlError Instead of dumping the error on the stdout, make the callee and opportunity to decide what to do with it. This is mostly for the ethtool testing. Signed-off-by: Stanislav Fomichev Signed-off-by: Jakub Kicinski --- tools/net/ynl/lib/ynl.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) (limited to 'tools/net/ynl/lib/ynl.py') diff --git a/tools/net/ynl/lib/ynl.py b/tools/net/ynl/lib/ynl.py index 8778994d40c0..373c0edb5f83 100644 --- a/tools/net/ynl/lib/ynl.py +++ b/tools/net/ynl/lib/ynl.py @@ -67,6 +67,14 @@ 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), @@ -551,9 +559,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:") -- cgit From f3d07b02b2b8eba5b0e168405614e15cd6617a43 Mon Sep 17 00:00:00 2001 From: Stanislav Fomichev Date: Wed, 29 Mar 2023 15:16:55 -0700 Subject: tools: ynl: ethtool testing tool This is what I've been using to see whether the spec makes sense. A small subset of getters (mostly the unprivileged ones) is implemented. Some setters (channels) also work. Setters for messages with bitmasks are not implemented. Initially I was trying to make this tool look 1:1 like real ethtool, but eventually gave up :-) Sample output: $ ./tools/net/ynl/ethtool enp0s31f6 Settings for enp0s31f6: Supported ports: [ TP ] Supported link modes: 10baseT/Half 10baseT/Full 100baseT/Half 100baseT/Full 1000baseT/Full Supported pause frame use: no Supports auto-negotiation: yes Supported FEC modes: Not reported Speed: Unknown! Duplex: Unknown! (255) Auto-negotiation: on Port: Twisted Pair PHYAD: 2 Transceiver: Internal MDI-X: Unknown (auto) Current message level: drv probe link Link detected: no Signed-off-by: Stanislav Fomichev Signed-off-by: Jakub Kicinski --- tools/net/ynl/lib/ynl.py | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'tools/net/ynl/lib/ynl.py') diff --git a/tools/net/ynl/lib/ynl.py b/tools/net/ynl/lib/ynl.py index 373c0edb5f83..7690e0b0cb3f 100644 --- a/tools/net/ynl/lib/ynl.py +++ b/tools/net/ynl/lib/ynl.py @@ -527,6 +527,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] -- cgit From ebe3bdc4359e07b4e347af8d5324fb436302bbe1 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Fri, 7 Apr 2023 07:56:09 -0700 Subject: tools: ynl: throw a more meaningful exception if family not supported cli.py currently throws a pure KeyError if kernel doesn't support a netlink family. Users who did not write ynl (hah) may waste their time investigating what's wrong with the Python code. Improve the error message: Traceback (most recent call last): File "/home/kicinski/devel/linux/tools/net/ynl/lib/ynl.py", line 362, in __init__ self.family = GenlFamily(self.yaml['name']) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/kicinski/devel/linux/tools/net/ynl/lib/ynl.py", line 331, in __init__ self.genl_family = genl_family_name_to_id[family_name] ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^ KeyError: 'netdev' During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/home/kicinski/devel/linux/./tools/net/ynl/cli.py", line 52, in main() File "/home/kicinski/devel/linux/./tools/net/ynl/cli.py", line 31, in main ynl = YnlFamily(args.spec, args.schema) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/kicinski/devel/linux/tools/net/ynl/lib/ynl.py", line 364, in __init__ raise Exception(f"Family '{self.yaml['name']}' not supported by the kernel") Exception: Family 'netdev' not supported by the kernel Signed-off-by: Jakub Kicinski Link: https://lore.kernel.org/r/20230407145609.297525-1-kuba@kernel.org Signed-off-by: Paolo Abeni --- tools/net/ynl/lib/ynl.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'tools/net/ynl/lib/ynl.py') diff --git a/tools/net/ynl/lib/ynl.py b/tools/net/ynl/lib/ynl.py index 7690e0b0cb3f..aa77bcae4807 100644 --- a/tools/net/ynl/lib/ynl.py +++ b/tools/net/ynl/lib/ynl.py @@ -358,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']: -- cgit From 081e8df6819997eae236f75dd52f0c147c4be939 Mon Sep 17 00:00:00 2001 From: Jakub Kicinski Date: Wed, 24 May 2023 10:07:12 -0700 Subject: tools: ynl: avoid dict errors on older Python versions Python 3.9.0 or newer supports combining dicts() with |, but older versions of Python are still used in the wild (e.g. on CentOS 8, which goes EoL May 31, 2024). With Python 3.6.8 we get: TypeError: unsupported operand type(s) for |: 'dict' and 'dict' Use older syntax. Tested with non-legacy families only. Fixes: f036d936ca57 ("tools: ynl: Add fixed-header support to ynl") Reviewed-by: Simon Horman Reviewed-by: Donald Hunter Tested-by: Donald Hunter Link: https://lore.kernel.org/r/20230524170712.2036128-1-kuba@kernel.org Signed-off-by: Jakub Kicinski --- tools/net/ynl/lib/ynl.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'tools/net/ynl/lib/ynl.py') diff --git a/tools/net/ynl/lib/ynl.py b/tools/net/ynl/lib/ynl.py index aa77bcae4807..3144f33196be 100644 --- a/tools/net/ynl/lib/ynl.py +++ b/tools/net/ynl/lib/ynl.py @@ -591,8 +591,9 @@ class YnlFamily(SpecFamily): print('Unexpected message: ' + repr(gm)) continue - rsp.append(self._decode(gm.raw_attrs, op.attr_set.name) - | gm.fixed_header_attrs) + rsp_msg = self._decode(gm.raw_attrs, op.attr_set.name) + rsp_msg.update(gm.fixed_header_attrs) + rsp.append(rsp_msg) if not rsp: return None -- cgit