xref: /linux/tools/net/ynl/pyynl/lib/ynl.py (revision 91ec2035134982b98fab0609a9fd8480e8217dc1)
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    The spec can be selected either by file path (def_path=) or, when it
665    ships in a well-known location, by family name (family="xyz"); exactly
666    one of the two must be given. For example:
667
668      from pyynl import YnlFamily
669
670      ynl = YnlFamily(family="netdev")
671
672    Primary use of the class is to execute Netlink commands:
673
674      ynl.<op_name>(attrs, ...)
675
676    By default this will execute the <op_name> as "do", pass dump=True
677    to perform a dump operation.
678
679    ynl.<op_name> is a shorthand / convenience wrapper for the following
680    methods which take the op_name as a string:
681
682      ynl.do(op_name, attrs, flags=None) -- execute a do operation
683      ynl.dump(op_name, attrs)           -- execute a dump operation
684      ynl.do_multi(ops)                  -- batch multiple do operations
685
686    The flags argument in ynl.do() allows passing in extra NLM_F_* flags
687    which may be necessary for old families.
688
689    Notification API:
690
691      ynl.ntf_subscribe(mcast_name)      -- join a multicast group
692      ynl.ntf_listen_all_nsid()          -- listen on all netns
693      ynl.ntf_bind(addr=(0, 0))          -- bind socket for unicast notifications
694      ynl.check_ntf()                    -- drain pending notifications
695      ynl.poll_ntf(duration=None)        -- yield notifications
696
697    Policy introspection allows querying validation criteria from the running
698    kernel. Allows checking whether kernel supports a given attribute or value.
699
700      ynl.get_policy(op_name, mode)      -- query kernel policy for an op
701    """
702    def __init__(self, def_path=None, schema=None, process_unknown=None,
703                 recv_size=0, family=None):
704        super().__init__(def_path, schema, family=family)
705
706        self.include_raw = False
707        # Specs from /usr (selected by family=) have a higher chance of being
708        # stale, default to ignoring unknown attrs. In-tree users, and users
709        # who bundle the spec need to make a conscious decision.
710        if process_unknown is None:
711            process_unknown = family is not None
712        self.process_unknown = process_unknown
713
714        try:
715            if self.proto == "netlink-raw":
716                self.nlproto = NetlinkProtocol(self.yaml['name'],
717                                               self.yaml['protonum'])
718            else:
719                self.nlproto = GenlProtocol(self.yaml['name'])
720        except KeyError as err:
721            raise YnlException(f"Family '{self.yaml['name']}' not supported by the kernel") from err
722
723        self._recv_dbg = False
724        # Note that netlink will use conservative (min) message size for
725        # the first dump recv() on the socket, our setting will only matter
726        # from the second recv() on.
727        self._recv_size = recv_size if recv_size else 131072
728        # Netlink will always allocate at least PAGE_SIZE - sizeof(skb_shinfo)
729        # for a message, so smaller receive sizes will lead to truncation.
730        # Note that the min size for other families may be larger than 4k!
731        if self._recv_size < 4000:
732            raise ConfigError()
733
734        self.sock = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, self.nlproto.proto_num)
735        self.sock.setsockopt(Netlink.SOL_NETLINK, Netlink.NETLINK_CAP_ACK, 1)
736        self.sock.setsockopt(Netlink.SOL_NETLINK, Netlink.NETLINK_EXT_ACK, 1)
737        self.sock.setsockopt(Netlink.SOL_NETLINK, Netlink.NETLINK_GET_STRICT_CHK, 1)
738
739        self.async_msg_ids = set()
740        self.async_msg_queue = queue.Queue()
741
742        for msg in self.msgs.values():
743            if msg.is_async:
744                self.async_msg_ids.add(msg.rsp_value)
745
746        for op_name, op in self.ops.items():
747            bound_f = functools.partial(self._op, op_name)
748            setattr(self, op.ident_name, bound_f)
749
750    def close(self):
751        if self.sock is not None:
752            self.sock.close()
753            self.sock = None
754
755    def __enter__(self):
756        return self
757
758    def __exit__(self, exc_type, exc, tb):
759        self.close()
760
761    def ntf_subscribe(self, mcast_name):
762        mcast_id = self.nlproto.get_mcast_id(mcast_name, self.mcast_groups)
763        self.sock.bind((0, 0))
764        self.sock.setsockopt(Netlink.SOL_NETLINK, Netlink.NETLINK_ADD_MEMBERSHIP,
765                             mcast_id)
766
767    def ntf_listen_all_nsid(self):
768        """Enable NETLINK_LISTEN_ALL_NSID to receive notifications from all
769        namespaces that have an nsid mapped in the current one."""
770        self.sock.setsockopt(Netlink.SOL_NETLINK,
771                             Netlink.NETLINK_LISTEN_ALL_NSID, 1)
772
773    @staticmethod
774    def _decode_nsid(ancdata):
775        for cmsg_level, cmsg_type, cmsg_data in ancdata:
776            if (cmsg_level == Netlink.SOL_NETLINK and
777                    cmsg_type == Netlink.NETLINK_LISTEN_ALL_NSID):
778                nsid = struct.unpack('i', cmsg_data)[0]
779                if nsid >= 0:
780                    return nsid
781                return None
782        return None
783
784    def ntf_bind(self, addr=(0, 0)):
785        """Bind socket for receiving unicast notifications."""
786        self.sock.bind(addr)
787
788    def set_recv_dbg(self, enabled):
789        self._recv_dbg = enabled
790
791    def _recv_dbg_print(self, reply, nl_msgs):
792        if not self._recv_dbg:
793            return
794        print("Recv: read", len(reply), "bytes,",
795              len(nl_msgs.msgs), "messages", file=sys.stderr)
796        for nl_msg in nl_msgs:
797            print("  ", nl_msg, file=sys.stderr)
798
799    def _encode_enum(self, attr_spec, value):
800        enum = self.consts[attr_spec['enum']]
801        if enum.type == 'flags' or attr_spec.get('enum-as-flags', False):
802            scalar = 0
803            if isinstance(value, str):
804                value = [value]
805            for single_value in value:
806                scalar += enum.entries[single_value].user_value(as_flags = True)
807            return scalar
808        return enum.entries[value].user_value()
809
810    def _get_scalar(self, attr_spec, value):
811        try:
812            return int(value)
813        except (ValueError, TypeError) as e:
814            if 'enum' in attr_spec:
815                return self._encode_enum(attr_spec, value)
816            if attr_spec.display_hint:
817                return self._from_string(value, attr_spec)
818            raise e
819
820    # pylint: disable=too-many-statements
821    def _add_attr(self, space, name, value, search_attrs):
822        try:
823            attr = self.attr_sets[space][name]
824        except KeyError as err:
825            raise YnlException(f"Space '{space}' has no attribute '{name}'") from err
826        nl_type = attr.value
827
828        if attr.is_multi and isinstance(value, list):
829            attr_payload = b''
830            for subvalue in value:
831                attr_payload += self._add_attr(space, name, subvalue, search_attrs)
832            return attr_payload
833
834        if attr["type"] == 'nest':
835            nl_type |= Netlink.NLA_F_NESTED
836            sub_space = attr['nested-attributes']
837            attr_payload = self._add_nest_attrs(value, sub_space, search_attrs)
838        elif attr['type'] == 'indexed-array' and attr['sub-type'] == 'nest':
839            nl_type |= Netlink.NLA_F_NESTED
840            sub_space = attr['nested-attributes']
841            attr_payload = self._encode_indexed_array(value, sub_space,
842                                                      search_attrs)
843        elif attr["type"] == 'flag':
844            if not value:
845                # If value is absent or false then skip attribute creation.
846                return b''
847            attr_payload = b''
848        elif attr["type"] == 'string':
849            attr_payload = str(value).encode('ascii') + b'\x00'
850        elif attr["type"] == 'binary':
851            if value is None:
852                attr_payload = b''
853            elif isinstance(value, bytes):
854                attr_payload = value
855            elif isinstance(value, str):
856                if attr.display_hint:
857                    attr_payload = self._from_string(value, attr)
858                else:
859                    attr_payload = bytes.fromhex(value)
860            elif isinstance(value, dict) and attr.struct_name:
861                attr_payload = self._encode_struct(attr.struct_name, value)
862            elif isinstance(value, list) and attr.sub_type in NlAttr.type_formats:
863                format_ = NlAttr.get_format(attr.sub_type)
864                attr_payload = b''.join([format_.pack(x) for x in value])
865            else:
866                raise YnlException(f'Unknown type for binary attribute, value: {value}')
867        elif attr['type'] in NlAttr.type_formats or attr.is_auto_scalar:
868            scalar = self._get_scalar(attr, value)
869            if attr.is_auto_scalar:
870                attr_type = attr["type"][0] + ('32' if scalar.bit_length() <= 32 else '64')
871            else:
872                attr_type = attr["type"]
873            format_ = NlAttr.get_format(attr_type, attr.byte_order)
874            attr_payload = format_.pack(scalar)
875        elif attr['type'] in "bitfield32":
876            scalar_value = self._get_scalar(attr, value["value"])
877            scalar_selector = self._get_scalar(attr, value["selector"])
878            attr_payload = struct.pack("II", scalar_value, scalar_selector)
879        elif attr['type'] == 'sub-message':
880            msg_format, _ = self._resolve_selector(attr, search_attrs)
881            attr_payload = b''
882            if msg_format.fixed_header:
883                attr_payload += self._encode_struct(msg_format.fixed_header, value)
884            if msg_format.attr_set:
885                if msg_format.attr_set in self.attr_sets:
886                    nl_type |= Netlink.NLA_F_NESTED
887                    sub_attrs = SpaceAttrs(msg_format.attr_set, value, search_attrs)
888                    for subname, subvalue in value.items():
889                        attr_payload += self._add_attr(msg_format.attr_set,
890                                                       subname, subvalue, sub_attrs)
891                else:
892                    raise YnlException(f"Unknown attribute-set '{msg_format.attr_set}'")
893        else:
894            raise YnlException(f'Unknown type at {space} {name} {value} {attr["type"]}')
895
896        return self._add_attr_raw(nl_type, attr_payload)
897
898    def _add_attr_raw(self, nl_type, attr_payload):
899        pad = b'\x00' * ((4 - len(attr_payload) % 4) % 4)
900        return struct.pack('HH', len(attr_payload) + 4, nl_type) + attr_payload + pad
901
902    def _add_nest_attrs(self, value, sub_space, search_attrs):
903        sub_attrs = SpaceAttrs(self.attr_sets[sub_space], value, search_attrs)
904        attr_payload = b''
905        for subname, subvalue in value.items():
906            attr_payload += self._add_attr(sub_space, subname, subvalue,
907                                           sub_attrs)
908        return attr_payload
909
910    def _encode_indexed_array(self, vals, sub_space, search_attrs):
911        attr_payload = b''
912        for i, val in enumerate(vals):
913            idx = i | Netlink.NLA_F_NESTED
914            val_payload = self._add_nest_attrs(val, sub_space, search_attrs)
915            attr_payload += self._add_attr_raw(idx, val_payload)
916        return attr_payload
917
918    def _get_enum_or_unknown(self, enum, raw):
919        try:
920            name = enum.entries_by_val[raw].name
921        except KeyError as error:
922            if self.process_unknown:
923                name = f"Unknown({raw})"
924            else:
925                raise error
926        return name
927
928    def _decode_enum(self, raw, attr_spec):
929        enum = self.consts[attr_spec['enum']]
930        if enum.type == 'flags' or attr_spec.get('enum-as-flags', False):
931            i = 0
932            value = set()
933            while raw:
934                if raw & 1:
935                    value.add(self._get_enum_or_unknown(enum, i))
936                raw >>= 1
937                i += 1
938        else:
939            value = self._get_enum_or_unknown(enum, raw)
940        return value
941
942    def _decode_binary(self, attr, attr_spec):
943        if attr_spec.struct_name:
944            decoded = self._decode_struct(attr.raw, attr_spec.struct_name)
945        elif attr_spec.sub_type:
946            decoded = attr.as_c_array(attr_spec.sub_type)
947            if 'enum' in attr_spec:
948                decoded = [ self._decode_enum(x, attr_spec) for x in decoded ]
949            elif attr_spec.display_hint:
950                decoded = [ self._formatted_string(x, attr_spec.display_hint)
951                            for x in decoded ]
952        else:
953            decoded = attr.as_bin()
954            if attr_spec.display_hint:
955                decoded = self._formatted_string(decoded, attr_spec.display_hint)
956        return decoded
957
958    def _decode_array_attr(self, attr, attr_spec):
959        decoded = []
960        offset = 0
961        while offset < len(attr.raw):
962            item = NlAttr(attr.raw, offset)
963            offset += item.full_len
964
965            if attr_spec["sub-type"] == 'nest':
966                subattrs = self._decode(NlAttrs(item.raw), attr_spec['nested-attributes'])
967                decoded.append({ item.type: subattrs })
968            elif attr_spec["sub-type"] == 'binary':
969                subattr = item.as_bin()
970                if attr_spec.display_hint:
971                    subattr = self._formatted_string(subattr, attr_spec.display_hint)
972                decoded.append(subattr)
973            elif attr_spec["sub-type"] in NlAttr.type_formats:
974                subattr = item.as_scalar(attr_spec['sub-type'], attr_spec.byte_order)
975                if 'enum' in attr_spec:
976                    subattr = self._decode_enum(subattr, attr_spec)
977                elif attr_spec.display_hint:
978                    subattr = self._formatted_string(subattr, attr_spec.display_hint)
979                decoded.append(subattr)
980            else:
981                raise YnlException(f'Unknown {attr_spec["sub-type"]} with name {attr_spec["name"]}')
982        return decoded
983
984    def _decode_nest_type_value(self, attr, attr_spec):
985        decoded = {}
986        value = attr
987        for name in attr_spec['type-value']:
988            value = NlAttr(value.raw, 0)
989            decoded[name] = value.type
990        subattrs = self._decode(NlAttrs(value.raw), attr_spec['nested-attributes'])
991        decoded.update(subattrs)
992        return decoded
993
994    def _decode_unknown(self, attr):
995        if attr.is_nest:
996            return self._decode(NlAttrs(attr.raw), None)
997        return attr.as_bin()
998
999    def _rsp_add(self, rsp, name, is_multi, decoded):
1000        if is_multi is None:
1001            if name in rsp and not isinstance(rsp[name], list):
1002                rsp[name] = [rsp[name]]
1003                is_multi = True
1004            else:
1005                is_multi = False
1006
1007        if not is_multi:
1008            rsp[name] = decoded
1009        elif name in rsp:
1010            rsp[name].append(decoded)
1011        else:
1012            rsp[name] = [decoded]
1013
1014    def _resolve_selector(self, attr_spec, search_attrs):
1015        sub_msg = attr_spec.sub_message
1016        if sub_msg not in self.sub_msgs:
1017            raise YnlException(f"No sub-message spec named {sub_msg} for {attr_spec.name}")
1018        sub_msg_spec = self.sub_msgs[sub_msg]
1019
1020        selector = attr_spec.selector
1021        value = search_attrs.lookup(selector)
1022        if value not in sub_msg_spec.formats:
1023            raise YnlException(f"No message format for '{value}' in sub-message spec '{sub_msg}'")
1024
1025        spec = sub_msg_spec.formats[value]
1026        return spec, value
1027
1028    def _decode_sub_msg(self, attr, attr_spec, search_attrs):
1029        msg_format, _ = self._resolve_selector(attr_spec, search_attrs)
1030        decoded = {}
1031        offset = 0
1032        if msg_format.fixed_header:
1033            decoded.update(self._decode_struct(attr.raw, msg_format.fixed_header))
1034            offset = self.struct_size(msg_format.fixed_header)
1035        if msg_format.attr_set:
1036            if msg_format.attr_set in self.attr_sets:
1037                subdict = self._decode(NlAttrs(attr.raw, offset), msg_format.attr_set)
1038                decoded.update(subdict)
1039            else:
1040                raise YnlException(f"Unknown attribute-set '{msg_format.attr_set}' "
1041                                   f"when decoding '{attr_spec.name}'")
1042        return decoded
1043
1044    # pylint: disable=too-many-statements
1045    def _decode(self, attrs, space, outer_attrs = None):
1046        rsp = {}
1047        search_attrs = {}
1048        if space:
1049            attr_space = self.attr_sets[space]
1050            search_attrs = SpaceAttrs(attr_space, rsp, outer_attrs)
1051
1052        for attr in attrs:
1053            try:
1054                attr_spec = attr_space.attrs_by_val[attr.type]
1055            except (KeyError, UnboundLocalError) as err:
1056                if not self.process_unknown:
1057                    raise YnlException(f"Space '{space}' has no attribute "
1058                                       f"with value '{attr.type}'") from err
1059                attr_name = f"UnknownAttr({attr.type})"
1060                self._rsp_add(rsp, attr_name, None, self._decode_unknown(attr))
1061                continue
1062
1063            try:
1064                if attr_spec["type"] == 'pad':
1065                    continue
1066                elif attr_spec["type"] == 'nest':
1067                    subdict = self._decode(NlAttrs(attr.raw),
1068                                           attr_spec['nested-attributes'],
1069                                           search_attrs)
1070                    decoded = subdict
1071                elif attr_spec["type"] == 'string':
1072                    decoded = attr.as_strz()
1073                elif attr_spec["type"] == 'binary':
1074                    decoded = self._decode_binary(attr, attr_spec)
1075                elif attr_spec["type"] == 'flag':
1076                    decoded = True
1077                elif attr_spec.is_auto_scalar:
1078                    decoded = attr.as_auto_scalar(attr_spec['type'], attr_spec.byte_order)
1079                    if 'enum' in attr_spec:
1080                        decoded = self._decode_enum(decoded, attr_spec)
1081                elif attr_spec["type"] in NlAttr.type_formats:
1082                    decoded = attr.as_scalar(attr_spec['type'], attr_spec.byte_order)
1083                    if 'enum' in attr_spec:
1084                        decoded = self._decode_enum(decoded, attr_spec)
1085                    elif attr_spec.display_hint:
1086                        decoded = self._formatted_string(decoded, attr_spec.display_hint)
1087                elif attr_spec["type"] == 'indexed-array':
1088                    decoded = self._decode_array_attr(attr, attr_spec)
1089                elif attr_spec["type"] == 'bitfield32':
1090                    value, selector = struct.unpack("II", attr.raw)
1091                    if 'enum' in attr_spec:
1092                        value = self._decode_enum(value, attr_spec)
1093                        selector = self._decode_enum(selector, attr_spec)
1094                    decoded = {"value": value, "selector": selector}
1095                elif attr_spec["type"] == 'sub-message':
1096                    decoded = self._decode_sub_msg(attr, attr_spec, search_attrs)
1097                elif attr_spec["type"] == 'nest-type-value':
1098                    decoded = self._decode_nest_type_value(attr, attr_spec)
1099                else:
1100                    if not self.process_unknown:
1101                        raise YnlException(f'Unknown {attr_spec["type"]} '
1102                                           f'with name {attr_spec["name"]}')
1103                    decoded = self._decode_unknown(attr)
1104
1105                self._rsp_add(rsp, attr_spec["name"], attr_spec.is_multi, decoded)
1106            except:
1107                print(f"Error decoding '{attr_spec.name}' from '{space}'")
1108                raise
1109
1110        return rsp
1111
1112    # pylint: disable=too-many-arguments, too-many-positional-arguments
1113    def _decode_extack_path(self, attrs, attr_set, offset, target, search_attrs):
1114        for attr in attrs:
1115            try:
1116                attr_spec = attr_set.attrs_by_val[attr.type]
1117            except KeyError as err:
1118                raise YnlException(
1119                    f"Space '{attr_set.name}' has no attribute with value '{attr.type}'") from err
1120            if offset > target:
1121                break
1122            if offset == target:
1123                return '.' + attr_spec.name
1124
1125            if offset + attr.full_len <= target:
1126                offset += attr.full_len
1127                continue
1128
1129            pathname = attr_spec.name
1130            if attr_spec['type'] == 'nest':
1131                sub_attrs = self.attr_sets[attr_spec['nested-attributes']]
1132                search_attrs = SpaceAttrs(sub_attrs, search_attrs.lookup(attr_spec['name']))
1133            elif attr_spec['type'] == 'sub-message':
1134                msg_format, value = self._resolve_selector(attr_spec, search_attrs)
1135                if msg_format is None:
1136                    raise YnlException(f"Can't resolve sub-message of "
1137                                       f"{attr_spec['name']} for extack")
1138                sub_attrs = self.attr_sets[msg_format.attr_set]
1139                pathname += f"({value})"
1140            else:
1141                raise YnlException(f"Can't dive into {attr.type} ({attr_spec['name']}) for extack")
1142            offset += 4
1143            subpath = self._decode_extack_path(NlAttrs(attr.raw), sub_attrs,
1144                                               offset, target, search_attrs)
1145            if subpath is None:
1146                return None
1147            return '.' + pathname + subpath
1148
1149        return None
1150
1151    def _decode_extack(self, request, op, extack, vals):
1152        if 'bad-attr-offs' not in extack:
1153            return
1154
1155        msg = self.nlproto.decode(self, NlMsg(request, 0, op.attr_set), op)
1156        offset = self.nlproto.msghdr_size() + self.struct_size(op.fixed_header)
1157        search_attrs = SpaceAttrs(op.attr_set, vals)
1158        path = self._decode_extack_path(msg.raw_attrs, op.attr_set, offset,
1159                                        extack['bad-attr-offs'], search_attrs)
1160        if path:
1161            del extack['bad-attr-offs']
1162            extack['bad-attr'] = path
1163
1164    def struct_size(self, name):
1165        if name:
1166            members = self.consts[name].members
1167            size = 0
1168            for m in members:
1169                if m.type in ['pad', 'binary']:
1170                    if m.struct:
1171                        size += self.struct_size(m.struct)
1172                    else:
1173                        size += m.len
1174                else:
1175                    format_ = NlAttr.get_format(m.type, m.byte_order)
1176                    size += format_.size
1177            return size
1178        return 0
1179
1180    def _decode_struct(self, data, name):
1181        members = self.consts[name].members
1182        attrs = {}
1183        offset = 0
1184        for m in members:
1185            value = None
1186            if m.type == 'pad':
1187                offset += m.len
1188            elif m.type == 'binary':
1189                if m.struct:
1190                    len_ = self.struct_size(m.struct)
1191                    value = self._decode_struct(data[offset : offset + len_],
1192                                                m.struct)
1193                    offset += len_
1194                else:
1195                    value = data[offset : offset + m.len]
1196                    offset += m.len
1197            else:
1198                format_ = NlAttr.get_format(m.type, m.byte_order)
1199                [ value ] = format_.unpack_from(data, offset)
1200                offset += format_.size
1201            if value is not None:
1202                if m.enum:
1203                    value = self._decode_enum(value, m)
1204                elif m.display_hint:
1205                    value = self._formatted_string(value, m.display_hint)
1206                attrs[m.name] = value
1207        return attrs
1208
1209    def _encode_struct(self, name, vals):
1210        members = self.consts[name].members
1211        attr_payload = b''
1212        for m in members:
1213            value = vals.pop(m.name) if m.name in vals else None
1214            if m.type == 'pad':
1215                attr_payload += bytearray(m.len)
1216            elif m.type == 'binary':
1217                if m.struct:
1218                    if value is None:
1219                        value = {}
1220                    attr_payload += self._encode_struct(m.struct, value)
1221                else:
1222                    if value is None:
1223                        attr_payload += bytearray(m.len)
1224                    else:
1225                        attr_payload += bytes.fromhex(value)
1226            else:
1227                if value is None:
1228                    value = 0
1229                format_ = NlAttr.get_format(m.type, m.byte_order)
1230                attr_payload += format_.pack(value)
1231        return attr_payload
1232
1233    def _formatted_string(self, raw, display_hint):
1234        if display_hint == 'mac':
1235            formatted = ':'.join(f'{b:02x}' for b in raw)
1236        elif display_hint == 'hex':
1237            if isinstance(raw, int):
1238                formatted = hex(raw)
1239            else:
1240                formatted = bytes.hex(raw, ' ')
1241        elif display_hint in [ 'ipv4', 'ipv6', 'ipv4-or-v6' ]:
1242            formatted = format(ipaddress.ip_address(raw))
1243        elif display_hint == 'uuid':
1244            formatted = str(uuid.UUID(bytes=raw))
1245        else:
1246            formatted = raw
1247        return formatted
1248
1249    def _from_string(self, string, attr_spec):
1250        if attr_spec.display_hint in ['ipv4', 'ipv6', 'ipv4-or-v6']:
1251            ip = ipaddress.ip_address(string)
1252            if attr_spec['type'] == 'binary':
1253                raw = ip.packed
1254            else:
1255                raw = int(ip)
1256        elif attr_spec.display_hint == 'hex':
1257            if attr_spec['type'] == 'binary':
1258                raw = bytes.fromhex(string)
1259            else:
1260                raw = int(string, 16)
1261        elif attr_spec.display_hint == 'mac':
1262            # Parse MAC address in format "00:11:22:33:44:55" or "001122334455"
1263            if ':' in string:
1264                mac_bytes = [int(x, 16) for x in string.split(':')]
1265            else:
1266                if len(string) % 2 != 0:
1267                    raise YnlException(f"Invalid MAC address format: {string}")
1268                mac_bytes = [int(string[i:i+2], 16) for i in range(0, len(string), 2)]
1269            raw = bytes(mac_bytes)
1270        else:
1271            raise YnlException(f"Display hint '{attr_spec.display_hint}' not implemented"
1272                            f" when parsing '{attr_spec['name']}'")
1273        return raw
1274
1275    def handle_ntf(self, decoded, nsid=None):
1276        msg = {}
1277        if self.include_raw:
1278            msg['raw'] = decoded
1279        op = self.rsp_by_value[decoded.cmd()]
1280        attrs = self._decode(decoded.raw_attrs, op.attr_set.name)
1281        if op.fixed_header:
1282            attrs.update(self._decode_struct(decoded.raw, op.fixed_header))
1283
1284        msg['name'] = op['name']
1285        msg['msg'] = attrs
1286        if nsid is not None:
1287            msg['nsid'] = nsid
1288        self.async_msg_queue.put(msg)
1289
1290    def _recvmsg(self, flags=0):
1291        reply, ancdata, _, _ = self.sock.recvmsg(self._recv_size, 4096, flags)
1292        return reply, ancdata
1293
1294    def check_ntf(self):
1295        while True:
1296            try:
1297                reply, ancdata = self._recvmsg(socket.MSG_DONTWAIT)
1298            except BlockingIOError:
1299                return
1300
1301            nsid = self._decode_nsid(ancdata)
1302            nms = NlMsgs(reply)
1303            self._recv_dbg_print(reply, nms)
1304            for nl_msg in nms:
1305                if nl_msg.error:
1306                    print("Netlink error in ntf!?", os.strerror(-nl_msg.error))
1307                    print(nl_msg)
1308                    continue
1309                if nl_msg.done:
1310                    print("Netlink done while checking for ntf!?")
1311                    continue
1312
1313                decoded = self.nlproto.decode(self, nl_msg, None)
1314                if decoded.cmd() not in self.async_msg_ids:
1315                    print("Unexpected msg id while checking for ntf", decoded)
1316                    continue
1317
1318                self.handle_ntf(decoded, nsid)
1319
1320    def poll_ntf(self, duration=None):
1321        start_time = time.time()
1322        selector = selectors.DefaultSelector()
1323        selector.register(self.sock, selectors.EVENT_READ)
1324
1325        while True:
1326            try:
1327                yield self.async_msg_queue.get_nowait()
1328            except queue.Empty:
1329                if duration is not None:
1330                    timeout = start_time + duration - time.time()
1331                    if timeout <= 0:
1332                        return
1333                else:
1334                    timeout = None
1335                events = selector.select(timeout)
1336                if events:
1337                    self.check_ntf()
1338
1339    def operation_do_attributes(self, name):
1340        """
1341        For a given operation name, find and return a supported
1342        set of attributes (as a dict).
1343        """
1344        op = self.find_operation(name)
1345        if not op:
1346            return None
1347
1348        return op['do']['request']['attributes'].copy()
1349
1350    def _encode_message(self, op, vals, flags, req_seq):
1351        nl_flags = Netlink.NLM_F_REQUEST | Netlink.NLM_F_ACK
1352        for flag in flags or []:
1353            nl_flags |= flag
1354
1355        msg = self.nlproto.message(nl_flags, op.req_value, 1, req_seq)
1356        if op.fixed_header:
1357            msg += self._encode_struct(op.fixed_header, vals)
1358        search_attrs = SpaceAttrs(op.attr_set, vals)
1359        for name, value in vals.items():
1360            msg += self._add_attr(op.attr_set.name, name, value, search_attrs)
1361        msg = _genl_msg_finalize(msg)
1362        return msg
1363
1364    # pylint: disable=too-many-statements
1365    def _ops(self, ops):
1366        reqs_by_seq = {}
1367        req_seq = random.randint(1024, 65535)
1368        payload = b''
1369        for (method, vals, flags) in ops:
1370            op = self.ops[method]
1371            msg = self._encode_message(op, vals, flags, req_seq)
1372            reqs_by_seq[req_seq] = (op, vals, msg, flags)
1373            payload += msg
1374            req_seq += 1
1375
1376        self.sock.send(payload, 0)
1377
1378        done = False
1379        rsp = []
1380        op_rsp = []
1381        while not done:
1382            reply, ancdata = self._recvmsg()
1383            nsid = self._decode_nsid(ancdata)
1384            nms = NlMsgs(reply)
1385            self._recv_dbg_print(reply, nms)
1386            for nl_msg in nms:
1387                if nl_msg.nl_seq in reqs_by_seq:
1388                    (op, vals, req_msg, req_flags) = reqs_by_seq[nl_msg.nl_seq]
1389                    if nl_msg.extack:
1390                        nl_msg.annotate_extack(op.attr_set)
1391                        self._decode_extack(req_msg, op, nl_msg.extack, vals)
1392                else:
1393                    op = None
1394                    req_flags = []
1395
1396                if nl_msg.error:
1397                    raise NlError(nl_msg)
1398                if nl_msg.done:
1399                    if nl_msg.extack:
1400                        print("Netlink warning:")
1401                        print(nl_msg)
1402
1403                    if Netlink.NLM_F_DUMP in req_flags:
1404                        rsp.append(op_rsp)
1405                    elif not op_rsp:
1406                        rsp.append(None)
1407                    elif len(op_rsp) == 1:
1408                        rsp.append(op_rsp[0])
1409                    else:
1410                        rsp.append(op_rsp)
1411                    op_rsp = []
1412
1413                    del reqs_by_seq[nl_msg.nl_seq]
1414                    done = len(reqs_by_seq) == 0
1415                    break
1416
1417                decoded = self.nlproto.decode(self, nl_msg, op)
1418
1419                # Check if this is a reply to our request
1420                if nl_msg.nl_seq not in reqs_by_seq or decoded.cmd() != op.rsp_value:
1421                    if decoded.cmd() in self.async_msg_ids:
1422                        self.handle_ntf(decoded, nsid)
1423                        continue
1424                    print('Unexpected message: ' + repr(decoded))
1425                    continue
1426
1427                rsp_msg = self._decode(decoded.raw_attrs, op.attr_set.name)
1428                if op.fixed_header:
1429                    rsp_msg.update(self._decode_struct(decoded.raw, op.fixed_header))
1430                op_rsp.append(rsp_msg)
1431
1432        return rsp
1433
1434    def _op(self, method, vals, flags=None, dump=False):
1435        req_flags = flags or []
1436        if dump:
1437            req_flags.append(Netlink.NLM_F_DUMP)
1438
1439        ops = [(method, vals, req_flags)]
1440        return self._ops(ops)[0]
1441
1442    def do(self, method, vals, flags=None):
1443        return self._op(method, vals, flags)
1444
1445    def dump(self, method, vals):
1446        return self._op(method, vals, dump=True)
1447
1448    def do_multi(self, ops):
1449        return self._ops(ops)
1450
1451    def get_policy(self, op_name, mode):
1452        """Query running kernel for the Netlink policy of an operation.
1453
1454        Allows checking whether kernel supports a given attribute or value.
1455        This method consults the running kernel, not the YAML spec.
1456
1457        Args:
1458            op_name: operation name as it appears in the YAML spec
1459            mode: 'do' or 'dump'
1460
1461        Returns:
1462            NlPolicy acting as a read-only dict mapping attribute names
1463            to their policy properties (type, min/max, nested, etc.),
1464            or None if the operation has no policy for the given mode.
1465            Empty policy usually implies that the operation rejects
1466            all attributes.
1467        """
1468        op = self.ops[op_name]
1469        op_policy, policy_table = _genl_policy_dump(self.nlproto.family_id,
1470                                                    op.req_value)
1471        if mode not in op_policy:
1472            return None
1473        policy_idx = op_policy[mode]
1474        return NlPolicy(self, policy_idx, policy_table, op.attr_set)
1475