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