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>
This commit is contained in:
Jakub Kicinski 2026-03-10 19:33:07 -07:00
commit 7bb1970494
4 changed files with 209 additions and 31 deletions

View File

@ -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)

View File

@ -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"]

View File

@ -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)

View File

@ -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",
]