public inbox for netdev@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH net-next 0/5] tools: ynl: policy query support
@ 2026-03-10  0:53 Jakub Kicinski
  2026-03-10  0:53 ` [PATCH net-next 1/5] tools: ynl: handle pad type during decode Jakub Kicinski
                   ` (5 more replies)
  0 siblings, 6 replies; 10+ messages in thread
From: Jakub Kicinski @ 2026-03-10  0:53 UTC (permalink / raw)
  To: davem, donald.hunter
  Cc: netdev, edumazet, pabeni, andrew+netdev, horms, shuah, sdf,
	linux-kselftest, Jakub Kicinski

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.

Jakub Kicinski (5):
  tools: ynl: handle pad type during decode
  tools: ynl: move policy decoding out of NlMsg
  tools: ynl: add short doc to class YnlFamily
  tools: ynl: add Python API for easier access to policies
  tools: ynl: cli: add --policy support

 tools/net/ynl/pyynl/cli.py                |  12 ++
 tools/net/ynl/pyynl/lib/__init__.py       |   5 +-
 tools/net/ynl/pyynl/lib/ynl.py            | 216 +++++++++++++++++++---
 tools/testing/selftests/net/lib/py/ynl.py |   6 +-
 4 files changed, 208 insertions(+), 31 deletions(-)

-- 
2.53.0


^ permalink raw reply	[flat|nested] 10+ messages in thread

* [PATCH net-next 1/5] tools: ynl: handle pad type during decode
  2026-03-10  0:53 [PATCH net-next 0/5] tools: ynl: policy query support Jakub Kicinski
@ 2026-03-10  0:53 ` Jakub Kicinski
  2026-03-10  0:53 ` [PATCH net-next 2/5] tools: ynl: move policy decoding out of NlMsg Jakub Kicinski
                   ` (4 subsequent siblings)
  5 siblings, 0 replies; 10+ messages in thread
From: Jakub Kicinski @ 2026-03-10  0:53 UTC (permalink / raw)
  To: davem, donald.hunter
  Cc: netdev, edumazet, pabeni, andrew+netdev, horms, shuah, sdf,
	linux-kselftest, Jakub Kicinski

Apparently Python code only handled the 'pad' type in structs
until now. Add it to attr decoding. nlctrl policy dumps need it.

Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
 tools/net/ynl/pyynl/lib/ynl.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/tools/net/ynl/pyynl/lib/ynl.py b/tools/net/ynl/pyynl/lib/ynl.py
index 9774005e7ad1..8302c19ab55c 100644
--- a/tools/net/ynl/pyynl/lib/ynl.py
+++ b/tools/net/ynl/pyynl/lib/ynl.py
@@ -814,7 +814,9 @@ from .nlspec import 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)
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH net-next 2/5] tools: ynl: move policy decoding out of NlMsg
  2026-03-10  0:53 [PATCH net-next 0/5] tools: ynl: policy query support Jakub Kicinski
  2026-03-10  0:53 ` [PATCH net-next 1/5] tools: ynl: handle pad type during decode Jakub Kicinski
@ 2026-03-10  0:53 ` Jakub Kicinski
  2026-03-10  0:53 ` [PATCH net-next 3/5] tools: ynl: add short doc to class YnlFamily Jakub Kicinski
                   ` (3 subsequent siblings)
  5 siblings, 0 replies; 10+ messages in thread
From: Jakub Kicinski @ 2026-03-10  0:53 UTC (permalink / raw)
  To: davem, donald.hunter
  Cc: netdev, edumazet, pabeni, andrew+netdev, horms, shuah, sdf,
	linux-kselftest, Jakub Kicinski

We'll soon need to decode policies from dump so move _decode_policy()
out of class NlMsg.

Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
 tools/net/ynl/pyynl/lib/ynl.py | 53 ++++++++++++++++++----------------
 1 file changed, 28 insertions(+), 25 deletions(-)

diff --git a/tools/net/ynl/pyynl/lib/ynl.py b/tools/net/ynl/pyynl/lib/ynl.py
index 8302c19ab55c..17482c17a976 100644
--- a/tools/net/ynl/pyynl/lib/ynl.py
+++ b/tools/net/ynl/pyynl/lib/ynl.py
@@ -247,7 +247,7 @@ from .nlspec import SpecFamily
                 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 +256,6 @@ from .nlspec import SpecFamily
             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 +309,33 @@ from .nlspec import SpecFamily
     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 = {}
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH net-next 3/5] tools: ynl: add short doc to class YnlFamily
  2026-03-10  0:53 [PATCH net-next 0/5] tools: ynl: policy query support Jakub Kicinski
  2026-03-10  0:53 ` [PATCH net-next 1/5] tools: ynl: handle pad type during decode Jakub Kicinski
  2026-03-10  0:53 ` [PATCH net-next 2/5] tools: ynl: move policy decoding out of NlMsg Jakub Kicinski
@ 2026-03-10  0:53 ` Jakub Kicinski
  2026-03-10  0:53 ` [PATCH net-next 4/5] tools: ynl: add Python API for easier access to policies Jakub Kicinski
                   ` (2 subsequent siblings)
  5 siblings, 0 replies; 10+ messages in thread
From: Jakub Kicinski @ 2026-03-10  0:53 UTC (permalink / raw)
  To: davem, donald.hunter
  Cc: netdev, edumazet, pabeni, andrew+netdev, horms, shuah, sdf,
	linux-kselftest, Jakub Kicinski

The class is quite long. It's getting hard to find the user-facing
methods. Add a short doc at the class level explaining the main API.

Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
 tools/net/ynl/pyynl/lib/ynl.py | 26 ++++++++++++++++++++++++++
 1 file changed, 26 insertions(+)

diff --git a/tools/net/ynl/pyynl/lib/ynl.py b/tools/net/ynl/pyynl/lib/ynl.py
index 17482c17a976..ec4d95f583fe 100644
--- a/tools/net/ynl/pyynl/lib/ynl.py
+++ b/tools/net/ynl/pyynl/lib/ynl.py
@@ -491,6 +491,32 @@ from .nlspec import SpecFamily
 
 
 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
+    """
     def __init__(self, def_path, schema=None, process_unknown=False,
                  recv_size=0):
         super().__init__(def_path, schema)
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH net-next 4/5] tools: ynl: add Python API for easier access to policies
  2026-03-10  0:53 [PATCH net-next 0/5] tools: ynl: policy query support Jakub Kicinski
                   ` (2 preceding siblings ...)
  2026-03-10  0:53 ` [PATCH net-next 3/5] tools: ynl: add short doc to class YnlFamily Jakub Kicinski
@ 2026-03-10  0:53 ` Jakub Kicinski
  2026-03-10  0:53 ` [PATCH net-next 5/5] tools: ynl: cli: add --policy support Jakub Kicinski
  2026-03-11  2:40 ` [PATCH net-next 0/5] tools: ynl: policy query support patchwork-bot+netdevbpf
  5 siblings, 0 replies; 10+ messages in thread
From: Jakub Kicinski @ 2026-03-10  0:53 UTC (permalink / raw)
  To: davem, donald.hunter
  Cc: netdev, edumazet, pabeni, andrew+netdev, horms, shuah, sdf,
	linux-kselftest, Jakub Kicinski

The format of Netlink policy dump is a bit curious with messages
in the same dump carrying both attrs and mapping info. Plus each
message carries a single piece of the puzzle the caller must then
reassemble.

I need to do this reassembly for a test, but I think it's generally
useful. So let's add proper support to YnlFamily to return more
user-friendly representation. See the various docs in the patch
for more details.

Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
 tools/net/ynl/pyynl/lib/__init__.py       |   5 +-
 tools/net/ynl/pyynl/lib/ynl.py            | 133 ++++++++++++++++++++++
 tools/testing/selftests/net/lib/py/ynl.py |   6 +-
 3 files changed, 139 insertions(+), 5 deletions(-)

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 ec4d95f583fe..e6fcedb597a8 100644
--- a/tools/net/ynl/pyynl/lib/ynl.py
+++ b/tools/net/ynl/pyynl/lib/ynl.py
@@ -77,15 +77,22 @@ from .nlspec import SpecFamily
 
     # 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 @@ from .nlspec import SpecFamily
     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 = {
@@ -384,6 +419,52 @@ from .nlspec import SpecFamily
                     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
@@ -516,6 +597,11 @@ from .nlspec import SpecFamily
       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):
@@ -1221,3 +1307,50 @@ from .nlspec import 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.
+            No 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 @@ from .ksft import ksft_pr, ktap_result
         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 @@ from .ksft import ksft_pr, ktap_result
     sys.exit(4)
 
 __all__ = [
-    "NlError", "Netlink", "YnlFamily", "SPEC_PATH",
+    "NlError", "NlPolicy", "Netlink", "YnlFamily", "SPEC_PATH",
     "EthtoolFamily", "RtnlFamily", "RtnlAddrFamily",
     "NetdevFamily", "NetshaperFamily", "DevlinkFamily", "PSPFamily",
 ]
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* [PATCH net-next 5/5] tools: ynl: cli: add --policy support
  2026-03-10  0:53 [PATCH net-next 0/5] tools: ynl: policy query support Jakub Kicinski
                   ` (3 preceding siblings ...)
  2026-03-10  0:53 ` [PATCH net-next 4/5] tools: ynl: add Python API for easier access to policies Jakub Kicinski
@ 2026-03-10  0:53 ` Jakub Kicinski
  2026-03-11  2:40 ` [PATCH net-next 0/5] tools: ynl: policy query support patchwork-bot+netdevbpf
  5 siblings, 0 replies; 10+ messages in thread
From: Jakub Kicinski @ 2026-03-10  0:53 UTC (permalink / raw)
  To: davem, donald.hunter
  Cc: netdev, edumazet, pabeni, andrew+netdev, horms, shuah, sdf,
	linux-kselftest, Jakub Kicinski

Add --policy flag which can be combined with --do or --dump to query
the kernel's netlink policy for an operation instead of executing it.

Examples:

  $ ynl --family netdev --do dev-get --policy
  {'ifindex': {'max-value': 4294967295, 'min-value': 1, 'type': 'u32'}}

  $ ynl --family ethtool --do channels-get --policy --output-json
  {"header": {"type": "nested", "policy": {"dev-index": ...}}}

  $ ynl --family netdev --dump dev-get --policy
  None

Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
 tools/net/ynl/pyynl/cli.py | 12 ++++++++++++
 1 file changed, 12 insertions(+)

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 @@ RELATIVE_SCHEMA_DIR='../../../../Documentation/netlink'
     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 @@ RELATIVE_SCHEMA_DIR='../../../../Documentation/netlink'
     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)
 
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 10+ messages in thread

* Re: [PATCH net-next 0/5] tools: ynl: policy query support
  2026-03-10  0:53 [PATCH net-next 0/5] tools: ynl: policy query support Jakub Kicinski
                   ` (4 preceding siblings ...)
  2026-03-10  0:53 ` [PATCH net-next 5/5] tools: ynl: cli: add --policy support Jakub Kicinski
@ 2026-03-11  2:40 ` patchwork-bot+netdevbpf
  2026-03-11 11:30   ` Donald Hunter
  5 siblings, 1 reply; 10+ messages in thread
From: patchwork-bot+netdevbpf @ 2026-03-11  2:40 UTC (permalink / raw)
  To: Jakub Kicinski
  Cc: davem, donald.hunter, netdev, edumazet, pabeni, andrew+netdev,
	horms, shuah, sdf, linux-kselftest

Hello:

This series was applied to netdev/net-next.git (main)
by Jakub Kicinski <kuba@kernel.org>:

On Mon,  9 Mar 2026 17:53:32 -0700 you wrote:
> 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.
> 
> [...]

Here is the summary with links:
  - [net-next,1/5] tools: ynl: handle pad type during decode
    https://git.kernel.org/netdev/net-next/c/7b1309c33927
  - [net-next,2/5] tools: ynl: move policy decoding out of NlMsg
    https://git.kernel.org/netdev/net-next/c/c26fda6212b8
  - [net-next,3/5] tools: ynl: add short doc to class YnlFamily
    https://git.kernel.org/netdev/net-next/c/8bbcfce5db97
  - [net-next,4/5] tools: ynl: add Python API for easier access to policies
    https://git.kernel.org/netdev/net-next/c/77a6401a8722
  - [net-next,5/5] tools: ynl: cli: add --policy support
    https://git.kernel.org/netdev/net-next/c/d6df5e9b2a56

You are awesome, thank you!
-- 
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html



^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH net-next 0/5] tools: ynl: policy query support
  2026-03-11  2:40 ` [PATCH net-next 0/5] tools: ynl: policy query support patchwork-bot+netdevbpf
@ 2026-03-11 11:30   ` Donald Hunter
  2026-03-11 18:35     ` Jakub Kicinski
  0 siblings, 1 reply; 10+ messages in thread
From: Donald Hunter @ 2026-03-11 11:30 UTC (permalink / raw)
  To: Jakub Kicinski
  Cc: davem, netdev, edumazet, pabeni, andrew+netdev, horms, shuah, sdf,
	linux-kselftest

patchwork-bot+netdevbpf@kernel.org writes:

> Hello:
>
> This series was applied to netdev/net-next.git (main)
> by Jakub Kicinski <kuba@kernel.org>:
>
> On Mon,  9 Mar 2026 17:53:32 -0700 you wrote:
>> 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.

I was mid review, looking at a couple of issues:

- It would be good to fail more gracefully for netlink-raw families
- I get "RecursionError: maximum recursion depth exceeded" for nl80211

Happy to address these in a followup.

Cheers,
Donald.

^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH net-next 0/5] tools: ynl: policy query support
  2026-03-11 11:30   ` Donald Hunter
@ 2026-03-11 18:35     ` Jakub Kicinski
  2026-03-12 17:17       ` Donald Hunter
  0 siblings, 1 reply; 10+ messages in thread
From: Jakub Kicinski @ 2026-03-11 18:35 UTC (permalink / raw)
  To: Donald Hunter
  Cc: davem, netdev, edumazet, pabeni, andrew+netdev, horms, shuah, sdf,
	linux-kselftest

On Wed, 11 Mar 2026 11:30:59 +0000 Donald Hunter wrote:
> > On Mon,  9 Mar 2026 17:53:32 -0700 you wrote:  
> >> 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.  
> 
> I was mid review

Sorry about that! :( I didn't see any reply to the one liner for a few
days I thought you may be AFK :)

> looking at a couple of issues:
> 
> - It would be good to fail more gracefully for netlink-raw families

This one I saw, but I figured the message that gets printed is...
reasonable. For low level functionality like policies we should 
assume user is relatively advanced? The policies are spotty
even in genetlink, a lot of families don't link them up properly :(
So explaining all of this is a bit of a rabbit hole.

> - I get "RecursionError: maximum recursion depth exceeded" for nl80211

:o Missed this one completely! My feeling is that we should lean into
the NlPolicy class more, and avoid rendering the full structure upfront.
WDYT about the following?

diff --git a/tools/net/ynl/pyynl/cli.py b/tools/net/ynl/pyynl/cli.py
index fc9e84754e4b..ff0cfbdc82e4 100755
--- a/tools/net/ynl/pyynl/cli.py
+++ b/tools/net/ynl/pyynl/cli.py
@@ -16,7 +16,8 @@ import textwrap
 
 # pylint: disable=no-name-in-module,wrong-import-position
 sys.path.append(pathlib.Path(__file__).resolve().parent.as_posix())
-from lib import YnlFamily, Netlink, NlError, SpecFamily, SpecException, YnlException
+from lib import SpecFamily, SpecException
+from lib import YnlFamily, Netlink, NlError, NlPolicy, YnlException
 
 SYS_SCHEMA_DIR='/usr/share/ynl'
 RELATIVE_SCHEMA_DIR='../../../../Documentation/netlink'
@@ -79,6 +80,8 @@ RELATIVE_SCHEMA_DIR='../../../../Documentation/netlink'
             return bytes.hex(o)
         if isinstance(o, set):
             return sorted(o)
+        if isinstance(o, NlPolicy):
+            return o.to_dict()
         return json.JSONEncoder.default(self, o)
 
 
@@ -313,11 +316,11 @@ RELATIVE_SCHEMA_DIR='../../../../Documentation/netlink'
     if args.policy:
         if args.do:
             pol = ynl.get_policy(args.do, 'do')
-            output(pol.attrs if pol else None)
+            output(pol if pol else None)
             args.do = None
         if args.dump:
             pol = ynl.get_policy(args.dump, 'dump')
-            output(pol.attrs if pol else None)
+            output(pol if pol else None)
             args.dump = None
 
     if args.ntf:
diff --git a/tools/net/ynl/pyynl/lib/ynl.py b/tools/net/ynl/pyynl/lib/ynl.py
index 0eedeee465d8..4411f1902ae4 100644
--- a/tools/net/ynl/pyynl/lib/ynl.py
+++ b/tools/net/ynl/pyynl/lib/ynl.py
@@ -143,32 +143,113 @@ from .nlspec import SpecFamily
     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.
+    Returned by YnlFamily.get_policy(). Attributes of the policy
+    are accessible as attributes of the object. Nested policies
+    can be accessed indexing the object like a dictionary::
 
-    Attributes:
-        attrs: dict mapping attribute names to policy dicts, e.g.
-        page-pool-stats-get do policy::
+        pol = ynl.get_policy('page-pool-stats-get', 'do')
+        pol['info'].type            # 'nested'
+        pol['info']['id'].type      # 'uint'
+        pol['info']['id'].min_value # 1
 
-            {
-                'info': {'type': 'nested', 'policy': {
-                    'id':      {'type': 'uint', 'min-value': 1,
-                                'max-value': 4294967295},
-                    'ifindex': {'type': 'u32', 'min-value': 1,
-                                'max-value': 2147483647},
-                }},
-            }
+    Each policy entry always has a 'type' attribute (e.g. u32, string,
+    nested). Optional attributes depending on the 'type': min-value,
+    max-value, min-length, max-length, mask.
 
-        Each policy dict always contains 'type' (e.g. u32, string,
-        nested). Optional keys: min-value, max-value, min-length,
-        max-length, mask, policy.
+    Policies can form infinite nesting loops. These loops are trimmed
+    when policy is converted to a dict with pol.to_dict().
     """
-    def __init__(self, attrs):
-        self.attrs = attrs
+    def __init__(self, ynl, policy_idx, policy_table, attr_set, props=None):
+        self._policy_idx = policy_idx
+        self._policy_table = policy_table
+        self._ynl = ynl
+        self._props = props or {}
+        self._entries = {}
+        if policy_idx is not None and policy_idx in policy_table:
+            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}'
+                self._entries[name] = (spec, decoded)
+
+    def __getitem__(self, name):
+        """Descend into a nested policy by attribute name."""
+        spec, decoded = self._entries[name]
+        props = dict(decoded)
+        child_idx = None
+        child_set = None
+        if 'policy-idx' in props:
+            child_idx = props.pop('policy-idx')
+            if spec and 'nested-attributes' in spec.yaml:
+                child_set = self._ynl.attr_sets[spec.yaml['nested-attributes']]
+        return NlPolicy(self._ynl, child_idx, self._policy_table,
+                        child_set, props)
+
+    def __getattr__(self, name):
+        """Access this policy entry's own properties (type, min-value, etc.).
+
+        Underscores in the name are converted to dashes, so that
+        pol.min_value looks up "min-value".
+        """
+        key = name.replace('_', '-')
+        try:
+            # Hack for level-0 which we still want to have .type but we don't
+            # want type to pointlessly show up in the dict / JSON form.
+            if not self._props and name == "type":
+                return "nested"
+            return self._props[key]
+        except KeyError:
+            raise AttributeError(name)
+
+    def get(self, name, default=None):
+        """Look up a child policy entry by attribute name, with a default."""
+        try:
+            return self[name]
+        except KeyError:
+            return default
+
+    def __contains__(self, name):
+        return name in self._entries
+
+    def __len__(self):
+        return len(self._entries)
+
+    def __iter__(self):
+        return iter(self._entries)
+
+    def keys(self):
+        """Return attribute names accepted by this policy."""
+        return self._entries.keys()
+
+    def to_dict(self, seen=None):
+        """Convert to a plain dict, suitable for JSON serialization.
+
+        Nested NlPolicy objects are expanded recursively. Cyclic
+        references are trimmed (resolved to just {"type": "nested"}).
+        """
+        if seen is None:
+            seen = set()
+        result = dict(self._props)
+        if self._policy_idx is not None:
+            if self._policy_idx not in seen:
+                seen = seen | {self._policy_idx}
+                children = {}
+                for name in self:
+                    children[name] = self[name].to_dict(seen)
+                if self._props:
+                    result['policy'] = children
+                else:
+                    result = children
+        return result
+
+    def __repr__(self):
+        return repr(self.to_dict())
 
 
 class NlAttr:
@@ -1308,28 +1389,6 @@ from .nlspec import 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.
 
@@ -1341,8 +1400,8 @@ from .nlspec import SpecFamily
             mode: 'do' or 'dump'
 
         Returns:
-            NlPolicy with an attrs dict mapping attribute names to
-            their policy properties (type, min/max, nested, etc.),
+            NlPolicy acting as a read-only 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.
@@ -1353,5 +1412,4 @@ from .nlspec import SpecFamily
         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)
+        return NlPolicy(self, policy_idx, policy_table, op.attr_set)

^ permalink raw reply related	[flat|nested] 10+ messages in thread

* Re: [PATCH net-next 0/5] tools: ynl: policy query support
  2026-03-11 18:35     ` Jakub Kicinski
@ 2026-03-12 17:17       ` Donald Hunter
  0 siblings, 0 replies; 10+ messages in thread
From: Donald Hunter @ 2026-03-12 17:17 UTC (permalink / raw)
  To: Jakub Kicinski
  Cc: davem, netdev, edumazet, pabeni, andrew+netdev, horms, shuah, sdf,
	linux-kselftest

Jakub Kicinski <kuba@kernel.org> writes:

> On Wed, 11 Mar 2026 11:30:59 +0000 Donald Hunter wrote:
>> > On Mon,  9 Mar 2026 17:53:32 -0700 you wrote:  
>> >> 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.  
>> 
>> I was mid review
>
> Sorry about that! :( I didn't see any reply to the one liner for a few
> days I thought you may be AFK :)
>
>> looking at a couple of issues:
>> 
>> - It would be good to fail more gracefully for netlink-raw families
>
> This one I saw, but I figured the message that gets printed is...
> reasonable. For low level functionality like policies we should 
> assume user is relatively advanced? The policies are spotty
> even in genetlink, a lot of families don't link them up properly :(
> So explaining all of this is a bit of a rabbit hole.

That's fair. I had it in mind to work on avoiding emitting stack traces
for "normal usage". If I get to that then I can see about handling this
case as well.

>> - I get "RecursionError: maximum recursion depth exceeded" for nl80211
>
> :o Missed this one completely! My feeling is that we should lean into
> the NlPolicy class more, and avoid rendering the full structure upfront.
> WDYT about the following?

I agree with the approach. The code below looks reasonable, though I
haven't been thorough - I'll wait for the patch.

> diff --git a/tools/net/ynl/pyynl/cli.py b/tools/net/ynl/pyynl/cli.py
> index fc9e84754e4b..ff0cfbdc82e4 100755
> --- a/tools/net/ynl/pyynl/cli.py
> +++ b/tools/net/ynl/pyynl/cli.py
> @@ -16,7 +16,8 @@ import textwrap
>  
>  # pylint: disable=no-name-in-module,wrong-import-position
>  sys.path.append(pathlib.Path(__file__).resolve().parent.as_posix())
> -from lib import YnlFamily, Netlink, NlError, SpecFamily, SpecException, YnlException
> +from lib import SpecFamily, SpecException
> +from lib import YnlFamily, Netlink, NlError, NlPolicy, YnlException
>  
>  SYS_SCHEMA_DIR='/usr/share/ynl'
>  RELATIVE_SCHEMA_DIR='../../../../Documentation/netlink'
> @@ -79,6 +80,8 @@ RELATIVE_SCHEMA_DIR='../../../../Documentation/netlink'
>              return bytes.hex(o)
>          if isinstance(o, set):
>              return sorted(o)
> +        if isinstance(o, NlPolicy):
> +            return o.to_dict()
>          return json.JSONEncoder.default(self, o)
>  
>  
> @@ -313,11 +316,11 @@ RELATIVE_SCHEMA_DIR='../../../../Documentation/netlink'
>      if args.policy:
>          if args.do:
>              pol = ynl.get_policy(args.do, 'do')
> -            output(pol.attrs if pol else None)
> +            output(pol if pol else None)
>              args.do = None
>          if args.dump:
>              pol = ynl.get_policy(args.dump, 'dump')
> -            output(pol.attrs if pol else None)
> +            output(pol if pol else None)
>              args.dump = None
>  
>      if args.ntf:
> diff --git a/tools/net/ynl/pyynl/lib/ynl.py b/tools/net/ynl/pyynl/lib/ynl.py
> index 0eedeee465d8..4411f1902ae4 100644
> --- a/tools/net/ynl/pyynl/lib/ynl.py
> +++ b/tools/net/ynl/pyynl/lib/ynl.py
> @@ -143,32 +143,113 @@ from .nlspec import SpecFamily
>      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.
> +    Returned by YnlFamily.get_policy(). Attributes of the policy
> +    are accessible as attributes of the object. Nested policies
> +    can be accessed indexing the object like a dictionary::
>  
> -    Attributes:
> -        attrs: dict mapping attribute names to policy dicts, e.g.
> -        page-pool-stats-get do policy::
> +        pol = ynl.get_policy('page-pool-stats-get', 'do')
> +        pol['info'].type            # 'nested'
> +        pol['info']['id'].type      # 'uint'
> +        pol['info']['id'].min_value # 1
>  
> -            {
> -                'info': {'type': 'nested', 'policy': {
> -                    'id':      {'type': 'uint', 'min-value': 1,
> -                                'max-value': 4294967295},
> -                    'ifindex': {'type': 'u32', 'min-value': 1,
> -                                'max-value': 2147483647},
> -                }},
> -            }
> +    Each policy entry always has a 'type' attribute (e.g. u32, string,
> +    nested). Optional attributes depending on the 'type': min-value,
> +    max-value, min-length, max-length, mask.
>  
> -        Each policy dict always contains 'type' (e.g. u32, string,
> -        nested). Optional keys: min-value, max-value, min-length,
> -        max-length, mask, policy.
> +    Policies can form infinite nesting loops. These loops are trimmed
> +    when policy is converted to a dict with pol.to_dict().
>      """
> -    def __init__(self, attrs):
> -        self.attrs = attrs
> +    def __init__(self, ynl, policy_idx, policy_table, attr_set, props=None):
> +        self._policy_idx = policy_idx
> +        self._policy_table = policy_table
> +        self._ynl = ynl
> +        self._props = props or {}
> +        self._entries = {}
> +        if policy_idx is not None and policy_idx in policy_table:
> +            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}'
> +                self._entries[name] = (spec, decoded)
> +
> +    def __getitem__(self, name):
> +        """Descend into a nested policy by attribute name."""
> +        spec, decoded = self._entries[name]
> +        props = dict(decoded)
> +        child_idx = None
> +        child_set = None
> +        if 'policy-idx' in props:
> +            child_idx = props.pop('policy-idx')
> +            if spec and 'nested-attributes' in spec.yaml:
> +                child_set = self._ynl.attr_sets[spec.yaml['nested-attributes']]
> +        return NlPolicy(self._ynl, child_idx, self._policy_table,
> +                        child_set, props)
> +
> +    def __getattr__(self, name):
> +        """Access this policy entry's own properties (type, min-value, etc.).
> +
> +        Underscores in the name are converted to dashes, so that
> +        pol.min_value looks up "min-value".
> +        """
> +        key = name.replace('_', '-')
> +        try:
> +            # Hack for level-0 which we still want to have .type but we don't
> +            # want type to pointlessly show up in the dict / JSON form.
> +            if not self._props and name == "type":
> +                return "nested"
> +            return self._props[key]
> +        except KeyError:
> +            raise AttributeError(name)
> +
> +    def get(self, name, default=None):
> +        """Look up a child policy entry by attribute name, with a default."""
> +        try:
> +            return self[name]
> +        except KeyError:
> +            return default
> +
> +    def __contains__(self, name):
> +        return name in self._entries
> +
> +    def __len__(self):
> +        return len(self._entries)
> +
> +    def __iter__(self):
> +        return iter(self._entries)
> +
> +    def keys(self):
> +        """Return attribute names accepted by this policy."""
> +        return self._entries.keys()
> +
> +    def to_dict(self, seen=None):
> +        """Convert to a plain dict, suitable for JSON serialization.
> +
> +        Nested NlPolicy objects are expanded recursively. Cyclic
> +        references are trimmed (resolved to just {"type": "nested"}).
> +        """
> +        if seen is None:
> +            seen = set()
> +        result = dict(self._props)
> +        if self._policy_idx is not None:
> +            if self._policy_idx not in seen:
> +                seen = seen | {self._policy_idx}
> +                children = {}
> +                for name in self:
> +                    children[name] = self[name].to_dict(seen)
> +                if self._props:
> +                    result['policy'] = children
> +                else:
> +                    result = children
> +        return result
> +
> +    def __repr__(self):
> +        return repr(self.to_dict())
>  
>  
>  class NlAttr:
> @@ -1308,28 +1389,6 @@ from .nlspec import 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.
>  
> @@ -1341,8 +1400,8 @@ from .nlspec import SpecFamily
>              mode: 'do' or 'dump'
>  
>          Returns:
> -            NlPolicy with an attrs dict mapping attribute names to
> -            their policy properties (type, min/max, nested, etc.),
> +            NlPolicy acting as a read-only 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.
> @@ -1353,5 +1412,4 @@ from .nlspec import SpecFamily
>          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)
> +        return NlPolicy(self, policy_idx, policy_table, op.attr_set)

^ permalink raw reply	[flat|nested] 10+ messages in thread

end of thread, other threads:[~2026-03-12 17:18 UTC | newest]

Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-10  0:53 [PATCH net-next 0/5] tools: ynl: policy query support Jakub Kicinski
2026-03-10  0:53 ` [PATCH net-next 1/5] tools: ynl: handle pad type during decode Jakub Kicinski
2026-03-10  0:53 ` [PATCH net-next 2/5] tools: ynl: move policy decoding out of NlMsg Jakub Kicinski
2026-03-10  0:53 ` [PATCH net-next 3/5] tools: ynl: add short doc to class YnlFamily Jakub Kicinski
2026-03-10  0:53 ` [PATCH net-next 4/5] tools: ynl: add Python API for easier access to policies Jakub Kicinski
2026-03-10  0:53 ` [PATCH net-next 5/5] tools: ynl: cli: add --policy support Jakub Kicinski
2026-03-11  2:40 ` [PATCH net-next 0/5] tools: ynl: policy query support patchwork-bot+netdevbpf
2026-03-11 11:30   ` Donald Hunter
2026-03-11 18:35     ` Jakub Kicinski
2026-03-12 17:17       ` Donald Hunter

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox