xref: /linux/tools/net/ynl/pyynl/lib/ynl.py (revision b74e71b6a986650ea04d99ef443c6b3b18da52c5)
1# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
2#
3# pylint: disable=missing-class-docstring, missing-function-docstring
4# pylint: disable=too-many-branches, too-many-locals, too-many-instance-attributes
5# pylint: disable=too-many-lines
6
7"""
8YAML Netlink Library
9
10An implementation of the genetlink and raw netlink protocols.
11"""
12
13from collections import namedtuple
14from enum import Enum
15import functools
16import os
17import random
18import socket
19import struct
20from struct import Struct
21import sys
22import ipaddress
23import uuid
24import queue
25import selectors
26import time
27
28from .nlspec import SpecFamily
29
30#
31# Generic Netlink code which should really be in some library, but I can't quickly find one.
32#
33
34
35class YnlException(Exception):
36    pass
37
38
39# pylint: disable=too-few-public-methods
40class Netlink:
41    # Netlink socket
42    SOL_NETLINK = 270
43
44    NETLINK_ADD_MEMBERSHIP = 1
45    NETLINK_LISTEN_ALL_NSID = 8
46    NETLINK_CAP_ACK = 10
47    NETLINK_EXT_ACK = 11
48    NETLINK_GET_STRICT_CHK = 12
49
50    # Netlink message
51    NLMSG_ERROR = 2
52    NLMSG_DONE = 3
53
54    NLM_F_REQUEST = 1
55    NLM_F_ACK = 4
56    NLM_F_ROOT = 0x100
57    NLM_F_MATCH = 0x200
58
59    NLM_F_REPLACE = 0x100
60    NLM_F_EXCL = 0x200
61    NLM_F_CREATE = 0x400
62    NLM_F_APPEND = 0x800
63
64    NLM_F_CAPPED = 0x100
65    NLM_F_ACK_TLVS = 0x200
66
67    NLM_F_DUMP = NLM_F_ROOT | NLM_F_MATCH
68
69    NLA_F_NESTED = 0x8000
70    NLA_F_NET_BYTEORDER = 0x4000
71
72    NLA_TYPE_MASK = NLA_F_NESTED | NLA_F_NET_BYTEORDER
73
74    # Genetlink defines
75    NETLINK_GENERIC = 16
76
77    GENL_ID_CTRL = 0x10
78
79    # nlctrl
80    CTRL_CMD_GETFAMILY = 3
81    CTRL_CMD_GETPOLICY = 10
82
83    CTRL_ATTR_FAMILY_ID = 1
84    CTRL_ATTR_FAMILY_NAME = 2
85    CTRL_ATTR_MAXATTR = 5
86    CTRL_ATTR_MCAST_GROUPS = 7
87    CTRL_ATTR_POLICY = 8
88    CTRL_ATTR_OP_POLICY = 9
89    CTRL_ATTR_OP = 10
90
91    CTRL_ATTR_MCAST_GRP_NAME = 1
92    CTRL_ATTR_MCAST_GRP_ID = 2
93
94    CTRL_ATTR_POLICY_DO = 1
95    CTRL_ATTR_POLICY_DUMP = 2
96
97    # Extack types
98    NLMSGERR_ATTR_MSG = 1
99    NLMSGERR_ATTR_OFFS = 2
100    NLMSGERR_ATTR_COOKIE = 3
101    NLMSGERR_ATTR_POLICY = 4
102    NLMSGERR_ATTR_MISS_TYPE = 5
103    NLMSGERR_ATTR_MISS_NEST = 6
104
105    # Policy types
106    NL_POLICY_TYPE_ATTR_TYPE = 1
107    NL_POLICY_TYPE_ATTR_MIN_VALUE_S = 2
108    NL_POLICY_TYPE_ATTR_MAX_VALUE_S = 3
109    NL_POLICY_TYPE_ATTR_MIN_VALUE_U = 4
110    NL_POLICY_TYPE_ATTR_MAX_VALUE_U = 5
111    NL_POLICY_TYPE_ATTR_MIN_LENGTH = 6
112    NL_POLICY_TYPE_ATTR_MAX_LENGTH = 7
113    NL_POLICY_TYPE_ATTR_POLICY_IDX = 8
114    NL_POLICY_TYPE_ATTR_POLICY_MAXTYPE = 9
115    NL_POLICY_TYPE_ATTR_BITFIELD32_MASK = 10
116    NL_POLICY_TYPE_ATTR_PAD = 11
117    NL_POLICY_TYPE_ATTR_MASK = 12
118
119    AttrType = Enum('AttrType', ['flag', 'u8', 'u16', 'u32', 'u64',
120                                  's8', 's16', 's32', 's64',
121                                  'binary', 'string', 'nul-string',
122                                  'nested', 'nested-array',
123                                  'bitfield32', 'sint', 'uint'])
124
125class NlError(Exception):
126    def __init__(self, nl_msg):
127        self.nl_msg = nl_msg
128        self.error = -nl_msg.error
129
130    def __str__(self):
131        msg = "Netlink error: "
132
133        extack = self.nl_msg.extack.copy() if self.nl_msg.extack else {}
134        if 'msg' in extack:
135            msg += extack['msg'] + ': '
136            del extack['msg']
137        msg += os.strerror(self.error)
138        if extack:
139            msg += ' ' + str(extack)
140        return msg
141
142
143class ConfigError(Exception):
144    pass
145
146
147class NlPolicy:
148    """Kernel policy for one mode (do or dump) of one operation.
149
150    Returned by YnlFamily.get_policy(). Attributes of the policy
151    are accessible as attributes of the object. Nested policies
152    can be accessed indexing the object like a dictionary::
153
154        pol = ynl.get_policy('page-pool-stats-get', 'do')
155        pol['info'].type            # 'nested'
156        pol['info']['id'].type      # 'uint'
157        pol['info']['id'].min_value # 1
158
159    Each policy entry always has a 'type' attribute (e.g. u32, string,
160    nested). Optional attributes depending on the 'type': min-value,
161    max-value, min-length, max-length, mask.
162
163    Policies can form infinite nesting loops. These loops are trimmed
164    when policy is converted to a dict with pol.to_dict().
165    """
166    def __init__(self, ynl, policy_idx, policy_table, attr_set, props=None):
167        self._policy_idx = policy_idx
168        self._policy_table = policy_table
169        self._ynl = ynl
170        self._props = props or {}
171        self._entries = {}
172        self._cache = {}
173        if policy_idx is not None and policy_idx in policy_table:
174            for attr_id, decoded in policy_table[policy_idx].items():
175                if attr_set and attr_id in attr_set.attrs_by_val:
176                    spec = attr_set.attrs_by_val[attr_id]
177                    name = spec['name']
178                else:
179                    spec = None
180                    name = f'attr-{attr_id}'
181                self._entries[name] = (spec, decoded)
182
183    def __getitem__(self, name):
184        """Descend into a nested policy by attribute name."""
185        if name not in self._cache:
186            spec, decoded = self._entries[name]
187            props = dict(decoded)
188            child_idx = None
189            child_set = None
190            if 'policy-idx' in props:
191                child_idx = props.pop('policy-idx')
192                if spec and 'nested-attributes' in spec.yaml:
193                    child_set = self._ynl.attr_sets[spec.yaml['nested-attributes']]
194            self._cache[name] = NlPolicy(self._ynl, child_idx,
195                                         self._policy_table,
196                                         child_set, props)
197        return self._cache[name]
198
199    def __getattr__(self, name):
200        """Access this policy entry's own properties (type, min-value, etc.).
201
202        Underscores in the name are converted to dashes, so that
203        pol.min_value looks up "min-value".
204        """
205        key = name.replace('_', '-')
206        try:
207            # Hack for level-0 which we still want to have .type but we don't
208            # want type to pointlessly show up in the dict / JSON form.
209            if not self._props and name == "type":
210                return "nested"
211            return self._props[key]
212        except KeyError:
213            raise AttributeError(name)
214
215    def get(self, name, default=None):
216        """Look up a child policy entry by attribute name, with a default."""
217        try:
218            return self[name]
219        except KeyError:
220            return default
221
222    def __contains__(self, name):
223        return name in self._entries
224
225    def __len__(self):
226        return len(self._entries)
227
228    def __iter__(self):
229        return iter(self._entries)
230
231    def keys(self):
232        """Return attribute names accepted by this policy."""
233        return self._entries.keys()
234
235    def to_dict(self, seen=None):
236        """Convert to a plain dict, suitable for JSON serialization.
237
238        Nested NlPolicy objects are expanded recursively. Cyclic
239        references are trimmed (resolved to just {"type": "nested"}).
240        """
241        if seen is None:
242            seen = set()
243        result = dict(self._props)
244        if self._policy_idx is not None:
245            if self._policy_idx not in seen:
246                seen = seen | {self._policy_idx}
247                children = {}
248                for name in self:
249                    children[name] = self[name].to_dict(seen)
250                if self._props:
251                    result['policy'] = children
252                else:
253                    result = children
254        return result
255
256    def __repr__(self):
257        return repr(self.to_dict())
258
259
260class NlAttr:
261    ScalarFormat = namedtuple('ScalarFormat', ['native', 'big', 'little'])
262    type_formats = {
263        'u8' : ScalarFormat(Struct('B'), Struct("B"),  Struct("B")),
264        's8' : ScalarFormat(Struct('b'), Struct("b"),  Struct("b")),
265        'u16': ScalarFormat(Struct('H'), Struct(">H"), Struct("<H")),
266        's16': ScalarFormat(Struct('h'), Struct(">h"), Struct("<h")),
267        'u32': ScalarFormat(Struct('I'), Struct(">I"), Struct("<I")),
268        's32': ScalarFormat(Struct('i'), Struct(">i"), Struct("<i")),
269        'u64': ScalarFormat(Struct('Q'), Struct(">Q"), Struct("<Q")),
270        's64': ScalarFormat(Struct('q'), Struct(">q"), Struct("<q"))
271    }
272
273    def __init__(self, raw, offset):
274        self._len, self._type = struct.unpack("HH", raw[offset : offset + 4])
275        self.type = self._type & ~Netlink.NLA_TYPE_MASK
276        self.is_nest = self._type & Netlink.NLA_F_NESTED
277        self.payload_len = self._len
278        self.full_len = (self.payload_len + 3) & ~3
279        self.raw = raw[offset + 4 : offset + self.payload_len]
280
281    @classmethod
282    def get_format(cls, attr_type, byte_order=None):
283        format_ = cls.type_formats[attr_type]
284        if byte_order:
285            return format_.big if byte_order == "big-endian" \
286                else format_.little
287        return format_.native
288
289    def as_scalar(self, attr_type, byte_order=None):
290        format_ = self.get_format(attr_type, byte_order)
291        return format_.unpack(self.raw)[0]
292
293    def as_auto_scalar(self, attr_type, byte_order=None):
294        if len(self.raw) != 4 and len(self.raw) != 8:
295            raise YnlException(f"Auto-scalar len payload be 4 or 8 bytes, got {len(self.raw)}")
296        real_type = attr_type[0] + str(len(self.raw) * 8)
297        format_ = self.get_format(real_type, byte_order)
298        return format_.unpack(self.raw)[0]
299
300    def as_strz(self):
301        return self.raw.decode('ascii')[:-1]
302
303    def as_bin(self):
304        return self.raw
305
306    def as_c_array(self, c_type):
307        format_ = self.get_format(c_type)
308        return [ x[0] for x in format_.iter_unpack(self.raw) ]
309
310    def __repr__(self):
311        return f"[type:{self.type} len:{self._len}] {self.raw}"
312
313
314class NlAttrs:
315    def __init__(self, msg, offset=0):
316        self.attrs = []
317
318        while offset < len(msg):
319            attr = NlAttr(msg, offset)
320            offset += attr.full_len
321            self.attrs.append(attr)
322
323    def __iter__(self):
324        yield from self.attrs
325
326    def __repr__(self):
327        msg = ''
328        for a in self.attrs:
329            if msg:
330                msg += '\n'
331            msg += repr(a)
332        return msg
333
334
335class NlMsg:
336    def __init__(self, msg, offset, attr_space=None):
337        self.hdr = msg[offset : offset + 16]
338
339        self.nl_len, self.nl_type, self.nl_flags, self.nl_seq, self.nl_portid = \
340            struct.unpack("IHHII", self.hdr)
341
342        self.raw = msg[offset + 16 : offset + self.nl_len]
343
344        self.error = 0
345        self.done = 0
346
347        extack_off = None
348        if self.nl_type == Netlink.NLMSG_ERROR:
349            self.error = struct.unpack("i", self.raw[0:4])[0]
350            self.done = 1
351            extack_off = 20
352        elif self.nl_type == Netlink.NLMSG_DONE:
353            self.error = struct.unpack("i", self.raw[0:4])[0]
354            self.done = 1
355            extack_off = 4
356
357        self.extack = None
358        if self.nl_flags & Netlink.NLM_F_ACK_TLVS and extack_off:
359            self.extack = {}
360            extack_attrs = NlAttrs(self.raw[extack_off:])
361            for extack in extack_attrs:
362                if extack.type == Netlink.NLMSGERR_ATTR_MSG:
363                    self.extack['msg'] = extack.as_strz()
364                elif extack.type == Netlink.NLMSGERR_ATTR_MISS_TYPE:
365                    self.extack['miss-type'] = extack.as_scalar('u32')
366                elif extack.type == Netlink.NLMSGERR_ATTR_MISS_NEST:
367                    self.extack['miss-nest'] = extack.as_scalar('u32')
368                elif extack.type == Netlink.NLMSGERR_ATTR_OFFS:
369                    self.extack['bad-attr-offs'] = extack.as_scalar('u32')
370                elif extack.type == Netlink.NLMSGERR_ATTR_POLICY:
371                    self.extack['policy'] = _genl_decode_policy(extack.raw)
372                else:
373                    if 'unknown' not in self.extack:
374                        self.extack['unknown'] = []
375                    self.extack['unknown'].append(extack)
376
377            if attr_space:
378                self.annotate_extack(attr_space)
379
380    def annotate_extack(self, attr_space):
381        """ Make extack more human friendly with attribute information """
382
383        # We don't have the ability to parse nests yet, so only do global
384        if 'miss-type' in self.extack and 'miss-nest' not in self.extack:
385            miss_type = self.extack['miss-type']
386            if miss_type in attr_space.attrs_by_val:
387                spec = attr_space.attrs_by_val[miss_type]
388                self.extack['miss-type'] = spec['name']
389                if 'doc' in spec:
390                    self.extack['miss-type-doc'] = spec['doc']
391
392    def cmd(self):
393        return self.nl_type
394
395    def __repr__(self):
396        msg = (f"nl_len = {self.nl_len} ({len(self.raw)}) "
397               f"nl_flags = 0x{self.nl_flags:x} nl_type = {self.nl_type}")
398        if self.error:
399            msg += '\n\terror: ' + str(self.error)
400        if self.extack:
401            msg += '\n\textack: ' + repr(self.extack)
402        return msg
403
404
405# pylint: disable=too-few-public-methods
406class NlMsgs:
407    def __init__(self, data):
408        self.msgs = []
409
410        offset = 0
411        while offset < len(data):
412            msg = NlMsg(data, offset)
413            offset += msg.nl_len
414            self.msgs.append(msg)
415
416    def __iter__(self):
417        yield from self.msgs
418
419
420def _genl_msg(nl_type, nl_flags, genl_cmd, genl_version, seq=None):
421    # we prepend length in _genl_msg_finalize()
422    if seq is None:
423        seq = random.randint(1, 1024)
424    nlmsg = struct.pack("HHII", nl_type, nl_flags, seq, 0)
425    genlmsg = struct.pack("BBH", genl_cmd, genl_version, 0)
426    return nlmsg + genlmsg
427
428
429def _genl_msg_finalize(msg):
430    return struct.pack("I", len(msg) + 4) + msg
431
432
433def _genl_decode_policy(raw):
434    policy = {}
435    for attr in NlAttrs(raw):
436        if attr.type == Netlink.NL_POLICY_TYPE_ATTR_TYPE:
437            type_ = attr.as_scalar('u32')
438            policy['type'] = Netlink.AttrType(type_).name
439        elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MIN_VALUE_S:
440            policy['min-value'] = attr.as_scalar('s64')
441        elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MAX_VALUE_S:
442            policy['max-value'] = attr.as_scalar('s64')
443        elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MIN_VALUE_U:
444            policy['min-value'] = attr.as_scalar('u64')
445        elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MAX_VALUE_U:
446            policy['max-value'] = attr.as_scalar('u64')
447        elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MIN_LENGTH:
448            policy['min-length'] = attr.as_scalar('u32')
449        elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MAX_LENGTH:
450            policy['max-length'] = attr.as_scalar('u32')
451        elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_POLICY_IDX:
452            policy['policy-idx'] = attr.as_scalar('u32')
453        elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_BITFIELD32_MASK:
454            policy['bitfield32-mask'] = attr.as_scalar('u32')
455        elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MASK:
456            policy['mask'] = attr.as_scalar('u64')
457    return policy
458
459
460# pylint: disable=too-many-nested-blocks
461def _genl_load_families():
462    genl_family_name_to_id = {}
463
464    with socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, Netlink.NETLINK_GENERIC) as sock:
465        sock.setsockopt(Netlink.SOL_NETLINK, Netlink.NETLINK_CAP_ACK, 1)
466
467        msg = _genl_msg(Netlink.GENL_ID_CTRL,
468                        Netlink.NLM_F_REQUEST | Netlink.NLM_F_ACK | Netlink.NLM_F_DUMP,
469                        Netlink.CTRL_CMD_GETFAMILY, 1)
470        msg = _genl_msg_finalize(msg)
471
472        sock.send(msg, 0)
473
474        while True:
475            reply = sock.recv(128 * 1024)
476            nms = NlMsgs(reply)
477            for nl_msg in nms:
478                if nl_msg.error:
479                    raise YnlException(f"Netlink error: {nl_msg.error}")
480                if nl_msg.done:
481                    return genl_family_name_to_id
482
483                gm = GenlMsg(nl_msg)
484                fam = {}
485                for attr in NlAttrs(gm.raw):
486                    if attr.type == Netlink.CTRL_ATTR_FAMILY_ID:
487                        fam['id'] = attr.as_scalar('u16')
488                    elif attr.type == Netlink.CTRL_ATTR_FAMILY_NAME:
489                        fam['name'] = attr.as_strz()
490                    elif attr.type == Netlink.CTRL_ATTR_MAXATTR:
491                        fam['maxattr'] = attr.as_scalar('u32')
492                    elif attr.type == Netlink.CTRL_ATTR_MCAST_GROUPS:
493                        fam['mcast'] = {}
494                        for entry in NlAttrs(attr.raw):
495                            mcast_name = None
496                            mcast_id = None
497                            for entry_attr in NlAttrs(entry.raw):
498                                if entry_attr.type == Netlink.CTRL_ATTR_MCAST_GRP_NAME:
499                                    mcast_name = entry_attr.as_strz()
500                                elif entry_attr.type == Netlink.CTRL_ATTR_MCAST_GRP_ID:
501                                    mcast_id = entry_attr.as_scalar('u32')
502                            if mcast_name and mcast_id is not None:
503                                fam['mcast'][mcast_name] = mcast_id
504                if 'name' in fam and 'id' in fam:
505                    genl_family_name_to_id[fam['name']] = fam
506
507
508# pylint: disable=too-many-nested-blocks
509def _genl_policy_dump(family_id, op):
510    op_policy = {}
511    policy_table = {}
512
513    with socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, Netlink.NETLINK_GENERIC) as sock:
514        sock.setsockopt(Netlink.SOL_NETLINK, Netlink.NETLINK_CAP_ACK, 1)
515
516        msg = _genl_msg(Netlink.GENL_ID_CTRL,
517                        Netlink.NLM_F_REQUEST | Netlink.NLM_F_ACK | Netlink.NLM_F_DUMP,
518                        Netlink.CTRL_CMD_GETPOLICY, 1)
519        msg += struct.pack('HHHxx', 6, Netlink.CTRL_ATTR_FAMILY_ID, family_id)
520        msg += struct.pack('HHI', 8, Netlink.CTRL_ATTR_OP, op)
521        msg = _genl_msg_finalize(msg)
522
523        sock.send(msg, 0)
524
525        while True:
526            reply = sock.recv(128 * 1024)
527            nms = NlMsgs(reply)
528            for nl_msg in nms:
529                if nl_msg.error:
530                    raise YnlException(f"Netlink error: {nl_msg.error}")
531                if nl_msg.done:
532                    return op_policy, policy_table
533
534                gm = GenlMsg(nl_msg)
535                for attr in NlAttrs(gm.raw):
536                    if attr.type == Netlink.CTRL_ATTR_OP_POLICY:
537                        for op_attr in NlAttrs(attr.raw):
538                            for method_attr in NlAttrs(op_attr.raw):
539                                if method_attr.type == Netlink.CTRL_ATTR_POLICY_DO:
540                                    op_policy['do'] = method_attr.as_scalar('u32')
541                                elif method_attr.type == Netlink.CTRL_ATTR_POLICY_DUMP:
542                                    op_policy['dump'] = method_attr.as_scalar('u32')
543                    elif attr.type == Netlink.CTRL_ATTR_POLICY:
544                        for pidx_attr in NlAttrs(attr.raw):
545                            policy_idx = pidx_attr.type
546                            for aid_attr in NlAttrs(pidx_attr.raw):
547                                attr_id = aid_attr.type
548                                decoded = _genl_decode_policy(aid_attr.raw)
549                                if policy_idx not in policy_table:
550                                    policy_table[policy_idx] = {}
551                                policy_table[policy_idx][attr_id] = decoded
552
553
554class GenlMsg:
555    def __init__(self, nl_msg):
556        self.nl = nl_msg
557        self.genl_cmd, self.genl_version, _ = struct.unpack_from("BBH", nl_msg.raw, 0)
558        self.raw = nl_msg.raw[4:]
559        self.raw_attrs = []
560
561    def cmd(self):
562        return self.genl_cmd
563
564    def __repr__(self):
565        msg = repr(self.nl)
566        msg += f"\tgenl_cmd = {self.genl_cmd} genl_ver = {self.genl_version}\n"
567        for a in self.raw_attrs:
568            msg += '\t\t' + repr(a) + '\n'
569        return msg
570
571
572class NetlinkProtocol:
573    def __init__(self, family_name, proto_num):
574        self.family_name = family_name
575        self.proto_num = proto_num
576
577    def _message(self, nl_type, nl_flags, seq=None):
578        if seq is None:
579            seq = random.randint(1, 1024)
580        nlmsg = struct.pack("HHII", nl_type, nl_flags, seq, 0)
581        return nlmsg
582
583    def message(self, flags, command, _version, seq=None):
584        return self._message(command, flags, seq)
585
586    def _decode(self, nl_msg):
587        return nl_msg
588
589    def decode(self, ynl, nl_msg, op):
590        msg = self._decode(nl_msg)
591        if op is None:
592            op = ynl.rsp_by_value[msg.cmd()]
593        fixed_header_size = ynl.struct_size(op.fixed_header)
594        msg.raw_attrs = NlAttrs(msg.raw, fixed_header_size)
595        return msg
596
597    def get_mcast_id(self, mcast_name, mcast_groups):
598        if mcast_name not in mcast_groups:
599            raise YnlException(f'Multicast group "{mcast_name}" not present in the spec')
600        return mcast_groups[mcast_name].value
601
602    def msghdr_size(self):
603        return 16
604
605
606class GenlProtocol(NetlinkProtocol):
607    genl_family_name_to_id = {}
608
609    def __init__(self, family_name):
610        super().__init__(family_name, Netlink.NETLINK_GENERIC)
611
612        if not GenlProtocol.genl_family_name_to_id:
613            GenlProtocol.genl_family_name_to_id = _genl_load_families()
614
615        self.genl_family = GenlProtocol.genl_family_name_to_id[family_name]
616        self.family_id = GenlProtocol.genl_family_name_to_id[family_name]['id']
617
618    def message(self, flags, command, version, seq=None):
619        nlmsg = self._message(self.family_id, flags, seq)
620        genlmsg = struct.pack("BBH", command, version, 0)
621        return nlmsg + genlmsg
622
623    def _decode(self, nl_msg):
624        return GenlMsg(nl_msg)
625
626    def get_mcast_id(self, mcast_name, mcast_groups):
627        if mcast_name not in self.genl_family['mcast']:
628            raise YnlException(f'Multicast group "{mcast_name}" not present in the family')
629        return self.genl_family['mcast'][mcast_name]
630
631    def msghdr_size(self):
632        return super().msghdr_size() + 4
633
634
635# pylint: disable=too-few-public-methods
636class SpaceAttrs:
637    SpecValuesPair = namedtuple('SpecValuesPair', ['spec', 'values'])
638
639    def __init__(self, attr_space, attrs, outer = None):
640        outer_scopes = outer.scopes if outer else []
641        inner_scope = self.SpecValuesPair(attr_space, attrs)
642        self.scopes = [inner_scope] + outer_scopes
643
644    def lookup(self, name):
645        for scope in self.scopes:
646            if name in scope.spec:
647                if name in scope.values:
648                    return scope.values[name]
649                spec_name = scope.spec.yaml['name']
650                raise YnlException(
651                    f"No value for '{name}' in attribute space '{spec_name}'")
652        raise YnlException(f"Attribute '{name}' not defined in any attribute-set")
653
654
655#
656# YNL implementation details.
657#
658
659
660class YnlFamily(SpecFamily):
661    """
662    YNL family -- a Netlink interface built from a YAML spec.
663
664    Primary use of the class is to execute Netlink commands:
665
666      ynl.<op_name>(attrs, ...)
667
668    By default this will execute the <op_name> as "do", pass dump=True
669    to perform a dump operation.
670
671    ynl.<op_name> is a shorthand / convenience wrapper for the following
672    methods which take the op_name as a string:
673
674      ynl.do(op_name, attrs, flags=None) -- execute a do operation
675      ynl.dump(op_name, attrs)           -- execute a dump operation
676      ynl.do_multi(ops)                  -- batch multiple do operations
677
678    The flags argument in ynl.do() allows passing in extra NLM_F_* flags
679    which may be necessary for old families.
680
681    Notification API:
682
683      ynl.ntf_subscribe(mcast_name)      -- join a multicast group
684      ynl.ntf_listen_all_nsid()          -- listen on all netns
685      ynl.ntf_bind(addr=(0, 0))          -- bind socket for unicast notifications
686      ynl.check_ntf()                    -- drain pending notifications
687      ynl.poll_ntf(duration=None)        -- yield notifications
688
689    Policy introspection allows querying validation criteria from the running
690    kernel. Allows checking whether kernel supports a given attribute or value.
691
692      ynl.get_policy(op_name, mode)      -- query kernel policy for an op
693    """
694    def __init__(self, def_path, schema=None, process_unknown=False,
695                 recv_size=0):
696        super().__init__(def_path, schema)
697
698        self.include_raw = False
699        self.process_unknown = process_unknown
700
701        try:
702            if self.proto == "netlink-raw":
703                self.nlproto = NetlinkProtocol(self.yaml['name'],
704                                               self.yaml['protonum'])
705            else:
706                self.nlproto = GenlProtocol(self.yaml['name'])
707        except KeyError as err:
708            raise YnlException(f"Family '{self.yaml['name']}' not supported by the kernel") from err
709
710        self._recv_dbg = False
711        # Note that netlink will use conservative (min) message size for
712        # the first dump recv() on the socket, our setting will only matter
713        # from the second recv() on.
714        self._recv_size = recv_size if recv_size else 131072
715        # Netlink will always allocate at least PAGE_SIZE - sizeof(skb_shinfo)
716        # for a message, so smaller receive sizes will lead to truncation.
717        # Note that the min size for other families may be larger than 4k!
718        if self._recv_size < 4000:
719            raise ConfigError()
720
721        self.sock = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, self.nlproto.proto_num)
722        self.sock.setsockopt(Netlink.SOL_NETLINK, Netlink.NETLINK_CAP_ACK, 1)
723        self.sock.setsockopt(Netlink.SOL_NETLINK, Netlink.NETLINK_EXT_ACK, 1)
724        self.sock.setsockopt(Netlink.SOL_NETLINK, Netlink.NETLINK_GET_STRICT_CHK, 1)
725
726        self.async_msg_ids = set()
727        self.async_msg_queue = queue.Queue()
728
729        for msg in self.msgs.values():
730            if msg.is_async:
731                self.async_msg_ids.add(msg.rsp_value)
732
733        for op_name, op in self.ops.items():
734            bound_f = functools.partial(self._op, op_name)
735            setattr(self, op.ident_name, bound_f)
736
737    def close(self):
738        if self.sock is not None:
739            self.sock.close()
740            self.sock = None
741
742    def __enter__(self):
743        return self
744
745    def __exit__(self, exc_type, exc, tb):
746        self.close()
747
748    def ntf_subscribe(self, mcast_name):
749        mcast_id = self.nlproto.get_mcast_id(mcast_name, self.mcast_groups)
750        self.sock.bind((0, 0))
751        self.sock.setsockopt(Netlink.SOL_NETLINK, Netlink.NETLINK_ADD_MEMBERSHIP,
752                             mcast_id)
753
754    def ntf_listen_all_nsid(self):
755        """Enable NETLINK_LISTEN_ALL_NSID to receive notifications from all
756        namespaces that have an nsid mapped in the current one."""
757        self.sock.setsockopt(Netlink.SOL_NETLINK,
758                             Netlink.NETLINK_LISTEN_ALL_NSID, 1)
759
760    @staticmethod
761    def _decode_nsid(ancdata):
762        for cmsg_level, cmsg_type, cmsg_data in ancdata:
763            if (cmsg_level == Netlink.SOL_NETLINK and
764                    cmsg_type == Netlink.NETLINK_LISTEN_ALL_NSID):
765                nsid = struct.unpack('i', cmsg_data)[0]
766                if nsid >= 0:
767                    return nsid
768                return None
769        return None
770
771    def ntf_bind(self, addr=(0, 0)):
772        """Bind socket for receiving unicast notifications."""
773        self.sock.bind(addr)
774
775    def set_recv_dbg(self, enabled):
776        self._recv_dbg = enabled
777
778    def _recv_dbg_print(self, reply, nl_msgs):
779        if not self._recv_dbg:
780            return
781        print("Recv: read", len(reply), "bytes,",
782              len(nl_msgs.msgs), "messages", file=sys.stderr)
783        for nl_msg in nl_msgs:
784            print("  ", nl_msg, file=sys.stderr)
785
786    def _encode_enum(self, attr_spec, value):
787        enum = self.consts[attr_spec['enum']]
788        if enum.type == 'flags' or attr_spec.get('enum-as-flags', False):
789            scalar = 0
790            if isinstance(value, str):
791                value = [value]
792            for single_value in value:
793                scalar += enum.entries[single_value].user_value(as_flags = True)
794            return scalar
795        return enum.entries[value].user_value()
796
797    def _get_scalar(self, attr_spec, value):
798        try:
799            return int(value)
800        except (ValueError, TypeError) as e:
801            if 'enum' in attr_spec:
802                return self._encode_enum(attr_spec, value)
803            if attr_spec.display_hint:
804                return self._from_string(value, attr_spec)
805            raise e
806
807    # pylint: disable=too-many-statements
808    def _add_attr(self, space, name, value, search_attrs):
809        try:
810            attr = self.attr_sets[space][name]
811        except KeyError as err:
812            raise YnlException(f"Space '{space}' has no attribute '{name}'") from err
813        nl_type = attr.value
814
815        if attr.is_multi and isinstance(value, list):
816            attr_payload = b''
817            for subvalue in value:
818                attr_payload += self._add_attr(space, name, subvalue, search_attrs)
819            return attr_payload
820
821        if attr["type"] == 'nest':
822            nl_type |= Netlink.NLA_F_NESTED
823            sub_space = attr['nested-attributes']
824            attr_payload = self._add_nest_attrs(value, sub_space, search_attrs)
825        elif attr['type'] == 'indexed-array' and attr['sub-type'] == 'nest':
826            nl_type |= Netlink.NLA_F_NESTED
827            sub_space = attr['nested-attributes']
828            attr_payload = self._encode_indexed_array(value, sub_space,
829                                                      search_attrs)
830        elif attr["type"] == 'flag':
831            if not value:
832                # If value is absent or false then skip attribute creation.
833                return b''
834            attr_payload = b''
835        elif attr["type"] == 'string':
836            attr_payload = str(value).encode('ascii') + b'\x00'
837        elif attr["type"] == 'binary':
838            if value is None:
839                attr_payload = b''
840            elif isinstance(value, bytes):
841                attr_payload = value
842            elif isinstance(value, str):
843                if attr.display_hint:
844                    attr_payload = self._from_string(value, attr)
845                else:
846                    attr_payload = bytes.fromhex(value)
847            elif isinstance(value, dict) and attr.struct_name:
848                attr_payload = self._encode_struct(attr.struct_name, value)
849            elif isinstance(value, list) and attr.sub_type in NlAttr.type_formats:
850                format_ = NlAttr.get_format(attr.sub_type)
851                attr_payload = b''.join([format_.pack(x) for x in value])
852            else:
853                raise YnlException(f'Unknown type for binary attribute, value: {value}')
854        elif attr['type'] in NlAttr.type_formats or attr.is_auto_scalar:
855            scalar = self._get_scalar(attr, value)
856            if attr.is_auto_scalar:
857                attr_type = attr["type"][0] + ('32' if scalar.bit_length() <= 32 else '64')
858            else:
859                attr_type = attr["type"]
860            format_ = NlAttr.get_format(attr_type, attr.byte_order)
861            attr_payload = format_.pack(scalar)
862        elif attr['type'] in "bitfield32":
863            scalar_value = self._get_scalar(attr, value["value"])
864            scalar_selector = self._get_scalar(attr, value["selector"])
865            attr_payload = struct.pack("II", scalar_value, scalar_selector)
866        elif attr['type'] == 'sub-message':
867            msg_format, _ = self._resolve_selector(attr, search_attrs)
868            attr_payload = b''
869            if msg_format.fixed_header:
870                attr_payload += self._encode_struct(msg_format.fixed_header, value)
871            if msg_format.attr_set:
872                if msg_format.attr_set in self.attr_sets:
873                    nl_type |= Netlink.NLA_F_NESTED
874                    sub_attrs = SpaceAttrs(msg_format.attr_set, value, search_attrs)
875                    for subname, subvalue in value.items():
876                        attr_payload += self._add_attr(msg_format.attr_set,
877                                                       subname, subvalue, sub_attrs)
878                else:
879                    raise YnlException(f"Unknown attribute-set '{msg_format.attr_set}'")
880        else:
881            raise YnlException(f'Unknown type at {space} {name} {value} {attr["type"]}')
882
883        return self._add_attr_raw(nl_type, attr_payload)
884
885    def _add_attr_raw(self, nl_type, attr_payload):
886        pad = b'\x00' * ((4 - len(attr_payload) % 4) % 4)
887        return struct.pack('HH', len(attr_payload) + 4, nl_type) + attr_payload + pad
888
889    def _add_nest_attrs(self, value, sub_space, search_attrs):
890        sub_attrs = SpaceAttrs(self.attr_sets[sub_space], value, search_attrs)
891        attr_payload = b''
892        for subname, subvalue in value.items():
893            attr_payload += self._add_attr(sub_space, subname, subvalue,
894                                           sub_attrs)
895        return attr_payload
896
897    def _encode_indexed_array(self, vals, sub_space, search_attrs):
898        attr_payload = b''
899        for i, val in enumerate(vals):
900            idx = i | Netlink.NLA_F_NESTED
901            val_payload = self._add_nest_attrs(val, sub_space, search_attrs)
902            attr_payload += self._add_attr_raw(idx, val_payload)
903        return attr_payload
904
905    def _get_enum_or_unknown(self, enum, raw):
906        try:
907            name = enum.entries_by_val[raw].name
908        except KeyError as error:
909            if self.process_unknown:
910                name = f"Unknown({raw})"
911            else:
912                raise error
913        return name
914
915    def _decode_enum(self, raw, attr_spec):
916        enum = self.consts[attr_spec['enum']]
917        if enum.type == 'flags' or attr_spec.get('enum-as-flags', False):
918            i = 0
919            value = set()
920            while raw:
921                if raw & 1:
922                    value.add(self._get_enum_or_unknown(enum, i))
923                raw >>= 1
924                i += 1
925        else:
926            value = self._get_enum_or_unknown(enum, raw)
927        return value
928
929    def _decode_binary(self, attr, attr_spec):
930        if attr_spec.struct_name:
931            decoded = self._decode_struct(attr.raw, attr_spec.struct_name)
932        elif attr_spec.sub_type:
933            decoded = attr.as_c_array(attr_spec.sub_type)
934            if 'enum' in attr_spec:
935                decoded = [ self._decode_enum(x, attr_spec) for x in decoded ]
936            elif attr_spec.display_hint:
937                decoded = [ self._formatted_string(x, attr_spec.display_hint)
938                            for x in decoded ]
939        else:
940            decoded = attr.as_bin()
941            if attr_spec.display_hint:
942                decoded = self._formatted_string(decoded, attr_spec.display_hint)
943        return decoded
944
945    def _decode_array_attr(self, attr, attr_spec):
946        decoded = []
947        offset = 0
948        while offset < len(attr.raw):
949            item = NlAttr(attr.raw, offset)
950            offset += item.full_len
951
952            if attr_spec["sub-type"] == 'nest':
953                subattrs = self._decode(NlAttrs(item.raw), attr_spec['nested-attributes'])
954                decoded.append({ item.type: subattrs })
955            elif attr_spec["sub-type"] == 'binary':
956                subattr = item.as_bin()
957                if attr_spec.display_hint:
958                    subattr = self._formatted_string(subattr, attr_spec.display_hint)
959                decoded.append(subattr)
960            elif attr_spec["sub-type"] in NlAttr.type_formats:
961                subattr = item.as_scalar(attr_spec['sub-type'], attr_spec.byte_order)
962                if 'enum' in attr_spec:
963                    subattr = self._decode_enum(subattr, attr_spec)
964                elif attr_spec.display_hint:
965                    subattr = self._formatted_string(subattr, attr_spec.display_hint)
966                decoded.append(subattr)
967            else:
968                raise YnlException(f'Unknown {attr_spec["sub-type"]} with name {attr_spec["name"]}')
969        return decoded
970
971    def _decode_nest_type_value(self, attr, attr_spec):
972        decoded = {}
973        value = attr
974        for name in attr_spec['type-value']:
975            value = NlAttr(value.raw, 0)
976            decoded[name] = value.type
977        subattrs = self._decode(NlAttrs(value.raw), attr_spec['nested-attributes'])
978        decoded.update(subattrs)
979        return decoded
980
981    def _decode_unknown(self, attr):
982        if attr.is_nest:
983            return self._decode(NlAttrs(attr.raw), None)
984        return attr.as_bin()
985
986    def _rsp_add(self, rsp, name, is_multi, decoded):
987        if is_multi is None:
988            if name in rsp and not isinstance(rsp[name], list):
989                rsp[name] = [rsp[name]]
990                is_multi = True
991            else:
992                is_multi = False
993
994        if not is_multi:
995            rsp[name] = decoded
996        elif name in rsp:
997            rsp[name].append(decoded)
998        else:
999            rsp[name] = [decoded]
1000
1001    def _resolve_selector(self, attr_spec, search_attrs):
1002        sub_msg = attr_spec.sub_message
1003        if sub_msg not in self.sub_msgs:
1004            raise YnlException(f"No sub-message spec named {sub_msg} for {attr_spec.name}")
1005        sub_msg_spec = self.sub_msgs[sub_msg]
1006
1007        selector = attr_spec.selector
1008        value = search_attrs.lookup(selector)
1009        if value not in sub_msg_spec.formats:
1010            raise YnlException(f"No message format for '{value}' in sub-message spec '{sub_msg}'")
1011
1012        spec = sub_msg_spec.formats[value]
1013        return spec, value
1014
1015    def _decode_sub_msg(self, attr, attr_spec, search_attrs):
1016        msg_format, _ = self._resolve_selector(attr_spec, search_attrs)
1017        decoded = {}
1018        offset = 0
1019        if msg_format.fixed_header:
1020            decoded.update(self._decode_struct(attr.raw, msg_format.fixed_header))
1021            offset = self.struct_size(msg_format.fixed_header)
1022        if msg_format.attr_set:
1023            if msg_format.attr_set in self.attr_sets:
1024                subdict = self._decode(NlAttrs(attr.raw, offset), msg_format.attr_set)
1025                decoded.update(subdict)
1026            else:
1027                raise YnlException(f"Unknown attribute-set '{msg_format.attr_set}' "
1028                                   f"when decoding '{attr_spec.name}'")
1029        return decoded
1030
1031    # pylint: disable=too-many-statements
1032    def _decode(self, attrs, space, outer_attrs = None):
1033        rsp = {}
1034        search_attrs = {}
1035        if space:
1036            attr_space = self.attr_sets[space]
1037            search_attrs = SpaceAttrs(attr_space, rsp, outer_attrs)
1038
1039        for attr in attrs:
1040            try:
1041                attr_spec = attr_space.attrs_by_val[attr.type]
1042            except (KeyError, UnboundLocalError) as err:
1043                if not self.process_unknown:
1044                    raise YnlException(f"Space '{space}' has no attribute "
1045                                       f"with value '{attr.type}'") from err
1046                attr_name = f"UnknownAttr({attr.type})"
1047                self._rsp_add(rsp, attr_name, None, self._decode_unknown(attr))
1048                continue
1049
1050            try:
1051                if attr_spec["type"] == 'pad':
1052                    continue
1053                elif attr_spec["type"] == 'nest':
1054                    subdict = self._decode(NlAttrs(attr.raw),
1055                                           attr_spec['nested-attributes'],
1056                                           search_attrs)
1057                    decoded = subdict
1058                elif attr_spec["type"] == 'string':
1059                    decoded = attr.as_strz()
1060                elif attr_spec["type"] == 'binary':
1061                    decoded = self._decode_binary(attr, attr_spec)
1062                elif attr_spec["type"] == 'flag':
1063                    decoded = True
1064                elif attr_spec.is_auto_scalar:
1065                    decoded = attr.as_auto_scalar(attr_spec['type'], attr_spec.byte_order)
1066                    if 'enum' in attr_spec:
1067                        decoded = self._decode_enum(decoded, attr_spec)
1068                elif attr_spec["type"] in NlAttr.type_formats:
1069                    decoded = attr.as_scalar(attr_spec['type'], attr_spec.byte_order)
1070                    if 'enum' in attr_spec:
1071                        decoded = self._decode_enum(decoded, attr_spec)
1072                    elif attr_spec.display_hint:
1073                        decoded = self._formatted_string(decoded, attr_spec.display_hint)
1074                elif attr_spec["type"] == 'indexed-array':
1075                    decoded = self._decode_array_attr(attr, attr_spec)
1076                elif attr_spec["type"] == 'bitfield32':
1077                    value, selector = struct.unpack("II", attr.raw)
1078                    if 'enum' in attr_spec:
1079                        value = self._decode_enum(value, attr_spec)
1080                        selector = self._decode_enum(selector, attr_spec)
1081                    decoded = {"value": value, "selector": selector}
1082                elif attr_spec["type"] == 'sub-message':
1083                    decoded = self._decode_sub_msg(attr, attr_spec, search_attrs)
1084                elif attr_spec["type"] == 'nest-type-value':
1085                    decoded = self._decode_nest_type_value(attr, attr_spec)
1086                else:
1087                    if not self.process_unknown:
1088                        raise YnlException(f'Unknown {attr_spec["type"]} '
1089                                           f'with name {attr_spec["name"]}')
1090                    decoded = self._decode_unknown(attr)
1091
1092                self._rsp_add(rsp, attr_spec["name"], attr_spec.is_multi, decoded)
1093            except:
1094                print(f"Error decoding '{attr_spec.name}' from '{space}'")
1095                raise
1096
1097        return rsp
1098
1099    # pylint: disable=too-many-arguments, too-many-positional-arguments
1100    def _decode_extack_path(self, attrs, attr_set, offset, target, search_attrs):
1101        for attr in attrs:
1102            try:
1103                attr_spec = attr_set.attrs_by_val[attr.type]
1104            except KeyError as err:
1105                raise YnlException(
1106                    f"Space '{attr_set.name}' has no attribute with value '{attr.type}'") from err
1107            if offset > target:
1108                break
1109            if offset == target:
1110                return '.' + attr_spec.name
1111
1112            if offset + attr.full_len <= target:
1113                offset += attr.full_len
1114                continue
1115
1116            pathname = attr_spec.name
1117            if attr_spec['type'] == 'nest':
1118                sub_attrs = self.attr_sets[attr_spec['nested-attributes']]
1119                search_attrs = SpaceAttrs(sub_attrs, search_attrs.lookup(attr_spec['name']))
1120            elif attr_spec['type'] == 'sub-message':
1121                msg_format, value = self._resolve_selector(attr_spec, search_attrs)
1122                if msg_format is None:
1123                    raise YnlException(f"Can't resolve sub-message of "
1124                                       f"{attr_spec['name']} for extack")
1125                sub_attrs = self.attr_sets[msg_format.attr_set]
1126                pathname += f"({value})"
1127            else:
1128                raise YnlException(f"Can't dive into {attr.type} ({attr_spec['name']}) for extack")
1129            offset += 4
1130            subpath = self._decode_extack_path(NlAttrs(attr.raw), sub_attrs,
1131                                               offset, target, search_attrs)
1132            if subpath is None:
1133                return None
1134            return '.' + pathname + subpath
1135
1136        return None
1137
1138    def _decode_extack(self, request, op, extack, vals):
1139        if 'bad-attr-offs' not in extack:
1140            return
1141
1142        msg = self.nlproto.decode(self, NlMsg(request, 0, op.attr_set), op)
1143        offset = self.nlproto.msghdr_size() + self.struct_size(op.fixed_header)
1144        search_attrs = SpaceAttrs(op.attr_set, vals)
1145        path = self._decode_extack_path(msg.raw_attrs, op.attr_set, offset,
1146                                        extack['bad-attr-offs'], search_attrs)
1147        if path:
1148            del extack['bad-attr-offs']
1149            extack['bad-attr'] = path
1150
1151    def struct_size(self, name):
1152        if name:
1153            members = self.consts[name].members
1154            size = 0
1155            for m in members:
1156                if m.type in ['pad', 'binary']:
1157                    if m.struct:
1158                        size += self.struct_size(m.struct)
1159                    else:
1160                        size += m.len
1161                else:
1162                    format_ = NlAttr.get_format(m.type, m.byte_order)
1163                    size += format_.size
1164            return size
1165        return 0
1166
1167    def _decode_struct(self, data, name):
1168        members = self.consts[name].members
1169        attrs = {}
1170        offset = 0
1171        for m in members:
1172            value = None
1173            if m.type == 'pad':
1174                offset += m.len
1175            elif m.type == 'binary':
1176                if m.struct:
1177                    len_ = self.struct_size(m.struct)
1178                    value = self._decode_struct(data[offset : offset + len_],
1179                                                m.struct)
1180                    offset += len_
1181                else:
1182                    value = data[offset : offset + m.len]
1183                    offset += m.len
1184            else:
1185                format_ = NlAttr.get_format(m.type, m.byte_order)
1186                [ value ] = format_.unpack_from(data, offset)
1187                offset += format_.size
1188            if value is not None:
1189                if m.enum:
1190                    value = self._decode_enum(value, m)
1191                elif m.display_hint:
1192                    value = self._formatted_string(value, m.display_hint)
1193                attrs[m.name] = value
1194        return attrs
1195
1196    def _encode_struct(self, name, vals):
1197        members = self.consts[name].members
1198        attr_payload = b''
1199        for m in members:
1200            value = vals.pop(m.name) if m.name in vals else None
1201            if m.type == 'pad':
1202                attr_payload += bytearray(m.len)
1203            elif m.type == 'binary':
1204                if m.struct:
1205                    if value is None:
1206                        value = {}
1207                    attr_payload += self._encode_struct(m.struct, value)
1208                else:
1209                    if value is None:
1210                        attr_payload += bytearray(m.len)
1211                    else:
1212                        attr_payload += bytes.fromhex(value)
1213            else:
1214                if value is None:
1215                    value = 0
1216                format_ = NlAttr.get_format(m.type, m.byte_order)
1217                attr_payload += format_.pack(value)
1218        return attr_payload
1219
1220    def _formatted_string(self, raw, display_hint):
1221        if display_hint == 'mac':
1222            formatted = ':'.join(f'{b:02x}' for b in raw)
1223        elif display_hint == 'hex':
1224            if isinstance(raw, int):
1225                formatted = hex(raw)
1226            else:
1227                formatted = bytes.hex(raw, ' ')
1228        elif display_hint in [ 'ipv4', 'ipv6', 'ipv4-or-v6' ]:
1229            formatted = format(ipaddress.ip_address(raw))
1230        elif display_hint == 'uuid':
1231            formatted = str(uuid.UUID(bytes=raw))
1232        else:
1233            formatted = raw
1234        return formatted
1235
1236    def _from_string(self, string, attr_spec):
1237        if attr_spec.display_hint in ['ipv4', 'ipv6', 'ipv4-or-v6']:
1238            ip = ipaddress.ip_address(string)
1239            if attr_spec['type'] == 'binary':
1240                raw = ip.packed
1241            else:
1242                raw = int(ip)
1243        elif attr_spec.display_hint == 'hex':
1244            if attr_spec['type'] == 'binary':
1245                raw = bytes.fromhex(string)
1246            else:
1247                raw = int(string, 16)
1248        elif attr_spec.display_hint == 'mac':
1249            # Parse MAC address in format "00:11:22:33:44:55" or "001122334455"
1250            if ':' in string:
1251                mac_bytes = [int(x, 16) for x in string.split(':')]
1252            else:
1253                if len(string) % 2 != 0:
1254                    raise YnlException(f"Invalid MAC address format: {string}")
1255                mac_bytes = [int(string[i:i+2], 16) for i in range(0, len(string), 2)]
1256            raw = bytes(mac_bytes)
1257        else:
1258            raise YnlException(f"Display hint '{attr_spec.display_hint}' not implemented"
1259                            f" when parsing '{attr_spec['name']}'")
1260        return raw
1261
1262    def handle_ntf(self, decoded, nsid=None):
1263        msg = {}
1264        if self.include_raw:
1265            msg['raw'] = decoded
1266        op = self.rsp_by_value[decoded.cmd()]
1267        attrs = self._decode(decoded.raw_attrs, op.attr_set.name)
1268        if op.fixed_header:
1269            attrs.update(self._decode_struct(decoded.raw, op.fixed_header))
1270
1271        msg['name'] = op['name']
1272        msg['msg'] = attrs
1273        if nsid is not None:
1274            msg['nsid'] = nsid
1275        self.async_msg_queue.put(msg)
1276
1277    def _recvmsg(self, flags=0):
1278        reply, ancdata, _, _ = self.sock.recvmsg(self._recv_size, 4096, flags)
1279        return reply, ancdata
1280
1281    def check_ntf(self):
1282        while True:
1283            try:
1284                reply, ancdata = self._recvmsg(socket.MSG_DONTWAIT)
1285            except BlockingIOError:
1286                return
1287
1288            nsid = self._decode_nsid(ancdata)
1289            nms = NlMsgs(reply)
1290            self._recv_dbg_print(reply, nms)
1291            for nl_msg in nms:
1292                if nl_msg.error:
1293                    print("Netlink error in ntf!?", os.strerror(-nl_msg.error))
1294                    print(nl_msg)
1295                    continue
1296                if nl_msg.done:
1297                    print("Netlink done while checking for ntf!?")
1298                    continue
1299
1300                decoded = self.nlproto.decode(self, nl_msg, None)
1301                if decoded.cmd() not in self.async_msg_ids:
1302                    print("Unexpected msg id while checking for ntf", decoded)
1303                    continue
1304
1305                self.handle_ntf(decoded, nsid)
1306
1307    def poll_ntf(self, duration=None):
1308        start_time = time.time()
1309        selector = selectors.DefaultSelector()
1310        selector.register(self.sock, selectors.EVENT_READ)
1311
1312        while True:
1313            try:
1314                yield self.async_msg_queue.get_nowait()
1315            except queue.Empty:
1316                if duration is not None:
1317                    timeout = start_time + duration - time.time()
1318                    if timeout <= 0:
1319                        return
1320                else:
1321                    timeout = None
1322                events = selector.select(timeout)
1323                if events:
1324                    self.check_ntf()
1325
1326    def operation_do_attributes(self, name):
1327        """
1328        For a given operation name, find and return a supported
1329        set of attributes (as a dict).
1330        """
1331        op = self.find_operation(name)
1332        if not op:
1333            return None
1334
1335        return op['do']['request']['attributes'].copy()
1336
1337    def _encode_message(self, op, vals, flags, req_seq):
1338        nl_flags = Netlink.NLM_F_REQUEST | Netlink.NLM_F_ACK
1339        for flag in flags or []:
1340            nl_flags |= flag
1341
1342        msg = self.nlproto.message(nl_flags, op.req_value, 1, req_seq)
1343        if op.fixed_header:
1344            msg += self._encode_struct(op.fixed_header, vals)
1345        search_attrs = SpaceAttrs(op.attr_set, vals)
1346        for name, value in vals.items():
1347            msg += self._add_attr(op.attr_set.name, name, value, search_attrs)
1348        msg = _genl_msg_finalize(msg)
1349        return msg
1350
1351    # pylint: disable=too-many-statements
1352    def _ops(self, ops):
1353        reqs_by_seq = {}
1354        req_seq = random.randint(1024, 65535)
1355        payload = b''
1356        for (method, vals, flags) in ops:
1357            op = self.ops[method]
1358            msg = self._encode_message(op, vals, flags, req_seq)
1359            reqs_by_seq[req_seq] = (op, vals, msg, flags)
1360            payload += msg
1361            req_seq += 1
1362
1363        self.sock.send(payload, 0)
1364
1365        done = False
1366        rsp = []
1367        op_rsp = []
1368        while not done:
1369            reply, ancdata = self._recvmsg()
1370            nsid = self._decode_nsid(ancdata)
1371            nms = NlMsgs(reply)
1372            self._recv_dbg_print(reply, nms)
1373            for nl_msg in nms:
1374                if nl_msg.nl_seq in reqs_by_seq:
1375                    (op, vals, req_msg, req_flags) = reqs_by_seq[nl_msg.nl_seq]
1376                    if nl_msg.extack:
1377                        nl_msg.annotate_extack(op.attr_set)
1378                        self._decode_extack(req_msg, op, nl_msg.extack, vals)
1379                else:
1380                    op = None
1381                    req_flags = []
1382
1383                if nl_msg.error:
1384                    raise NlError(nl_msg)
1385                if nl_msg.done:
1386                    if nl_msg.extack:
1387                        print("Netlink warning:")
1388                        print(nl_msg)
1389
1390                    if Netlink.NLM_F_DUMP in req_flags:
1391                        rsp.append(op_rsp)
1392                    elif not op_rsp:
1393                        rsp.append(None)
1394                    elif len(op_rsp) == 1:
1395                        rsp.append(op_rsp[0])
1396                    else:
1397                        rsp.append(op_rsp)
1398                    op_rsp = []
1399
1400                    del reqs_by_seq[nl_msg.nl_seq]
1401                    done = len(reqs_by_seq) == 0
1402                    break
1403
1404                decoded = self.nlproto.decode(self, nl_msg, op)
1405
1406                # Check if this is a reply to our request
1407                if nl_msg.nl_seq not in reqs_by_seq or decoded.cmd() != op.rsp_value:
1408                    if decoded.cmd() in self.async_msg_ids:
1409                        self.handle_ntf(decoded, nsid)
1410                        continue
1411                    print('Unexpected message: ' + repr(decoded))
1412                    continue
1413
1414                rsp_msg = self._decode(decoded.raw_attrs, op.attr_set.name)
1415                if op.fixed_header:
1416                    rsp_msg.update(self._decode_struct(decoded.raw, op.fixed_header))
1417                op_rsp.append(rsp_msg)
1418
1419        return rsp
1420
1421    def _op(self, method, vals, flags=None, dump=False):
1422        req_flags = flags or []
1423        if dump:
1424            req_flags.append(Netlink.NLM_F_DUMP)
1425
1426        ops = [(method, vals, req_flags)]
1427        return self._ops(ops)[0]
1428
1429    def do(self, method, vals, flags=None):
1430        return self._op(method, vals, flags)
1431
1432    def dump(self, method, vals):
1433        return self._op(method, vals, dump=True)
1434
1435    def do_multi(self, ops):
1436        return self._ops(ops)
1437
1438    def get_policy(self, op_name, mode):
1439        """Query running kernel for the Netlink policy of an operation.
1440
1441        Allows checking whether kernel supports a given attribute or value.
1442        This method consults the running kernel, not the YAML spec.
1443
1444        Args:
1445            op_name: operation name as it appears in the YAML spec
1446            mode: 'do' or 'dump'
1447
1448        Returns:
1449            NlPolicy acting as a read-only dict mapping attribute names
1450            to their policy properties (type, min/max, nested, etc.),
1451            or None if the operation has no policy for the given mode.
1452            Empty policy usually implies that the operation rejects
1453            all attributes.
1454        """
1455        op = self.ops[op_name]
1456        op_policy, policy_table = _genl_policy_dump(self.nlproto.family_id,
1457                                                    op.req_value)
1458        if mode not in op_policy:
1459            return None
1460        policy_idx = op_policy[mode]
1461        return NlPolicy(self, policy_idx, policy_table, op.attr_set)
1462