From 22ef8a263c17f02176e3a322b76da07411092c17 Mon Sep 17 00:00:00 2001 From: Hangbin Liu Date: Wed, 8 Apr 2026 15:08:49 +0800 Subject: tools: ynl: move ethtool.py to selftest We have converted all the samples to selftests. This script is the last piece of random "PoC" code we still have lying around. Let's move it to tests. Signed-off-by: Hangbin Liu Link: https://patch.msgid.link/20260408-b4-ynl_ethtool-v2-1-7623a5e8f70b@gmail.com Signed-off-by: Jakub Kicinski --- tools/net/ynl/pyynl/ethtool.py | 464 -------------------------------- tools/net/ynl/tests/Makefile | 5 +- tools/net/ynl/tests/ethtool.py | 464 ++++++++++++++++++++++++++++++++ tools/net/ynl/tests/test_ynl_ethtool.sh | 2 +- 4 files changed, 469 insertions(+), 466 deletions(-) delete mode 100755 tools/net/ynl/pyynl/ethtool.py create mode 100755 tools/net/ynl/tests/ethtool.py diff --git a/tools/net/ynl/pyynl/ethtool.py b/tools/net/ynl/pyynl/ethtool.py deleted file mode 100755 index f1a2a2a89985..000000000000 --- a/tools/net/ynl/pyynl/ethtool.py +++ /dev/null @@ -1,464 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause -# -# pylint: disable=too-many-locals, too-many-branches, too-many-statements -# pylint: disable=too-many-return-statements - -""" YNL ethtool utility """ - -import argparse -import pathlib -import pprint -import sys -import re -import os - -# pylint: disable=no-name-in-module,wrong-import-position -sys.path.append(pathlib.Path(__file__).resolve().parent.as_posix()) -# pylint: disable=import-error -from cli import schema_dir, spec_dir -from lib import YnlFamily - - -def args_to_req(ynl, op_name, args, req): - """ - Verify and convert command-line arguments to the ynl-compatible request. - """ - valid_attrs = ynl.operation_do_attributes(op_name) - valid_attrs.remove('header') # not user-provided - - if len(args) == 0: - print(f'no attributes, expected: {valid_attrs}') - sys.exit(1) - - i = 0 - while i < len(args): - attr = args[i] - if i + 1 >= len(args): - print(f'expected value for \'{attr}\'') - sys.exit(1) - - if attr not in valid_attrs: - print(f'invalid attribute \'{attr}\', expected: {valid_attrs}') - sys.exit(1) - - val = args[i+1] - i += 2 - - req[attr] = val - -def print_field(reply, *desc): - """ - Pretty-print a set of fields from the reply. desc specifies the - fields and the optional type (bool/yn). - """ - if not reply: - return - - if len(desc) == 0: - print_field(reply, *zip(reply.keys(), reply.keys())) - return - - for spec in desc: - try: - field, name, tp = spec - except ValueError: - field, name = spec - tp = 'int' - - value = reply.get(field, None) - if tp == 'yn': - value = 'yes' if value else 'no' - elif tp == 'bool' or isinstance(value, bool): - value = 'on' if value else 'off' - else: - value = 'n/a' if value is None else value - - print(f'{name}: {value}') - -def print_speed(name, value): - """ - Print out the speed-like strings from the value dict. - """ - speed_re = re.compile(r'[0-9]+base[^/]+/.+') - speed = [ k for k, v in value.items() if v and speed_re.match(k) ] - print(f'{name}: {" ".join(speed)}') - -def doit(ynl, args, op_name): - """ - Prepare request header, parse arguments and doit. - """ - req = { - 'header': { - 'dev-name': args.device, - }, - } - - args_to_req(ynl, op_name, args.args, req) - ynl.do(op_name, req) - -def dumpit(ynl, args, op_name, extra=None): - """ - Prepare request header, parse arguments and dumpit (filtering out the - devices we're not interested in). - """ - extra = extra or {} - reply = ynl.dump(op_name, { 'header': {} } | extra) - if not reply: - return {} - - for msg in reply: - if msg['header']['dev-name'] == args.device: - if args.json: - pprint.PrettyPrinter().pprint(msg) - sys.exit(0) - msg.pop('header', None) - return msg - - print(f"Not supported for device {args.device}") - sys.exit(1) - -def bits_to_dict(attr): - """ - Convert ynl-formatted bitmask to a dict of bit=value. - """ - ret = {} - if 'bits' not in attr: - return {} - if 'bit' not in attr['bits']: - return {} - for bit in attr['bits']['bit']: - if bit['name'] == '': - continue - name = bit['name'] - value = bit.get('value', False) - ret[name] = value - return ret - -def main(): - """ YNL ethtool utility """ - - parser = argparse.ArgumentParser(description='ethtool wannabe') - parser.add_argument('--json', action=argparse.BooleanOptionalAction) - parser.add_argument('--show-priv-flags', action=argparse.BooleanOptionalAction) - parser.add_argument('--set-priv-flags', action=argparse.BooleanOptionalAction) - parser.add_argument('--show-eee', action=argparse.BooleanOptionalAction) - parser.add_argument('--set-eee', action=argparse.BooleanOptionalAction) - parser.add_argument('-a', '--show-pause', action=argparse.BooleanOptionalAction) - parser.add_argument('-A', '--set-pause', action=argparse.BooleanOptionalAction) - parser.add_argument('-c', '--show-coalesce', action=argparse.BooleanOptionalAction) - parser.add_argument('-C', '--set-coalesce', action=argparse.BooleanOptionalAction) - parser.add_argument('-g', '--show-ring', action=argparse.BooleanOptionalAction) - parser.add_argument('-G', '--set-ring', action=argparse.BooleanOptionalAction) - parser.add_argument('-k', '--show-features', action=argparse.BooleanOptionalAction) - parser.add_argument('-K', '--set-features', action=argparse.BooleanOptionalAction) - parser.add_argument('-l', '--show-channels', action=argparse.BooleanOptionalAction) - parser.add_argument('-L', '--set-channels', action=argparse.BooleanOptionalAction) - parser.add_argument('-T', '--show-time-stamping', action=argparse.BooleanOptionalAction) - parser.add_argument('-S', '--statistics', action=argparse.BooleanOptionalAction) - # TODO: --show-tunnels tunnel-info-get - # TODO: --show-module module-get - # TODO: --get-plca-cfg plca-get - # TODO: --get-plca-status plca-get-status - # TODO: --show-mm mm-get - # TODO: --show-fec fec-get - # TODO: --dump-module-eerpom module-eeprom-get - # TODO: pse-get - # TODO: rss-get - parser.add_argument('device', metavar='device', type=str) - parser.add_argument('args', metavar='args', type=str, nargs='*') - - args = parser.parse_args() - - spec = os.path.join(spec_dir(), 'ethtool.yaml') - schema = os.path.join(schema_dir(), 'genetlink-legacy.yaml') - - ynl = YnlFamily(spec, schema) - - if args.set_priv_flags: - # TODO: parse the bitmask - print("not implemented") - return - - if args.set_eee: - doit(ynl, args, 'eee-set') - return - - if args.set_pause: - doit(ynl, args, 'pause-set') - return - - if args.set_coalesce: - doit(ynl, args, 'coalesce-set') - return - - if args.set_features: - # TODO: parse the bitmask - print("not implemented") - return - - if args.set_channels: - doit(ynl, args, 'channels-set') - return - - if args.set_ring: - doit(ynl, args, 'rings-set') - return - - if args.show_priv_flags: - flags = bits_to_dict(dumpit(ynl, args, 'privflags-get')['flags']) - print_field(flags) - return - - if args.show_eee: - eee = dumpit(ynl, args, 'eee-get') - ours = bits_to_dict(eee['modes-ours']) - peer = bits_to_dict(eee['modes-peer']) - - if 'enabled' in eee: - status = 'enabled' if eee['enabled'] else 'disabled' - if 'active' in eee and eee['active']: - status = status + ' - active' - else: - status = status + ' - inactive' - else: - status = 'not supported' - - print(f'EEE status: {status}') - print_field(eee, ('tx-lpi-timer', 'Tx LPI')) - print_speed('Advertised EEE link modes', ours) - print_speed('Link partner advertised EEE link modes', peer) - - return - - if args.show_pause: - print_field(dumpit(ynl, args, 'pause-get'), - ('autoneg', 'Autonegotiate', 'bool'), - ('rx', 'RX', 'bool'), - ('tx', 'TX', 'bool')) - return - - if args.show_coalesce: - print_field(dumpit(ynl, args, 'coalesce-get')) - return - - if args.show_features: - reply = dumpit(ynl, args, 'features-get') - available = bits_to_dict(reply['hw']) - requested = bits_to_dict(reply['wanted']).keys() - active = bits_to_dict(reply['active']).keys() - never_changed = bits_to_dict(reply['nochange']).keys() - - for f in sorted(available): - value = "off" - if f in active: - value = "on" - - fixed = "" - if f not in available or f in never_changed: - fixed = " [fixed]" - - req = "" - if f in requested: - if f in active: - req = " [requested on]" - else: - req = " [requested off]" - - print(f'{f}: {value}{fixed}{req}') - - return - - if args.show_channels: - reply = dumpit(ynl, args, 'channels-get') - print(f'Channel parameters for {args.device}:') - - print('Pre-set maximums:') - print_field(reply, - ('rx-max', 'RX'), - ('tx-max', 'TX'), - ('other-max', 'Other'), - ('combined-max', 'Combined')) - - print('Current hardware settings:') - print_field(reply, - ('rx-count', 'RX'), - ('tx-count', 'TX'), - ('other-count', 'Other'), - ('combined-count', 'Combined')) - - return - - if args.show_ring: - reply = dumpit(ynl, args, 'channels-get') - - print(f'Ring parameters for {args.device}:') - - print('Pre-set maximums:') - print_field(reply, - ('rx-max', 'RX'), - ('rx-mini-max', 'RX Mini'), - ('rx-jumbo-max', 'RX Jumbo'), - ('tx-max', 'TX')) - - print('Current hardware settings:') - print_field(reply, - ('rx', 'RX'), - ('rx-mini', 'RX Mini'), - ('rx-jumbo', 'RX Jumbo'), - ('tx', 'TX')) - - print_field(reply, - ('rx-buf-len', 'RX Buf Len'), - ('cqe-size', 'CQE Size'), - ('tx-push', 'TX Push', 'bool')) - - return - - if args.statistics: - print('NIC statistics:') - - # TODO: pass id? - strset = dumpit(ynl, args, 'strset-get') - pprint.PrettyPrinter().pprint(strset) - - req = { - 'groups': { - 'size': 1, - 'bits': { - 'bit': - # TODO: support passing the bitmask - #[ - #{ 'name': 'eth-phy', 'value': True }, - { 'name': 'eth-mac', 'value': True }, - #{ 'name': 'eth-ctrl', 'value': True }, - #{ 'name': 'rmon', 'value': True }, - #], - }, - }, - } - - rsp = dumpit(ynl, args, 'stats-get', req) - pprint.PrettyPrinter().pprint(rsp) - return - - if args.show_time_stamping: - req = { - 'header': { - 'flags': 'stats', - }, - } - - tsinfo = dumpit(ynl, args, 'tsinfo-get', req) - - print(f'Time stamping parameters for {args.device}:') - - print('Capabilities:') - _ = [print(f'\t{v}') for v in bits_to_dict(tsinfo['timestamping'])] - - print(f'PTP Hardware Clock: {tsinfo.get("phc-index", "none")}') - - if 'tx-types' in tsinfo: - print('Hardware Transmit Timestamp Modes:') - _ = [print(f'\t{v}') for v in bits_to_dict(tsinfo['tx-types'])] - else: - print('Hardware Transmit Timestamp Modes: none') - - if 'rx-filters' in tsinfo: - print('Hardware Receive Filter Modes:') - _ = [print(f'\t{v}') for v in bits_to_dict(tsinfo['rx-filters'])] - else: - print('Hardware Receive Filter Modes: none') - - if 'stats' in tsinfo and tsinfo['stats']: - print('Statistics:') - _ = [print(f'\t{k}: {v}') for k, v in tsinfo['stats'].items()] - - return - - print(f'Settings for {args.device}:') - linkmodes = dumpit(ynl, args, 'linkmodes-get') - ours = bits_to_dict(linkmodes['ours']) - - supported_ports = ('TP', 'AUI', 'BNC', 'MII', 'FIBRE', 'Backplane') - ports = [ p for p in supported_ports if ours.get(p, False)] - print(f'Supported ports: [ {" ".join(ports)} ]') - - print_speed('Supported link modes', ours) - - print_field(ours, ('Pause', 'Supported pause frame use', 'yn')) - print_field(ours, ('Autoneg', 'Supports auto-negotiation', 'yn')) - - supported_fec = ('None', 'PS', 'BASER', 'LLRS') - fec = [ p for p in supported_fec if ours.get(p, False)] - fec_str = " ".join(fec) - if len(fec) == 0: - fec_str = "Not reported" - - print(f'Supported FEC modes: {fec_str}') - - speed = 'Unknown!' - if linkmodes['speed'] > 0 and linkmodes['speed'] < 0xffffffff: - speed = f'{linkmodes["speed"]}Mb/s' - print(f'Speed: {speed}') - - duplex_modes = { - 0: 'Half', - 1: 'Full', - } - duplex = duplex_modes.get(linkmodes["duplex"], None) - if not duplex: - duplex = f'Unknown! ({linkmodes["duplex"]})' - print(f'Duplex: {duplex}') - - autoneg = "off" - if linkmodes.get("autoneg", 0) != 0: - autoneg = "on" - print(f'Auto-negotiation: {autoneg}') - - ports = { - 0: 'Twisted Pair', - 1: 'AUI', - 2: 'MII', - 3: 'FIBRE', - 4: 'BNC', - 5: 'Directly Attached Copper', - 0xef: 'None', - } - linkinfo = dumpit(ynl, args, 'linkinfo-get') - print(f'Port: {ports.get(linkinfo["port"], "Other")}') - - print_field(linkinfo, ('phyaddr', 'PHYAD')) - - transceiver = { - 0: 'Internal', - 1: 'External', - } - print(f'Transceiver: {transceiver.get(linkinfo["transceiver"], "Unknown")}') - - mdix_ctrl = { - 1: 'off', - 2: 'on', - } - mdix = mdix_ctrl.get(linkinfo['tp-mdix-ctrl'], None) - if mdix: - mdix = mdix + ' (forced)' - else: - mdix = mdix_ctrl.get(linkinfo['tp-mdix'], 'Unknown (auto)') - print(f'MDI-X: {mdix}') - - debug = dumpit(ynl, args, 'debug-get') - msgmask = bits_to_dict(debug.get("msgmask", [])).keys() - print(f'Current message level: {" ".join(msgmask)}') - - linkstate = dumpit(ynl, args, 'linkstate-get') - detected_states = { - 0: 'no', - 1: 'yes', - } - # TODO: wol-get - detected = detected_states.get(linkstate['link'], 'unknown') - print(f'Link detected: {detected}') - -if __name__ == '__main__': - main() diff --git a/tools/net/ynl/tests/Makefile b/tools/net/ynl/tests/Makefile index 9215e84cca05..40827ca8e579 100644 --- a/tools/net/ynl/tests/Makefile +++ b/tools/net/ynl/tests/Makefile @@ -36,7 +36,10 @@ TEST_GEN_FILES := \ rt-route \ # end of TEST_GEN_FILES -TEST_FILES := ynl_nsim_lib.sh +TEST_FILES := \ + ethtool.py \ + ynl_nsim_lib.sh \ +# end of TEST_FILES CFLAGS_netdev:=$(CFLAGS_netdev) $(CFLAGS_rt-link) CFLAGS_ovs:=$(CFLAGS_ovs_datapath) diff --git a/tools/net/ynl/tests/ethtool.py b/tools/net/ynl/tests/ethtool.py new file mode 100755 index 000000000000..6eeeb867edcf --- /dev/null +++ b/tools/net/ynl/tests/ethtool.py @@ -0,0 +1,464 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause +# +# pylint: disable=too-many-locals, too-many-branches, too-many-statements +# pylint: disable=too-many-return-statements + +""" YNL ethtool utility """ + +import argparse +import pathlib +import pprint +import sys +import re +import os + +# pylint: disable=no-name-in-module,wrong-import-position +sys.path.append(pathlib.Path(__file__).resolve().parent.parent.joinpath('pyynl').as_posix()) +# pylint: disable=import-error +from cli import schema_dir, spec_dir +from lib import YnlFamily + + +def args_to_req(ynl, op_name, args, req): + """ + Verify and convert command-line arguments to the ynl-compatible request. + """ + valid_attrs = ynl.operation_do_attributes(op_name) + valid_attrs.remove('header') # not user-provided + + if len(args) == 0: + print(f'no attributes, expected: {valid_attrs}') + sys.exit(1) + + i = 0 + while i < len(args): + attr = args[i] + if i + 1 >= len(args): + print(f'expected value for \'{attr}\'') + sys.exit(1) + + if attr not in valid_attrs: + print(f'invalid attribute \'{attr}\', expected: {valid_attrs}') + sys.exit(1) + + val = args[i+1] + i += 2 + + req[attr] = val + +def print_field(reply, *desc): + """ + Pretty-print a set of fields from the reply. desc specifies the + fields and the optional type (bool/yn). + """ + if not reply: + return + + if len(desc) == 0: + print_field(reply, *zip(reply.keys(), reply.keys())) + return + + for spec in desc: + try: + field, name, tp = spec + except ValueError: + field, name = spec + tp = 'int' + + value = reply.get(field, None) + if tp == 'yn': + value = 'yes' if value else 'no' + elif tp == 'bool' or isinstance(value, bool): + value = 'on' if value else 'off' + else: + value = 'n/a' if value is None else value + + print(f'{name}: {value}') + +def print_speed(name, value): + """ + Print out the speed-like strings from the value dict. + """ + speed_re = re.compile(r'[0-9]+base[^/]+/.+') + speed = [ k for k, v in value.items() if v and speed_re.match(k) ] + print(f'{name}: {" ".join(speed)}') + +def doit(ynl, args, op_name): + """ + Prepare request header, parse arguments and doit. + """ + req = { + 'header': { + 'dev-name': args.device, + }, + } + + args_to_req(ynl, op_name, args.args, req) + ynl.do(op_name, req) + +def dumpit(ynl, args, op_name, extra=None): + """ + Prepare request header, parse arguments and dumpit (filtering out the + devices we're not interested in). + """ + extra = extra or {} + reply = ynl.dump(op_name, { 'header': {} } | extra) + if not reply: + return {} + + for msg in reply: + if msg['header']['dev-name'] == args.device: + if args.json: + pprint.PrettyPrinter().pprint(msg) + sys.exit(0) + msg.pop('header', None) + return msg + + print(f"Not supported for device {args.device}") + sys.exit(1) + +def bits_to_dict(attr): + """ + Convert ynl-formatted bitmask to a dict of bit=value. + """ + ret = {} + if 'bits' not in attr: + return {} + if 'bit' not in attr['bits']: + return {} + for bit in attr['bits']['bit']: + if bit['name'] == '': + continue + name = bit['name'] + value = bit.get('value', False) + ret[name] = value + return ret + +def main(): + """ YNL ethtool utility """ + + parser = argparse.ArgumentParser(description='ethtool wannabe') + parser.add_argument('--json', action=argparse.BooleanOptionalAction) + parser.add_argument('--show-priv-flags', action=argparse.BooleanOptionalAction) + parser.add_argument('--set-priv-flags', action=argparse.BooleanOptionalAction) + parser.add_argument('--show-eee', action=argparse.BooleanOptionalAction) + parser.add_argument('--set-eee', action=argparse.BooleanOptionalAction) + parser.add_argument('-a', '--show-pause', action=argparse.BooleanOptionalAction) + parser.add_argument('-A', '--set-pause', action=argparse.BooleanOptionalAction) + parser.add_argument('-c', '--show-coalesce', action=argparse.BooleanOptionalAction) + parser.add_argument('-C', '--set-coalesce', action=argparse.BooleanOptionalAction) + parser.add_argument('-g', '--show-ring', action=argparse.BooleanOptionalAction) + parser.add_argument('-G', '--set-ring', action=argparse.BooleanOptionalAction) + parser.add_argument('-k', '--show-features', action=argparse.BooleanOptionalAction) + parser.add_argument('-K', '--set-features', action=argparse.BooleanOptionalAction) + parser.add_argument('-l', '--show-channels', action=argparse.BooleanOptionalAction) + parser.add_argument('-L', '--set-channels', action=argparse.BooleanOptionalAction) + parser.add_argument('-T', '--show-time-stamping', action=argparse.BooleanOptionalAction) + parser.add_argument('-S', '--statistics', action=argparse.BooleanOptionalAction) + # TODO: --show-tunnels tunnel-info-get + # TODO: --show-module module-get + # TODO: --get-plca-cfg plca-get + # TODO: --get-plca-status plca-get-status + # TODO: --show-mm mm-get + # TODO: --show-fec fec-get + # TODO: --dump-module-eerpom module-eeprom-get + # TODO: pse-get + # TODO: rss-get + parser.add_argument('device', metavar='device', type=str) + parser.add_argument('args', metavar='args', type=str, nargs='*') + + args = parser.parse_args() + + spec = os.path.join(spec_dir(), 'ethtool.yaml') + schema = os.path.join(schema_dir(), 'genetlink-legacy.yaml') + + ynl = YnlFamily(spec, schema) + + if args.set_priv_flags: + # TODO: parse the bitmask + print("not implemented") + return + + if args.set_eee: + doit(ynl, args, 'eee-set') + return + + if args.set_pause: + doit(ynl, args, 'pause-set') + return + + if args.set_coalesce: + doit(ynl, args, 'coalesce-set') + return + + if args.set_features: + # TODO: parse the bitmask + print("not implemented") + return + + if args.set_channels: + doit(ynl, args, 'channels-set') + return + + if args.set_ring: + doit(ynl, args, 'rings-set') + return + + if args.show_priv_flags: + flags = bits_to_dict(dumpit(ynl, args, 'privflags-get')['flags']) + print_field(flags) + return + + if args.show_eee: + eee = dumpit(ynl, args, 'eee-get') + ours = bits_to_dict(eee['modes-ours']) + peer = bits_to_dict(eee['modes-peer']) + + if 'enabled' in eee: + status = 'enabled' if eee['enabled'] else 'disabled' + if 'active' in eee and eee['active']: + status = status + ' - active' + else: + status = status + ' - inactive' + else: + status = 'not supported' + + print(f'EEE status: {status}') + print_field(eee, ('tx-lpi-timer', 'Tx LPI')) + print_speed('Advertised EEE link modes', ours) + print_speed('Link partner advertised EEE link modes', peer) + + return + + if args.show_pause: + print_field(dumpit(ynl, args, 'pause-get'), + ('autoneg', 'Autonegotiate', 'bool'), + ('rx', 'RX', 'bool'), + ('tx', 'TX', 'bool')) + return + + if args.show_coalesce: + print_field(dumpit(ynl, args, 'coalesce-get')) + return + + if args.show_features: + reply = dumpit(ynl, args, 'features-get') + available = bits_to_dict(reply['hw']) + requested = bits_to_dict(reply['wanted']).keys() + active = bits_to_dict(reply['active']).keys() + never_changed = bits_to_dict(reply['nochange']).keys() + + for f in sorted(available): + value = "off" + if f in active: + value = "on" + + fixed = "" + if f not in available or f in never_changed: + fixed = " [fixed]" + + req = "" + if f in requested: + if f in active: + req = " [requested on]" + else: + req = " [requested off]" + + print(f'{f}: {value}{fixed}{req}') + + return + + if args.show_channels: + reply = dumpit(ynl, args, 'channels-get') + print(f'Channel parameters for {args.device}:') + + print('Pre-set maximums:') + print_field(reply, + ('rx-max', 'RX'), + ('tx-max', 'TX'), + ('other-max', 'Other'), + ('combined-max', 'Combined')) + + print('Current hardware settings:') + print_field(reply, + ('rx-count', 'RX'), + ('tx-count', 'TX'), + ('other-count', 'Other'), + ('combined-count', 'Combined')) + + return + + if args.show_ring: + reply = dumpit(ynl, args, 'channels-get') + + print(f'Ring parameters for {args.device}:') + + print('Pre-set maximums:') + print_field(reply, + ('rx-max', 'RX'), + ('rx-mini-max', 'RX Mini'), + ('rx-jumbo-max', 'RX Jumbo'), + ('tx-max', 'TX')) + + print('Current hardware settings:') + print_field(reply, + ('rx', 'RX'), + ('rx-mini', 'RX Mini'), + ('rx-jumbo', 'RX Jumbo'), + ('tx', 'TX')) + + print_field(reply, + ('rx-buf-len', 'RX Buf Len'), + ('cqe-size', 'CQE Size'), + ('tx-push', 'TX Push', 'bool')) + + return + + if args.statistics: + print('NIC statistics:') + + # TODO: pass id? + strset = dumpit(ynl, args, 'strset-get') + pprint.PrettyPrinter().pprint(strset) + + req = { + 'groups': { + 'size': 1, + 'bits': { + 'bit': + # TODO: support passing the bitmask + #[ + #{ 'name': 'eth-phy', 'value': True }, + { 'name': 'eth-mac', 'value': True }, + #{ 'name': 'eth-ctrl', 'value': True }, + #{ 'name': 'rmon', 'value': True }, + #], + }, + }, + } + + rsp = dumpit(ynl, args, 'stats-get', req) + pprint.PrettyPrinter().pprint(rsp) + return + + if args.show_time_stamping: + req = { + 'header': { + 'flags': 'stats', + }, + } + + tsinfo = dumpit(ynl, args, 'tsinfo-get', req) + + print(f'Time stamping parameters for {args.device}:') + + print('Capabilities:') + _ = [print(f'\t{v}') for v in bits_to_dict(tsinfo['timestamping'])] + + print(f'PTP Hardware Clock: {tsinfo.get("phc-index", "none")}') + + if 'tx-types' in tsinfo: + print('Hardware Transmit Timestamp Modes:') + _ = [print(f'\t{v}') for v in bits_to_dict(tsinfo['tx-types'])] + else: + print('Hardware Transmit Timestamp Modes: none') + + if 'rx-filters' in tsinfo: + print('Hardware Receive Filter Modes:') + _ = [print(f'\t{v}') for v in bits_to_dict(tsinfo['rx-filters'])] + else: + print('Hardware Receive Filter Modes: none') + + if 'stats' in tsinfo and tsinfo['stats']: + print('Statistics:') + _ = [print(f'\t{k}: {v}') for k, v in tsinfo['stats'].items()] + + return + + print(f'Settings for {args.device}:') + linkmodes = dumpit(ynl, args, 'linkmodes-get') + ours = bits_to_dict(linkmodes['ours']) + + supported_ports = ('TP', 'AUI', 'BNC', 'MII', 'FIBRE', 'Backplane') + ports = [ p for p in supported_ports if ours.get(p, False)] + print(f'Supported ports: [ {" ".join(ports)} ]') + + print_speed('Supported link modes', ours) + + print_field(ours, ('Pause', 'Supported pause frame use', 'yn')) + print_field(ours, ('Autoneg', 'Supports auto-negotiation', 'yn')) + + supported_fec = ('None', 'PS', 'BASER', 'LLRS') + fec = [ p for p in supported_fec if ours.get(p, False)] + fec_str = " ".join(fec) + if len(fec) == 0: + fec_str = "Not reported" + + print(f'Supported FEC modes: {fec_str}') + + speed = 'Unknown!' + if linkmodes['speed'] > 0 and linkmodes['speed'] < 0xffffffff: + speed = f'{linkmodes["speed"]}Mb/s' + print(f'Speed: {speed}') + + duplex_modes = { + 0: 'Half', + 1: 'Full', + } + duplex = duplex_modes.get(linkmodes["duplex"], None) + if not duplex: + duplex = f'Unknown! ({linkmodes["duplex"]})' + print(f'Duplex: {duplex}') + + autoneg = "off" + if linkmodes.get("autoneg", 0) != 0: + autoneg = "on" + print(f'Auto-negotiation: {autoneg}') + + ports = { + 0: 'Twisted Pair', + 1: 'AUI', + 2: 'MII', + 3: 'FIBRE', + 4: 'BNC', + 5: 'Directly Attached Copper', + 0xef: 'None', + } + linkinfo = dumpit(ynl, args, 'linkinfo-get') + print(f'Port: {ports.get(linkinfo["port"], "Other")}') + + print_field(linkinfo, ('phyaddr', 'PHYAD')) + + transceiver = { + 0: 'Internal', + 1: 'External', + } + print(f'Transceiver: {transceiver.get(linkinfo["transceiver"], "Unknown")}') + + mdix_ctrl = { + 1: 'off', + 2: 'on', + } + mdix = mdix_ctrl.get(linkinfo['tp-mdix-ctrl'], None) + if mdix: + mdix = mdix + ' (forced)' + else: + mdix = mdix_ctrl.get(linkinfo['tp-mdix'], 'Unknown (auto)') + print(f'MDI-X: {mdix}') + + debug = dumpit(ynl, args, 'debug-get') + msgmask = bits_to_dict(debug.get("msgmask", [])).keys() + print(f'Current message level: {" ".join(msgmask)}') + + linkstate = dumpit(ynl, args, 'linkstate-get') + detected_states = { + 0: 'no', + 1: 'yes', + } + # TODO: wol-get + detected = detected_states.get(linkstate['link'], 'unknown') + print(f'Link detected: {detected}') + +if __name__ == '__main__': + main() diff --git a/tools/net/ynl/tests/test_ynl_ethtool.sh b/tools/net/ynl/tests/test_ynl_ethtool.sh index b826269017f4..b4480e9be7b7 100755 --- a/tools/net/ynl/tests/test_ynl_ethtool.sh +++ b/tools/net/ynl/tests/test_ynl_ethtool.sh @@ -8,7 +8,7 @@ KSELFTEST_KTAP_HELPERS="$(dirname "$(realpath "$0")")/../../../testing/selftests source "$KSELFTEST_KTAP_HELPERS" # Default ynl-ethtool path for direct execution, can be overridden by make install -ynl_ethtool="../pyynl/ethtool.py" +ynl_ethtool="./ethtool.py" readonly NSIM_ID="1337" readonly NSIM_DEV_NAME="nsim${NSIM_ID}" -- cgit v1.2.3 From 1c43d471a513e93b5420d8af5d43afd3a75f95f1 Mon Sep 17 00:00:00 2001 From: Hangbin Liu Date: Wed, 8 Apr 2026 15:08:50 +0800 Subject: tools: ynl: ethtool: use doit instead of dumpit for per-device GET Rename the local helper doit() to do_set() and dumpit() to do_get() to better reflect their purpose. Convert do_get() to use ynl.do() with an explicit device header instead of ynl.dump() followed by client-side filtering. This is more efficient as the kernel only processes and returns data for the requested device, rather than dumping all devices across the netns. Signed-off-by: Hangbin Liu Link: https://patch.msgid.link/20260408-b4-ynl_ethtool-v2-2-7623a5e8f70b@gmail.com Signed-off-by: Jakub Kicinski --- tools/net/ynl/tests/ethtool.py | 68 ++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/tools/net/ynl/tests/ethtool.py b/tools/net/ynl/tests/ethtool.py index 6eeeb867edcf..63854d21818c 100755 --- a/tools/net/ynl/tests/ethtool.py +++ b/tools/net/ynl/tests/ethtool.py @@ -84,9 +84,9 @@ def print_speed(name, value): speed = [ k for k, v in value.items() if v and speed_re.match(k) ] print(f'{name}: {" ".join(speed)}') -def doit(ynl, args, op_name): +def do_set(ynl, args, op_name): """ - Prepare request header, parse arguments and doit. + Prepare request header, parse arguments and do a set operation. """ req = { 'header': { @@ -97,26 +97,24 @@ def doit(ynl, args, op_name): args_to_req(ynl, op_name, args.args, req) ynl.do(op_name, req) -def dumpit(ynl, args, op_name, extra=None): +def do_get(ynl, args, op_name, extra=None): """ - Prepare request header, parse arguments and dumpit (filtering out the - devices we're not interested in). + Prepare request header and get info for a specific device using doit. """ extra = extra or {} - reply = ynl.dump(op_name, { 'header': {} } | extra) + req = {'header': {'dev-name': args.device}} + req['header'].update(extra.pop('header', {})) + req.update(extra) + + reply = ynl.do(op_name, req) if not reply: return {} - for msg in reply: - if msg['header']['dev-name'] == args.device: - if args.json: - pprint.PrettyPrinter().pprint(msg) - sys.exit(0) - msg.pop('header', None) - return msg - - print(f"Not supported for device {args.device}") - sys.exit(1) + if args.json: + pprint.PrettyPrinter().pprint(reply) + sys.exit(0) + reply.pop('header', None) + return reply def bits_to_dict(attr): """ @@ -181,15 +179,15 @@ def main(): return if args.set_eee: - doit(ynl, args, 'eee-set') + do_set(ynl, args, 'eee-set') return if args.set_pause: - doit(ynl, args, 'pause-set') + do_set(ynl, args, 'pause-set') return if args.set_coalesce: - doit(ynl, args, 'coalesce-set') + do_set(ynl, args, 'coalesce-set') return if args.set_features: @@ -198,20 +196,20 @@ def main(): return if args.set_channels: - doit(ynl, args, 'channels-set') + do_set(ynl, args, 'channels-set') return if args.set_ring: - doit(ynl, args, 'rings-set') + do_set(ynl, args, 'rings-set') return if args.show_priv_flags: - flags = bits_to_dict(dumpit(ynl, args, 'privflags-get')['flags']) + flags = bits_to_dict(do_get(ynl, args, 'privflags-get')['flags']) print_field(flags) return if args.show_eee: - eee = dumpit(ynl, args, 'eee-get') + eee = do_get(ynl, args, 'eee-get') ours = bits_to_dict(eee['modes-ours']) peer = bits_to_dict(eee['modes-peer']) @@ -232,18 +230,18 @@ def main(): return if args.show_pause: - print_field(dumpit(ynl, args, 'pause-get'), + print_field(do_get(ynl, args, 'pause-get'), ('autoneg', 'Autonegotiate', 'bool'), ('rx', 'RX', 'bool'), ('tx', 'TX', 'bool')) return if args.show_coalesce: - print_field(dumpit(ynl, args, 'coalesce-get')) + print_field(do_get(ynl, args, 'coalesce-get')) return if args.show_features: - reply = dumpit(ynl, args, 'features-get') + reply = do_get(ynl, args, 'features-get') available = bits_to_dict(reply['hw']) requested = bits_to_dict(reply['wanted']).keys() active = bits_to_dict(reply['active']).keys() @@ -270,7 +268,7 @@ def main(): return if args.show_channels: - reply = dumpit(ynl, args, 'channels-get') + reply = do_get(ynl, args, 'channels-get') print(f'Channel parameters for {args.device}:') print('Pre-set maximums:') @@ -290,7 +288,7 @@ def main(): return if args.show_ring: - reply = dumpit(ynl, args, 'channels-get') + reply = do_get(ynl, args, 'channels-get') print(f'Ring parameters for {args.device}:') @@ -319,7 +317,7 @@ def main(): print('NIC statistics:') # TODO: pass id? - strset = dumpit(ynl, args, 'strset-get') + strset = do_get(ynl, args, 'strset-get') pprint.PrettyPrinter().pprint(strset) req = { @@ -338,7 +336,7 @@ def main(): }, } - rsp = dumpit(ynl, args, 'stats-get', req) + rsp = do_get(ynl, args, 'stats-get', req) pprint.PrettyPrinter().pprint(rsp) return @@ -349,7 +347,7 @@ def main(): }, } - tsinfo = dumpit(ynl, args, 'tsinfo-get', req) + tsinfo = do_get(ynl, args, 'tsinfo-get', req) print(f'Time stamping parameters for {args.device}:') @@ -377,7 +375,7 @@ def main(): return print(f'Settings for {args.device}:') - linkmodes = dumpit(ynl, args, 'linkmodes-get') + linkmodes = do_get(ynl, args, 'linkmodes-get') ours = bits_to_dict(linkmodes['ours']) supported_ports = ('TP', 'AUI', 'BNC', 'MII', 'FIBRE', 'Backplane') @@ -425,7 +423,7 @@ def main(): 5: 'Directly Attached Copper', 0xef: 'None', } - linkinfo = dumpit(ynl, args, 'linkinfo-get') + linkinfo = do_get(ynl, args, 'linkinfo-get') print(f'Port: {ports.get(linkinfo["port"], "Other")}') print_field(linkinfo, ('phyaddr', 'PHYAD')) @@ -447,11 +445,11 @@ def main(): mdix = mdix_ctrl.get(linkinfo['tp-mdix'], 'Unknown (auto)') print(f'MDI-X: {mdix}') - debug = dumpit(ynl, args, 'debug-get') + debug = do_get(ynl, args, 'debug-get') msgmask = bits_to_dict(debug.get("msgmask", [])).keys() print(f'Current message level: {" ".join(msgmask)}') - linkstate = dumpit(ynl, args, 'linkstate-get') + linkstate = do_get(ynl, args, 'linkstate-get') detected_states = { 0: 'no', 1: 'yes', -- cgit v1.2.3 From 594ba4477164af58c9703039b63b8b07a3a55f18 Mon Sep 17 00:00:00 2001 From: Hangbin Liu Date: Wed, 8 Apr 2026 15:08:51 +0800 Subject: tools: ynl: ethtool: add --dbg-small-recv option Add a --dbg-small-recv debug option to control the recv() buffer size used by YNL, matching the same option already present in cli.py. This is useful if user need to get large netlink message. Signed-off-by: Hangbin Liu Link: https://patch.msgid.link/20260408-b4-ynl_ethtool-v2-3-7623a5e8f70b@gmail.com Signed-off-by: Jakub Kicinski --- tools/net/ynl/tests/ethtool.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tools/net/ynl/tests/ethtool.py b/tools/net/ynl/tests/ethtool.py index 63854d21818c..db3b62c652e7 100755 --- a/tools/net/ynl/tests/ethtool.py +++ b/tools/net/ynl/tests/ethtool.py @@ -166,12 +166,19 @@ def main(): parser.add_argument('device', metavar='device', type=str) parser.add_argument('args', metavar='args', type=str, nargs='*') + dbg_group = parser.add_argument_group('Debug options') + 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()") + args = parser.parse_args() spec = os.path.join(spec_dir(), 'ethtool.yaml') schema = os.path.join(schema_dir(), 'genetlink-legacy.yaml') - ynl = YnlFamily(spec, schema) + ynl = YnlFamily(spec, schema, recv_size=args.dbg_small_recv) + if args.dbg_small_recv: + ynl.set_recv_dbg(True) if args.set_priv_flags: # TODO: parse the bitmask -- cgit v1.2.3 From 1346586a9ac96588eff586ca1893dd2e88b88510 Mon Sep 17 00:00:00 2001 From: Hangbin Liu Date: Wed, 8 Apr 2026 15:08:52 +0800 Subject: netlink: add a nla_nest_end_safe() helper The nla_len field in struct nlattr is a __u16, which can only hold values up to 65535. If a nested attribute grows beyond this limit, nla_nest_end() silently truncates the length, producing a corrupted netlink message with no indication of the problem. Since nla_nest_end() is used everywhere and this issue rarely happens, let's add a new helper to check the length. Signed-off-by: Hangbin Liu Link: https://patch.msgid.link/20260408-b4-ynl_ethtool-v2-4-7623a5e8f70b@gmail.com Signed-off-by: Jakub Kicinski --- include/net/netlink.h | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/include/net/netlink.h b/include/net/netlink.h index 1a8356ca4b78..546d10586576 100644 --- a/include/net/netlink.h +++ b/include/net/netlink.h @@ -2264,6 +2264,25 @@ static inline int nla_nest_end(struct sk_buff *skb, struct nlattr *start) return skb->len; } +/** + * nla_nest_end_safe - Validate and finalize nesting of attributes + * @skb: socket buffer the attributes are stored in + * @start: container attribute + * + * Corrects the container attribute header to include all appended + * attributes. + * + * Returns: the total data length of the skb, or -EMSGSIZE if the + * nested attribute length exceeds U16_MAX. + */ +static inline int nla_nest_end_safe(struct sk_buff *skb, struct nlattr *start) +{ + if (skb_tail_pointer(skb) - (unsigned char *)start > U16_MAX) + return -EMSGSIZE; + + return nla_nest_end(skb, start); +} + /** * nla_nest_cancel - Cancel nesting of attributes * @skb: socket buffer the message is stored in -- cgit v1.2.3 From b2fb1a336383f1fb4667a9cc930c70f52ae1e20e Mon Sep 17 00:00:00 2001 From: Hangbin Liu Date: Wed, 8 Apr 2026 15:08:53 +0800 Subject: ethtool: strset: check nla_len overflow The netlink attribute length field nla_len is a __u16, which can only represent values up to 65535 bytes. NICs with a large number of statistics strings (e.g. mlx5_core with thousands of ETH_SS_STATS entries) can produce a ETHTOOL_A_STRINGSET_STRINGS nest that exceeds this limit. When nla_nest_end() writes the actual nest size back to nla_len, the value is silently truncated. This results in a corrupted netlink message being sent to userspace: the parser reads a wrong (truncated) attribute length and misaligns all subsequent attribute boundaries, causing decode errors. Fix this by using the new helper nla_nest_end_safe and error out if the size exceeds U16_MAX. Signed-off-by: Hangbin Liu Link: https://patch.msgid.link/20260408-b4-ynl_ethtool-v2-5-7623a5e8f70b@gmail.com Signed-off-by: Jakub Kicinski --- net/ethtool/strset.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/net/ethtool/strset.c b/net/ethtool/strset.c index 9271aba8255e..bb1e829ba099 100644 --- a/net/ethtool/strset.c +++ b/net/ethtool/strset.c @@ -443,7 +443,8 @@ static int strset_fill_set(struct sk_buff *skb, if (strset_fill_string(skb, set_info, i) < 0) goto nla_put_failure; } - nla_nest_end(skb, strings_attr); + if (nla_nest_end_safe(skb, strings_attr) < 0) + goto nla_put_failure; } nla_nest_end(skb, stringset_attr); -- cgit v1.2.3