diff options
| author | Jakub Kicinski <kuba@kernel.org> | 2026-03-11 05:33:07 +0300 |
|---|---|---|
| committer | Jakub Kicinski <kuba@kernel.org> | 2026-03-11 05:33:07 +0300 |
| commit | 7bb1970494faa6396fe4d622c4fe7edb1a9e217f (patch) | |
| tree | c42567f99ee9fe10b9c7404e651253ee90c9c888 | |
| parent | 73a864352570fd30d942652f05bfe9340d7a2055 (diff) | |
| parent | d6df5e9b2a565be08330e46a8a615aac9ed8711b (diff) | |
| download | linux-7bb1970494faa6396fe4d622c4fe7edb1a9e217f.tar.xz | |
Merge branch 'tools-ynl-policy-query-support'
Jakub Kicinski says:
====================
tools: ynl: policy query support
Improve the Netlink policy support in YNL. This series grew out of
improvements to policy checking, when writing selftests I realized
that instead of doing all the policy parsing in the test we're
better off making it part of YNL itself.
Patch 1 adds pad handling, apparently we never hit pad with commonly
used families. nlctrl policy dumps use pad more frequently.
Patch 2 is a trivial refactor.
Patch 3 pays off some technical debt in terms of documentation.
The YnlFamily class is growing in size and it's quite hard to
find its members. So document it a little bit.
Patch 4 is the main dish, the implementation of get_policy(op)
in YnlFamily.
Patch 5 plugs the new functionality into the CLI.
====================
Link: https://patch.msgid.link/20260310005337.3594225-1-kuba@kernel.org
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
| -rwxr-xr-x | tools/net/ynl/pyynl/cli.py | 12 | ||||
| -rw-r--r-- | tools/net/ynl/pyynl/lib/__init__.py | 5 | ||||
| -rw-r--r-- | tools/net/ynl/pyynl/lib/ynl.py | 217 | ||||
| -rw-r--r-- | tools/testing/selftests/net/lib/py/ynl.py | 6 |
4 files changed, 209 insertions, 31 deletions
diff --git a/tools/net/ynl/pyynl/cli.py b/tools/net/ynl/pyynl/cli.py index b452d4fb9434..fc9e84754e4b 100755 --- a/tools/net/ynl/pyynl/cli.py +++ b/tools/net/ynl/pyynl/cli.py @@ -256,6 +256,8 @@ def main(): schema_group.add_argument('--no-schema', action='store_true') dbg_group = parser.add_argument_group('Debug options') + io_group.add_argument('--policy', action='store_true', + help='Query kernel policy for the operation instead of executing it') dbg_group.add_argument('--dbg-small-recv', default=0, const=4000, action='store', nargs='?', type=int, metavar='INT', help="Length of buffers used for recv()") @@ -308,6 +310,16 @@ def main(): if args.dbg_small_recv: ynl.set_recv_dbg(True) + if args.policy: + if args.do: + pol = ynl.get_policy(args.do, 'do') + output(pol.attrs if pol else None) + args.do = None + if args.dump: + pol = ynl.get_policy(args.dump, 'dump') + output(pol.attrs if pol else None) + args.dump = None + if args.ntf: ynl.ntf_subscribe(args.ntf) diff --git a/tools/net/ynl/pyynl/lib/__init__.py b/tools/net/ynl/pyynl/lib/__init__.py index 33a96155fb3b..be741985ae4e 100644 --- a/tools/net/ynl/pyynl/lib/__init__.py +++ b/tools/net/ynl/pyynl/lib/__init__.py @@ -5,11 +5,12 @@ from .nlspec import SpecAttr, SpecAttrSet, SpecEnumEntry, SpecEnumSet, \ SpecFamily, SpecOperation, SpecSubMessage, SpecSubMessageFormat, \ SpecException -from .ynl import YnlFamily, Netlink, NlError, YnlException +from .ynl import YnlFamily, Netlink, NlError, NlPolicy, YnlException from .doc_generator import YnlDocGenerator __all__ = ["SpecAttr", "SpecAttrSet", "SpecEnumEntry", "SpecEnumSet", "SpecFamily", "SpecOperation", "SpecSubMessage", "SpecSubMessageFormat", "SpecException", - "YnlFamily", "Netlink", "NlError", "YnlDocGenerator", "YnlException"] + "YnlFamily", "Netlink", "NlError", "NlPolicy", "YnlException", + "YnlDocGenerator"] diff --git a/tools/net/ynl/pyynl/lib/ynl.py b/tools/net/ynl/pyynl/lib/ynl.py index 9774005e7ad1..0eedeee465d8 100644 --- a/tools/net/ynl/pyynl/lib/ynl.py +++ b/tools/net/ynl/pyynl/lib/ynl.py @@ -77,15 +77,22 @@ class Netlink: # nlctrl CTRL_CMD_GETFAMILY = 3 + CTRL_CMD_GETPOLICY = 10 CTRL_ATTR_FAMILY_ID = 1 CTRL_ATTR_FAMILY_NAME = 2 CTRL_ATTR_MAXATTR = 5 CTRL_ATTR_MCAST_GROUPS = 7 + CTRL_ATTR_POLICY = 8 + CTRL_ATTR_OP_POLICY = 9 + CTRL_ATTR_OP = 10 CTRL_ATTR_MCAST_GRP_NAME = 1 CTRL_ATTR_MCAST_GRP_ID = 2 + CTRL_ATTR_POLICY_DO = 1 + CTRL_ATTR_POLICY_DUMP = 2 + # Extack types NLMSGERR_ATTR_MSG = 1 NLMSGERR_ATTR_OFFS = 2 @@ -136,6 +143,34 @@ class ConfigError(Exception): pass +# pylint: disable=too-few-public-methods +class NlPolicy: + """Kernel policy for one mode (do or dump) of one operation. + + Returned by YnlFamily.get_policy(). Contains a dict of attributes + the kernel accepts, with their validation constraints. + + Attributes: + attrs: dict mapping attribute names to policy dicts, e.g. + page-pool-stats-get do policy:: + + { + 'info': {'type': 'nested', 'policy': { + 'id': {'type': 'uint', 'min-value': 1, + 'max-value': 4294967295}, + 'ifindex': {'type': 'u32', 'min-value': 1, + 'max-value': 2147483647}, + }}, + } + + Each policy dict always contains 'type' (e.g. u32, string, + nested). Optional keys: min-value, max-value, min-length, + max-length, mask, policy. + """ + def __init__(self, attrs): + self.attrs = attrs + + class NlAttr: ScalarFormat = namedtuple('ScalarFormat', ['native', 'big', 'little']) type_formats = { @@ -247,7 +282,7 @@ class NlMsg: elif extack.type == Netlink.NLMSGERR_ATTR_OFFS: self.extack['bad-attr-offs'] = extack.as_scalar('u32') elif extack.type == Netlink.NLMSGERR_ATTR_POLICY: - self.extack['policy'] = self._decode_policy(extack.raw) + self.extack['policy'] = _genl_decode_policy(extack.raw) else: if 'unknown' not in self.extack: self.extack['unknown'] = [] @@ -256,30 +291,6 @@ class NlMsg: if attr_space: self.annotate_extack(attr_space) - def _decode_policy(self, raw): - policy = {} - for attr in NlAttrs(raw): - if attr.type == Netlink.NL_POLICY_TYPE_ATTR_TYPE: - type_ = attr.as_scalar('u32') - policy['type'] = Netlink.AttrType(type_).name - elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MIN_VALUE_S: - policy['min-value'] = attr.as_scalar('s64') - elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MAX_VALUE_S: - policy['max-value'] = attr.as_scalar('s64') - elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MIN_VALUE_U: - policy['min-value'] = attr.as_scalar('u64') - elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MAX_VALUE_U: - policy['max-value'] = attr.as_scalar('u64') - elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MIN_LENGTH: - policy['min-length'] = attr.as_scalar('u32') - elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MAX_LENGTH: - policy['max-length'] = attr.as_scalar('u32') - elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_BITFIELD32_MASK: - policy['bitfield32-mask'] = attr.as_scalar('u32') - elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MASK: - policy['mask'] = attr.as_scalar('u64') - return policy - def annotate_extack(self, attr_space): """ Make extack more human friendly with attribute information """ @@ -333,6 +344,33 @@ def _genl_msg_finalize(msg): return struct.pack("I", len(msg) + 4) + msg +def _genl_decode_policy(raw): + policy = {} + for attr in NlAttrs(raw): + if attr.type == Netlink.NL_POLICY_TYPE_ATTR_TYPE: + type_ = attr.as_scalar('u32') + policy['type'] = Netlink.AttrType(type_).name + elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MIN_VALUE_S: + policy['min-value'] = attr.as_scalar('s64') + elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MAX_VALUE_S: + policy['max-value'] = attr.as_scalar('s64') + elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MIN_VALUE_U: + policy['min-value'] = attr.as_scalar('u64') + elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MAX_VALUE_U: + policy['max-value'] = attr.as_scalar('u64') + elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MIN_LENGTH: + policy['min-length'] = attr.as_scalar('u32') + elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MAX_LENGTH: + policy['max-length'] = attr.as_scalar('u32') + elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_POLICY_IDX: + policy['policy-idx'] = attr.as_scalar('u32') + elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_BITFIELD32_MASK: + policy['bitfield32-mask'] = attr.as_scalar('u32') + elif attr.type == Netlink.NL_POLICY_TYPE_ATTR_MASK: + policy['mask'] = attr.as_scalar('u64') + return policy + + # pylint: disable=too-many-nested-blocks def _genl_load_families(): genl_family_name_to_id = {} @@ -381,6 +419,52 @@ def _genl_load_families(): genl_family_name_to_id[fam['name']] = fam +# pylint: disable=too-many-nested-blocks +def _genl_policy_dump(family_id, op): + op_policy = {} + policy_table = {} + + with socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, Netlink.NETLINK_GENERIC) as sock: + sock.setsockopt(Netlink.SOL_NETLINK, Netlink.NETLINK_CAP_ACK, 1) + + msg = _genl_msg(Netlink.GENL_ID_CTRL, + Netlink.NLM_F_REQUEST | Netlink.NLM_F_ACK | Netlink.NLM_F_DUMP, + Netlink.CTRL_CMD_GETPOLICY, 1) + msg += struct.pack('HHHxx', 6, Netlink.CTRL_ATTR_FAMILY_ID, family_id) + msg += struct.pack('HHI', 8, Netlink.CTRL_ATTR_OP, op) + msg = _genl_msg_finalize(msg) + + sock.send(msg, 0) + + while True: + reply = sock.recv(128 * 1024) + nms = NlMsgs(reply) + for nl_msg in nms: + if nl_msg.error: + raise YnlException(f"Netlink error: {nl_msg.error}") + if nl_msg.done: + return op_policy, policy_table + + gm = GenlMsg(nl_msg) + for attr in NlAttrs(gm.raw): + if attr.type == Netlink.CTRL_ATTR_OP_POLICY: + for op_attr in NlAttrs(attr.raw): + for method_attr in NlAttrs(op_attr.raw): + if method_attr.type == Netlink.CTRL_ATTR_POLICY_DO: + op_policy['do'] = method_attr.as_scalar('u32') + elif method_attr.type == Netlink.CTRL_ATTR_POLICY_DUMP: + op_policy['dump'] = method_attr.as_scalar('u32') + elif attr.type == Netlink.CTRL_ATTR_POLICY: + for pidx_attr in NlAttrs(attr.raw): + policy_idx = pidx_attr.type + for aid_attr in NlAttrs(pidx_attr.raw): + attr_id = aid_attr.type + decoded = _genl_decode_policy(aid_attr.raw) + if policy_idx not in policy_table: + policy_table[policy_idx] = {} + policy_table[policy_idx][attr_id] = decoded + + class GenlMsg: def __init__(self, nl_msg): self.nl = nl_msg @@ -488,6 +572,37 @@ class SpaceAttrs: class YnlFamily(SpecFamily): + """ + YNL family -- a Netlink interface built from a YAML spec. + + Primary use of the class is to execute Netlink commands: + + ynl.<op_name>(attrs, ...) + + By default this will execute the <op_name> as "do", pass dump=True + to perform a dump operation. + + ynl.<op_name> is a shorthand / convenience wrapper for the following + methods which take the op_name as a string: + + ynl.do(op_name, attrs, flags=None) -- execute a do operation + ynl.dump(op_name, attrs) -- execute a dump operation + ynl.do_multi(ops) -- batch multiple do operations + + The flags argument in ynl.do() allows passing in extra NLM_F_* flags + which may be necessary for old families. + + Notification API: + + ynl.ntf_subscribe(mcast_name) -- join a multicast group + ynl.check_ntf() -- drain pending notifications + ynl.poll_ntf(duration=None) -- yield notifications + + Policy introspection allows querying validation criteria from the running + kernel. Allows checking whether kernel supports a given attribute or value. + + ynl.get_policy(op_name, mode) -- query kernel policy for an op + """ def __init__(self, def_path, schema=None, process_unknown=False, recv_size=0): super().__init__(def_path, schema) @@ -814,7 +929,9 @@ class YnlFamily(SpecFamily): continue try: - if attr_spec["type"] == 'nest': + if attr_spec["type"] == 'pad': + continue + elif attr_spec["type"] == 'nest': subdict = self._decode(NlAttrs(attr.raw), attr_spec['nested-attributes'], search_attrs) @@ -1190,3 +1307,51 @@ class YnlFamily(SpecFamily): def do_multi(self, ops): return self._ops(ops) + + def _resolve_policy(self, policy_idx, policy_table, attr_set): + attrs = {} + if policy_idx not in policy_table: + return attrs + for attr_id, decoded in policy_table[policy_idx].items(): + if attr_set and attr_id in attr_set.attrs_by_val: + spec = attr_set.attrs_by_val[attr_id] + name = spec['name'] + else: + spec = None + name = f'attr-{attr_id}' + if 'policy-idx' in decoded: + sub_set = None + if spec and 'nested-attributes' in spec.yaml: + sub_set = self.attr_sets[spec.yaml['nested-attributes']] + nested = self._resolve_policy(decoded['policy-idx'], + policy_table, sub_set) + del decoded['policy-idx'] + decoded['policy'] = nested + attrs[name] = decoded + return attrs + + def get_policy(self, op_name, mode): + """Query running kernel for the Netlink policy of an operation. + + Allows checking whether kernel supports a given attribute or value. + This method consults the running kernel, not the YAML spec. + + Args: + op_name: operation name as it appears in the YAML spec + mode: 'do' or 'dump' + + Returns: + NlPolicy with an attrs dict mapping attribute names to + their policy properties (type, min/max, nested, etc.), + or None if the operation has no policy for the given mode. + Empty policy usually implies that the operation rejects + all attributes. + """ + op = self.ops[op_name] + op_policy, policy_table = _genl_policy_dump(self.nlproto.family_id, + op.req_value) + if mode not in op_policy: + return None + policy_idx = op_policy[mode] + attrs = self._resolve_policy(policy_idx, policy_table, op.attr_set) + return NlPolicy(attrs) diff --git a/tools/testing/selftests/net/lib/py/ynl.py b/tools/testing/selftests/net/lib/py/ynl.py index b42986bf39e3..e8aa97936f6c 100644 --- a/tools/testing/selftests/net/lib/py/ynl.py +++ b/tools/testing/selftests/net/lib/py/ynl.py @@ -13,14 +13,14 @@ try: SPEC_PATH = KSFT_DIR / "net/lib/specs" sys.path.append(tools_full_path.as_posix()) - from net.lib.ynl.pyynl.lib import YnlFamily, NlError, Netlink + from net.lib.ynl.pyynl.lib import YnlFamily, NlError, NlPolicy, Netlink else: # Running in tree tools_full_path = KSRC / "tools" SPEC_PATH = KSRC / "Documentation/netlink/specs" sys.path.append(tools_full_path.as_posix()) - from net.ynl.pyynl.lib import YnlFamily, NlError, Netlink + from net.ynl.pyynl.lib import YnlFamily, NlError, NlPolicy, Netlink except ModuleNotFoundError as e: ksft_pr("Failed importing `ynl` library from kernel sources") ksft_pr(str(e)) @@ -28,7 +28,7 @@ except ModuleNotFoundError as e: sys.exit(4) __all__ = [ - "NlError", "Netlink", "YnlFamily", "SPEC_PATH", + "NlError", "NlPolicy", "Netlink", "YnlFamily", "SPEC_PATH", "EthtoolFamily", "RtnlFamily", "RtnlAddrFamily", "NetdevFamily", "NetshaperFamily", "DevlinkFamily", "PSPFamily", ] |
