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