1#!/usr/bin/env python3 2# SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause 3 4import argparse 5import json 6import pathlib 7import pprint 8import sys 9import re 10import os 11 12sys.path.append(pathlib.Path(__file__).resolve().parent.as_posix()) 13from lib import YnlFamily 14from cli import schema_dir, spec_dir 15 16def args_to_req(ynl, op_name, args, req): 17 """ 18 Verify and convert command-line arguments to the ynl-compatible request. 19 """ 20 valid_attrs = ynl.operation_do_attributes(op_name) 21 valid_attrs.remove('header') # not user-provided 22 23 if len(args) == 0: 24 print(f'no attributes, expected: {valid_attrs}') 25 sys.exit(1) 26 27 i = 0 28 while i < len(args): 29 attr = args[i] 30 if i + 1 >= len(args): 31 print(f'expected value for \'{attr}\'') 32 sys.exit(1) 33 34 if attr not in valid_attrs: 35 print(f'invalid attribute \'{attr}\', expected: {valid_attrs}') 36 sys.exit(1) 37 38 val = args[i+1] 39 i += 2 40 41 req[attr] = val 42 43def print_field(reply, *desc): 44 """ 45 Pretty-print a set of fields from the reply. desc specifies the 46 fields and the optional type (bool/yn). 47 """ 48 if len(desc) == 0: 49 return print_field(reply, *zip(reply.keys(), reply.keys())) 50 51 for spec in desc: 52 try: 53 field, name, tp = spec 54 except: 55 field, name = spec 56 tp = 'int' 57 58 value = reply.get(field, None) 59 if tp == 'yn': 60 value = 'yes' if value else 'no' 61 elif tp == 'bool' or isinstance(value, bool): 62 value = 'on' if value else 'off' 63 else: 64 value = 'n/a' if value is None else value 65 66 print(f'{name}: {value}') 67 68def print_speed(name, value): 69 """ 70 Print out the speed-like strings from the value dict. 71 """ 72 speed_re = re.compile(r'[0-9]+base[^/]+/.+') 73 speed = [ k for k, v in value.items() if v and speed_re.match(k) ] 74 print(f'{name}: {" ".join(speed)}') 75 76def doit(ynl, args, op_name): 77 """ 78 Prepare request header, parse arguments and doit. 79 """ 80 req = { 81 'header': { 82 'dev-name': args.device, 83 }, 84 } 85 86 args_to_req(ynl, op_name, args.args, req) 87 ynl.do(op_name, req) 88 89def dumpit(ynl, args, op_name, extra = {}): 90 """ 91 Prepare request header, parse arguments and dumpit (filtering out the 92 devices we're not interested in). 93 """ 94 reply = ynl.dump(op_name, { 'header': {} } | extra) 95 if not reply: 96 return {} 97 98 for msg in reply: 99 if msg['header']['dev-name'] == args.device: 100 if args.json: 101 pprint.PrettyPrinter().pprint(msg) 102 sys.exit(0) 103 msg.pop('header', None) 104 return msg 105 106 print(f"Not supported for device {args.device}") 107 sys.exit(1) 108 109def bits_to_dict(attr): 110 """ 111 Convert ynl-formatted bitmask to a dict of bit=value. 112 """ 113 ret = {} 114 if 'bits' not in attr: 115 return dict() 116 if 'bit' not in attr['bits']: 117 return dict() 118 for bit in attr['bits']['bit']: 119 if bit['name'] == '': 120 continue 121 name = bit['name'] 122 value = bit.get('value', False) 123 ret[name] = value 124 return ret 125 126def main(): 127 parser = argparse.ArgumentParser(description='ethtool wannabe') 128 parser.add_argument('--json', action=argparse.BooleanOptionalAction) 129 parser.add_argument('--show-priv-flags', action=argparse.BooleanOptionalAction) 130 parser.add_argument('--set-priv-flags', action=argparse.BooleanOptionalAction) 131 parser.add_argument('--show-eee', action=argparse.BooleanOptionalAction) 132 parser.add_argument('--set-eee', action=argparse.BooleanOptionalAction) 133 parser.add_argument('-a', '--show-pause', action=argparse.BooleanOptionalAction) 134 parser.add_argument('-A', '--set-pause', action=argparse.BooleanOptionalAction) 135 parser.add_argument('-c', '--show-coalesce', action=argparse.BooleanOptionalAction) 136 parser.add_argument('-C', '--set-coalesce', action=argparse.BooleanOptionalAction) 137 parser.add_argument('-g', '--show-ring', action=argparse.BooleanOptionalAction) 138 parser.add_argument('-G', '--set-ring', action=argparse.BooleanOptionalAction) 139 parser.add_argument('-k', '--show-features', action=argparse.BooleanOptionalAction) 140 parser.add_argument('-K', '--set-features', action=argparse.BooleanOptionalAction) 141 parser.add_argument('-l', '--show-channels', action=argparse.BooleanOptionalAction) 142 parser.add_argument('-L', '--set-channels', action=argparse.BooleanOptionalAction) 143 parser.add_argument('-T', '--show-time-stamping', action=argparse.BooleanOptionalAction) 144 parser.add_argument('-S', '--statistics', action=argparse.BooleanOptionalAction) 145 # TODO: --show-tunnels tunnel-info-get 146 # TODO: --show-module module-get 147 # TODO: --get-plca-cfg plca-get 148 # TODO: --get-plca-status plca-get-status 149 # TODO: --show-mm mm-get 150 # TODO: --show-fec fec-get 151 # TODO: --dump-module-eerpom module-eeprom-get 152 # TODO: pse-get 153 # TODO: rss-get 154 parser.add_argument('device', metavar='device', type=str) 155 parser.add_argument('args', metavar='args', type=str, nargs='*') 156 global args 157 args = parser.parse_args() 158 159 script_abs_dir = os.path.dirname(os.path.abspath(sys.argv[0])) 160 spec = os.path.join(spec_dir(), 'ethtool.yaml') 161 schema = os.path.join(schema_dir(), 'genetlink-legacy.yaml') 162 163 ynl = YnlFamily(spec, schema) 164 165 if args.set_priv_flags: 166 # TODO: parse the bitmask 167 print("not implemented") 168 return 169 170 if args.set_eee: 171 return doit(ynl, args, 'eee-set') 172 173 if args.set_pause: 174 return doit(ynl, args, 'pause-set') 175 176 if args.set_coalesce: 177 return doit(ynl, args, 'coalesce-set') 178 179 if args.set_features: 180 # TODO: parse the bitmask 181 print("not implemented") 182 return 183 184 if args.set_channels: 185 return doit(ynl, args, 'channels-set') 186 187 if args.set_ring: 188 return doit(ynl, args, 'rings-set') 189 190 if args.show_priv_flags: 191 flags = bits_to_dict(dumpit(ynl, args, 'privflags-get')['flags']) 192 print_field(flags) 193 return 194 195 if args.show_eee: 196 eee = dumpit(ynl, args, 'eee-get') 197 ours = bits_to_dict(eee['modes-ours']) 198 peer = bits_to_dict(eee['modes-peer']) 199 200 if 'enabled' in eee: 201 status = 'enabled' if eee['enabled'] else 'disabled' 202 if 'active' in eee and eee['active']: 203 status = status + ' - active' 204 else: 205 status = status + ' - inactive' 206 else: 207 status = 'not supported' 208 209 print(f'EEE status: {status}') 210 print_field(eee, ('tx-lpi-timer', 'Tx LPI')) 211 print_speed('Advertised EEE link modes', ours) 212 print_speed('Link partner advertised EEE link modes', peer) 213 214 return 215 216 if args.show_pause: 217 print_field(dumpit(ynl, args, 'pause-get'), 218 ('autoneg', 'Autonegotiate', 'bool'), 219 ('rx', 'RX', 'bool'), 220 ('tx', 'TX', 'bool')) 221 return 222 223 if args.show_coalesce: 224 print_field(dumpit(ynl, args, 'coalesce-get')) 225 return 226 227 if args.show_features: 228 reply = dumpit(ynl, args, 'features-get') 229 available = bits_to_dict(reply['hw']) 230 requested = bits_to_dict(reply['wanted']).keys() 231 active = bits_to_dict(reply['active']).keys() 232 never_changed = bits_to_dict(reply['nochange']).keys() 233 234 for f in sorted(available): 235 value = "off" 236 if f in active: 237 value = "on" 238 239 fixed = "" 240 if f not in available or f in never_changed: 241 fixed = " [fixed]" 242 243 req = "" 244 if f in requested: 245 if f in active: 246 req = " [requested on]" 247 else: 248 req = " [requested off]" 249 250 print(f'{f}: {value}{fixed}{req}') 251 252 return 253 254 if args.show_channels: 255 reply = dumpit(ynl, args, 'channels-get') 256 print(f'Channel parameters for {args.device}:') 257 258 print(f'Pre-set maximums:') 259 print_field(reply, 260 ('rx-max', 'RX'), 261 ('tx-max', 'TX'), 262 ('other-max', 'Other'), 263 ('combined-max', 'Combined')) 264 265 print(f'Current hardware settings:') 266 print_field(reply, 267 ('rx-count', 'RX'), 268 ('tx-count', 'TX'), 269 ('other-count', 'Other'), 270 ('combined-count', 'Combined')) 271 272 return 273 274 if args.show_ring: 275 reply = dumpit(ynl, args, 'channels-get') 276 277 print(f'Ring parameters for {args.device}:') 278 279 print(f'Pre-set maximums:') 280 print_field(reply, 281 ('rx-max', 'RX'), 282 ('rx-mini-max', 'RX Mini'), 283 ('rx-jumbo-max', 'RX Jumbo'), 284 ('tx-max', 'TX')) 285 286 print(f'Current hardware settings:') 287 print_field(reply, 288 ('rx', 'RX'), 289 ('rx-mini', 'RX Mini'), 290 ('rx-jumbo', 'RX Jumbo'), 291 ('tx', 'TX')) 292 293 print_field(reply, 294 ('rx-buf-len', 'RX Buf Len'), 295 ('cqe-size', 'CQE Size'), 296 ('tx-push', 'TX Push', 'bool')) 297 298 return 299 300 if args.statistics: 301 print(f'NIC statistics:') 302 303 # TODO: pass id? 304 strset = dumpit(ynl, args, 'strset-get') 305 pprint.PrettyPrinter().pprint(strset) 306 307 req = { 308 'groups': { 309 'size': 1, 310 'bits': { 311 'bit': 312 # TODO: support passing the bitmask 313 #[ 314 #{ 'name': 'eth-phy', 'value': True }, 315 { 'name': 'eth-mac', 'value': True }, 316 #{ 'name': 'eth-ctrl', 'value': True }, 317 #{ 'name': 'rmon', 'value': True }, 318 #], 319 }, 320 }, 321 } 322 323 rsp = dumpit(ynl, args, 'stats-get', req) 324 pprint.PrettyPrinter().pprint(rsp) 325 return 326 327 if args.show_time_stamping: 328 req = { 329 'header': { 330 'flags': 'stats', 331 }, 332 } 333 334 tsinfo = dumpit(ynl, args, 'tsinfo-get', req) 335 336 print(f'Time stamping parameters for {args.device}:') 337 338 print('Capabilities:') 339 [print(f'\t{v}') for v in bits_to_dict(tsinfo['timestamping'])] 340 341 print(f'PTP Hardware Clock: {tsinfo["phc-index"]}') 342 343 print('Hardware Transmit Timestamp Modes:') 344 [print(f'\t{v}') for v in bits_to_dict(tsinfo['tx-types'])] 345 346 print('Hardware Receive Filter Modes:') 347 [print(f'\t{v}') for v in bits_to_dict(tsinfo['rx-filters'])] 348 349 print('Statistics:') 350 [print(f'\t{k}: {v}') for k, v in tsinfo['stats'].items()] 351 return 352 353 print(f'Settings for {args.device}:') 354 linkmodes = dumpit(ynl, args, 'linkmodes-get') 355 ours = bits_to_dict(linkmodes['ours']) 356 357 supported_ports = ('TP', 'AUI', 'BNC', 'MII', 'FIBRE', 'Backplane') 358 ports = [ p for p in supported_ports if ours.get(p, False)] 359 print(f'Supported ports: [ {" ".join(ports)} ]') 360 361 print_speed('Supported link modes', ours) 362 363 print_field(ours, ('Pause', 'Supported pause frame use', 'yn')) 364 print_field(ours, ('Autoneg', 'Supports auto-negotiation', 'yn')) 365 366 supported_fec = ('None', 'PS', 'BASER', 'LLRS') 367 fec = [ p for p in supported_fec if ours.get(p, False)] 368 fec_str = " ".join(fec) 369 if len(fec) == 0: 370 fec_str = "Not reported" 371 372 print(f'Supported FEC modes: {fec_str}') 373 374 speed = 'Unknown!' 375 if linkmodes['speed'] > 0 and linkmodes['speed'] < 0xffffffff: 376 speed = f'{linkmodes["speed"]}Mb/s' 377 print(f'Speed: {speed}') 378 379 duplex_modes = { 380 0: 'Half', 381 1: 'Full', 382 } 383 duplex = duplex_modes.get(linkmodes["duplex"], None) 384 if not duplex: 385 duplex = f'Unknown! ({linkmodes["duplex"]})' 386 print(f'Duplex: {duplex}') 387 388 autoneg = "off" 389 if linkmodes.get("autoneg", 0) != 0: 390 autoneg = "on" 391 print(f'Auto-negotiation: {autoneg}') 392 393 ports = { 394 0: 'Twisted Pair', 395 1: 'AUI', 396 2: 'MII', 397 3: 'FIBRE', 398 4: 'BNC', 399 5: 'Directly Attached Copper', 400 0xef: 'None', 401 } 402 linkinfo = dumpit(ynl, args, 'linkinfo-get') 403 print(f'Port: {ports.get(linkinfo["port"], "Other")}') 404 405 print_field(linkinfo, ('phyaddr', 'PHYAD')) 406 407 transceiver = { 408 0: 'Internal', 409 1: 'External', 410 } 411 print(f'Transceiver: {transceiver.get(linkinfo["transceiver"], "Unknown")}') 412 413 mdix_ctrl = { 414 1: 'off', 415 2: 'on', 416 } 417 mdix = mdix_ctrl.get(linkinfo['tp-mdix-ctrl'], None) 418 if mdix: 419 mdix = mdix + ' (forced)' 420 else: 421 mdix = mdix_ctrl.get(linkinfo['tp-mdix'], 'Unknown (auto)') 422 print(f'MDI-X: {mdix}') 423 424 debug = dumpit(ynl, args, 'debug-get') 425 msgmask = bits_to_dict(debug.get("msgmask", [])).keys() 426 print(f'Current message level: {" ".join(msgmask)}') 427 428 linkstate = dumpit(ynl, args, 'linkstate-get') 429 detected_states = { 430 0: 'no', 431 1: 'yes', 432 } 433 # TODO: wol-get 434 detected = detected_states.get(linkstate['link'], 'unknown') 435 print(f'Link detected: {detected}') 436 437if __name__ == '__main__': 438 main() 439