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