* [PATCH net-next 2/2] tools: ynl: pyynl: pull the --family resolution logic into the lib
2026-06-30 0:14 [PATCH net-next 0/2] tools: ynl: pyynl: minor library ease of use improvements Jakub Kicinski
2026-06-30 0:14 ` [PATCH net-next 1/2] tools: ynl: pyynl: re-export the library API from the package root Jakub Kicinski
@ 2026-06-30 0:14 ` Jakub Kicinski
2026-06-30 10:10 ` Donald Hunter
1 sibling, 1 reply; 6+ messages in thread
From: Jakub Kicinski @ 2026-06-30 0:14 UTC (permalink / raw)
To: davem
Cc: netdev, edumazet, pabeni, andrew+netdev, horms, donald.hunter,
sdf, gal, jstancek, ast, Jakub Kicinski
When packaging YNL as a system level utility we added a --family
argument which auto-resolves the full spec path from a well known
path in /usr/share. Spelling out full YAML spec files is at this
point only done in-tree, for example in the selftests which need
the very latest YAML. But the selftests have their own wrapping
classes for each family so test authors aren't really bothered
by having to spell the paths out.
Afford the same ease of use to the Python library users.
Move the path resolution from the CLI code to the library.
This simplifies the pyynl use by a lot:
from pyynl import YnlFamily
ynl = YnlFamily(family="netdev")
Unless I'm missing a trick, resolving the /usr/share path
is hard enough for most users to lean towards shelling out
to ynl CLI with --output-json, which is sad.
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
---
tools/net/ynl/pyynl/cli.py | 53 +++++------------------------
tools/net/ynl/pyynl/lib/__init__.py | 3 +-
tools/net/ynl/pyynl/lib/nlspec.py | 14 +++++++-
tools/net/ynl/pyynl/lib/specdir.py | 51 +++++++++++++++++++++++++++
tools/net/ynl/pyynl/lib/ynl.py | 19 +++++++++--
5 files changed, 91 insertions(+), 49 deletions(-)
create mode 100644 tools/net/ynl/pyynl/lib/specdir.py
diff --git a/tools/net/ynl/pyynl/cli.py b/tools/net/ynl/pyynl/cli.py
index 8275a806cf73..05e422d4709e 100755
--- a/tools/net/ynl/pyynl/cli.py
+++ b/tools/net/ynl/pyynl/cli.py
@@ -17,9 +17,7 @@ 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
-
-SYS_SCHEMA_DIR='/usr/share/ynl'
-RELATIVE_SCHEMA_DIR='../../../../Documentation/netlink'
+from lib import list_families
# pylint: disable=too-few-public-methods,too-many-locals
class Colors:
@@ -48,30 +46,6 @@ RELATIVE_SCHEMA_DIR='../../../../Documentation/netlink'
""" Get terminal width in columns (80 if stdout is not a terminal) """
return shutil.get_terminal_size().columns
-def schema_dir():
- """
- Return the effective schema directory, preferring in-tree before
- system schema directory.
- """
- script_dir = os.path.dirname(os.path.abspath(__file__))
- schema_dir_ = os.path.abspath(f"{script_dir}/{RELATIVE_SCHEMA_DIR}")
- if not os.path.isdir(schema_dir_):
- schema_dir_ = SYS_SCHEMA_DIR
- if not os.path.isdir(schema_dir_):
- raise YnlException(f"Schema directory {schema_dir_} does not exist")
- return schema_dir_
-
-def spec_dir():
- """
- Return the effective spec directory, relative to the effective
- schema directory.
- """
- spec_dir_ = schema_dir() + '/specs'
- if not os.path.isdir(spec_dir_):
- raise YnlException(f"Spec directory {spec_dir_} does not exist")
- return spec_dir_
-
-
class YnlEncoder(json.JSONEncoder):
"""A custom encoder for emitting JSON with ynl-specific instance types"""
def default(self, o):
@@ -272,9 +246,8 @@ RELATIVE_SCHEMA_DIR='../../../../Documentation/netlink'
pprint.pprint(msg, width=term_width(), compact=True)
if args.list_families:
- for filename in sorted(os.listdir(spec_dir())):
- if filename.endswith('.yaml'):
- print(filename.removesuffix('.yaml'))
+ for family in list_families():
+ print(family)
return
if args.no_schema:
@@ -284,28 +257,20 @@ RELATIVE_SCHEMA_DIR='../../../../Documentation/netlink'
if args.json_text:
attrs = json.loads(args.json_text)
- if args.family:
- spec = f"{spec_dir()}/{args.family}.yaml"
- else:
- spec = args.spec
- if not os.path.isfile(spec):
- raise YnlException(f"Spec file {spec} does not exist")
+ if args.spec and not os.path.isfile(args.spec):
+ raise YnlException(f"Spec file {args.spec} does not exist")
+ # Spec/YnlFamily will raise if both or neither spec and family are given
if args.validate:
try:
- SpecFamily(spec, args.schema)
+ SpecFamily(args.spec, schema_path=args.schema, family=args.family)
except SpecException as error:
print(error)
sys.exit(1)
return
- if args.family: # set behaviour when using installed specs
- if args.schema is None and spec.startswith(SYS_SCHEMA_DIR):
- args.schema = '' # disable schema validation when installed
- if args.process_unknown is None:
- args.process_unknown = True
-
- ynl = YnlFamily(spec, args.schema, args.process_unknown,
+ ynl = YnlFamily(args.spec, schema=args.schema, family=args.family,
+ process_unknown=args.process_unknown,
recv_size=args.dbg_small_recv)
if args.dbg_small_recv:
ynl.set_recv_dbg(True)
diff --git a/tools/net/ynl/pyynl/lib/__init__.py b/tools/net/ynl/pyynl/lib/__init__.py
index be741985ae4e..aa4263c8cba9 100644
--- a/tools/net/ynl/pyynl/lib/__init__.py
+++ b/tools/net/ynl/pyynl/lib/__init__.py
@@ -5,12 +5,13 @@
from .nlspec import SpecAttr, SpecAttrSet, SpecEnumEntry, SpecEnumSet, \
SpecFamily, SpecOperation, SpecSubMessage, SpecSubMessageFormat, \
SpecException
+from .specdir import list_families
from .ynl import YnlFamily, Netlink, NlError, NlPolicy, YnlException
from .doc_generator import YnlDocGenerator
__all__ = ["SpecAttr", "SpecAttrSet", "SpecEnumEntry", "SpecEnumSet",
"SpecFamily", "SpecOperation", "SpecSubMessage", "SpecSubMessageFormat",
- "SpecException",
+ "SpecException", "list_families",
"YnlFamily", "Netlink", "NlError", "NlPolicy", "YnlException",
"YnlDocGenerator"]
diff --git a/tools/net/ynl/pyynl/lib/nlspec.py b/tools/net/ynl/pyynl/lib/nlspec.py
index 0469a0e270d0..2407271fb7f7 100644
--- a/tools/net/ynl/pyynl/lib/nlspec.py
+++ b/tools/net/ynl/pyynl/lib/nlspec.py
@@ -12,6 +12,8 @@ import importlib
import os
import yaml as pyyaml
+from .specdir import find_spec, SYS_SCHEMA_DIR
+
class SpecException(Exception):
"""Netlink spec exception.
@@ -444,7 +446,17 @@ import yaml as pyyaml
except AttributeError:
_yaml_loader = pyyaml.SafeLoader
- def __init__(self, spec_path, schema_path=None, exclude_ops=None):
+ def __init__(self, spec_path=None, schema_path=None, exclude_ops=None,
+ family=None):
+ if (spec_path is None) == (family is None):
+ raise ValueError("Specify exactly one of spec path or family name")
+ if family is not None:
+ spec_path = find_spec(family)
+ # Installed specs are assumed correct, so skip schema validation
+ # to save cycles; only validate in the development environment.
+ if schema_path is None and spec_path.startswith(SYS_SCHEMA_DIR):
+ schema_path = ''
+
with open(spec_path, "r", encoding='utf-8') as stream:
prefix = '# SPDX-License-Identifier: '
first = stream.readline().strip()
diff --git a/tools/net/ynl/pyynl/lib/specdir.py b/tools/net/ynl/pyynl/lib/specdir.py
new file mode 100644
index 000000000000..fcea9b9fb7b0
--- /dev/null
+++ b/tools/net/ynl/pyynl/lib/specdir.py
@@ -0,0 +1,51 @@
+# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
+
+"""
+Locating YNL spec and schema files on disk.
+
+Resolves the directory holding the YAML specs (preferring an in-tree copy
+over the installed system path) and maps family names to spec files.
+"""
+
+import os
+
+SYS_SCHEMA_DIR='/usr/share/ynl'
+RELATIVE_SCHEMA_DIR='../../../../../Documentation/netlink'
+
+
+def schema_dir():
+ """
+ Return the effective schema directory, preferring in-tree before
+ system schema directory.
+ """
+ script_dir = os.path.dirname(os.path.abspath(__file__))
+ schema_dir_ = os.path.abspath(f"{script_dir}/{RELATIVE_SCHEMA_DIR}")
+ if not os.path.isdir(schema_dir_):
+ schema_dir_ = SYS_SCHEMA_DIR
+ if not os.path.isdir(schema_dir_):
+ raise FileNotFoundError(f"Schema directory {schema_dir_} does not exist")
+ return schema_dir_
+
+def spec_dir():
+ """
+ Return the effective spec directory, relative to the effective
+ schema directory.
+ """
+ spec_dir_ = schema_dir() + '/specs'
+ if not os.path.isdir(spec_dir_):
+ raise FileNotFoundError(f"Spec directory {spec_dir_} does not exist")
+ return spec_dir_
+
+
+def find_spec(family):
+ """ Return the path to the YAML spec file for a family by name. """
+ spec = f"{spec_dir()}/{family}.yaml"
+ if not os.path.isfile(spec):
+ raise FileNotFoundError(f"Spec for family '{family}' not found at {spec}")
+ return spec
+
+
+def list_families():
+ """ Return the sorted names of all families with an installed spec. """
+ return sorted(f.removesuffix('.yaml')
+ for f in os.listdir(spec_dir()) if f.endswith('.yaml'))
diff --git a/tools/net/ynl/pyynl/lib/ynl.py b/tools/net/ynl/pyynl/lib/ynl.py
index 092d132edec1..8682bf588e1f 100644
--- a/tools/net/ynl/pyynl/lib/ynl.py
+++ b/tools/net/ynl/pyynl/lib/ynl.py
@@ -661,6 +661,14 @@ from .nlspec import SpecFamily
"""
YNL family -- a Netlink interface built from a YAML spec.
+ The spec can be selected either by file path (def_path=) or, when it
+ ships in a well-known location, by family name (family="xyz"); exactly
+ one of the two must be given. For example:
+
+ from pyynl import YnlFamily
+
+ ynl = YnlFamily(family="netdev")
+
Primary use of the class is to execute Netlink commands:
ynl.<op_name>(attrs, ...)
@@ -691,11 +699,16 @@ from .nlspec import SpecFamily
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)
+ def __init__(self, def_path=None, schema=None, process_unknown=None,
+ recv_size=0, family=None):
+ super().__init__(def_path, schema, family=family)
self.include_raw = False
+ # Specs from /usr (selected by family=) have a higher chance of being
+ # stale, default to ignoring unknown attrs. In-tree users, and users
+ # who bundle the spec need to make a conscious decision.
+ if process_unknown is None:
+ process_unknown = family is not None
self.process_unknown = process_unknown
try:
--
2.54.0
^ permalink raw reply related [flat|nested] 6+ messages in thread