Skip to content

Commit

Permalink
sonic-utils: initial support for link-training (#2071)
Browse files Browse the repository at this point in the history
HLD: sonic-net/SONiC#925

#### What I did
Add CLI support for link-trainig

#### How I did it
1. portconfig: initial support for link-training config
2. config/main.py: initial support for link-training config
3. intfutil: initial support for link-training show command
4. show/interfaces/__init__.py: initial support for link-training show command

#### How to verify it
1. Manual test
2. Ran the Unit-tests to the corresponding changes

#### Previous command output (if the output of a command-line utility has changed)

#### New command output (if the output of a command-line utility has changed)

```
admin@sonic:~$ sudo config interface link-training Ethernet0 on
admin@sonic:~$ sudo config interface link-training Ethernet8 on
admin@sonic:~$ sudo config interface link-training Ethernet16 off
admin@sonic:~$ sudo config interface link-training Ethernet24 on
admin@sonic:~$ sudo config interface link-training Ethernet32 off
admin@sonic:~$ show interfaces link-training status
  Interface      LT Oper    LT Admin    Oper    Admin
-----------  -----------  ----------  ------  -------
  Ethernet0      trained          on      up       up
  Ethernet8      trained          on      up       up
 Ethernet16          off         off    down       up
 Ethernet24  not trained          on    down       up
 Ethernet32          off         off    down       up
 Ethernet40          off           -    down       up
 Ethernet48          off           -    down       up
 Ethernet56          off           -    down       up
 Ethernet64          off           -    down       up
 Ethernet72          off           -    down       up
 Ethernet80          off           -    down       up
 Ethernet88          off           -    down       up
 Ethernet96          off           -    down       up
Ethernet104          off           -    down       up
Ethernet112          off           -    down       up
Ethernet120          off           -    down       up
Ethernet128          off           -    down       up
Ethernet136          off           -    down       up
Ethernet144          off           -    down       up
Ethernet152          off           -    down       up
Ethernet160          off           -    down       up
Ethernet168          off           -    down       up
Ethernet176          off           -    down       up
Ethernet184          off           -    down       up
Ethernet192          off           -    down       up
Ethernet200          off           -    down       up
Ethernet208          off           -    down       up
Ethernet216          off           -    down       up
Ethernet224          off           -    down       up
Ethernet232          off           -    down       up
Ethernet240          off           -    down       up
Ethernet248          off           -    down       up
admin@sonic:~$
```
  • Loading branch information
ds952811 authored and yxieca committed Aug 11, 2022
1 parent c088ec4 commit 9bdbfb8
Show file tree
Hide file tree
Showing 9 changed files with 251 additions and 25 deletions.
30 changes: 30 additions & 0 deletions config/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3730,6 +3730,36 @@ def speed(ctx, interface_name, interface_speed, verbose):
command += " -vv"
clicommon.run_command(command, display_cmd=verbose)

#
# 'link-training' subcommand
#

@interface.command()
@click.pass_context
@click.argument('interface_name', metavar='<interface_name>', required=True)
@click.argument('mode', metavar='<mode>', required=True, type=click.Choice(["on", "off"]))
@click.option('-v', '--verbose', is_flag=True, help="Enable verbose output")
def link_training(ctx, interface_name, mode, verbose):
"""Set interface link training mode"""
# Get the config_db connector
config_db = ctx.obj['config_db']

if clicommon.get_interface_naming_mode() == "alias":
interface_name = interface_alias_to_name(config_db, interface_name)
if interface_name is None:
ctx.fail("'interface_name' is None!")

log.log_info("'interface link-training {} {}' executing...".format(interface_name, mode))

if ctx.obj['namespace'] is DEFAULT_NAMESPACE:
command = "portconfig -p {} -lt {}".format(interface_name, mode)
else:
command = "portconfig -p {} -lt {} -n {}".format(interface_name, mode, ctx.obj['namespace'])

if verbose:
command += " -vv"
clicommon.run_command(command, display_cmd=verbose)

#
# 'autoneg' subcommand
#
Expand Down
66 changes: 66 additions & 0 deletions scripts/intfutil
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ PORT_INTERFACE_TYPE = 'interface_type'
PORT_ADV_INTERFACE_TYPES = 'adv_interface_types'
PORT_TPID = "tpid"
OPTICS_TYPE_RJ45 = 'RJ45'
PORT_LINK_TRAINING = 'link_training'
PORT_LINK_TRAINING_STATUS = 'link_training_status'

VLAN_SUB_INTERFACE_SEPARATOR = "."
VLAN_SUB_INTERFACE_TYPE = "802.1q-encapsulation"
Expand Down Expand Up @@ -739,6 +741,67 @@ class IntfTpid(object):
self.table += self.generate_intf_tpid()


# ========================== interface-link-training logic ==========================
header_link_training = ['Interface', 'LT Oper', 'LT Admin', 'Oper', 'Admin']

class IntfLinkTrainingStatus(object):

def __init__(self, intf_name, namespace_option, display_option):
self.db = None
self.config_db = None
self.table = []
self.multi_asic = multi_asic_util.MultiAsic(
display_option, namespace_option)

if intf_name is not None and intf_name == SUB_PORT:
self.intf_name = None
else:
self.intf_name = intf_name

def display_link_training_status(self):
self.get_intf_link_training_status()
# Sorting and tabulating the result table.
sorted_table = natsorted(self.table)
print(tabulate(sorted_table, header_link_training, tablefmt="simple", stralign='right'))

@multi_asic_util.run_on_multi_asic
def get_intf_link_training_status(self):
self.front_panel_ports_list = get_frontpanel_port_list(self.config_db)
self.appl_db_keys = appl_db_keys_get(self.db, self.front_panel_ports_list, self.intf_name)
if self.appl_db_keys:
self.table += self.generate_link_training_status()

def generate_link_training_status(self):
"""
Generate interface-link-training output
"""

i = {}
table = []
key = []

#
# Iterate through all the keys and append port's associated state to
# the result table.
#
for i in self.appl_db_keys:
key = re.split(':', i, maxsplit=1)[-1].strip()
if key in self.front_panel_ports_list:
if self.multi_asic.skip_display(constants.PORT_OBJ, key):
continue
lt_admin = appl_db_port_status_get(self.db, key, PORT_LINK_TRAINING)
if lt_admin not in ['on', 'off']:
lt_admin = '-'
lt_status = state_db_port_status_get(self.db, key, PORT_LINK_TRAINING_STATUS)
if lt_status in ['N/A', '', None]:
lt_status = 'off'
table.append((key,
lt_status.replace('_', ' '),
lt_admin,
appl_db_port_status_get(self.db, key, PORT_OPER_STATUS),
appl_db_port_status_get(self.db, key, PORT_ADMIN_STATUS)))
return table

def main():
parser = argparse.ArgumentParser(description='Display Interface information',
formatter_class=argparse.RawTextHelpFormatter)
Expand All @@ -759,6 +822,9 @@ def main():
elif args.command == "tpid":
interface_tpid = IntfTpid(args.interface, args.namespace, args.display)
interface_tpid.display_intf_tpid()
elif args.command == "link_training":
interface_lt_status = IntfLinkTrainingStatus(args.interface, args.namespace, args.display)
interface_lt_status.display_link_training_status()

sys.exit(0)

Expand Down
18 changes: 17 additions & 1 deletion scripts/portconfig
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ optional arguments:
-S --adv-speeds port advertised speeds
-t --interface-type port interface type
-T --adv-interface-types port advertised interface types
-lt --link-training port link training mode
"""
import os
import sys
Expand Down Expand Up @@ -49,6 +50,7 @@ PORT_AUTONEG_CONFIG_FIELD_NAME = "autoneg"
PORT_ADV_SPEEDS_CONFIG_FIELD_NAME = "adv_speeds"
PORT_INTERFACE_TYPE_CONFIG_FIELD_NAME = "interface_type"
PORT_ADV_INTERFACE_TYPES_CONFIG_FIELD_NAME = "adv_interface_types"
PORT_LINK_TRAINING_CONFIG_FIELD_NAME = "link_training"
PORT_CHANNEL_TABLE_NAME = "PORTCHANNEL"
PORT_CHANNEL_MBR_TABLE_NAME = "PORTCHANNEL_MEMBER"
TPID_CONFIG_FIELD_NAME = "tpid"
Expand Down Expand Up @@ -134,6 +136,16 @@ class portconfig(object):
print("Setting mtu %s on port %s" % (mtu, port))
self.db.mod_entry(PORT_TABLE_NAME, port, {PORT_MTU_CONFIG_FIELD_NAME: mtu})

def set_link_training(self, port, mode):
if self.verbose:
print("Setting link-training %s on port %s" % (mode, port))
lt_modes = ['on', 'off']
if mode not in lt_modes:
print('Invalid mode specified: {}'.format(mode))
print('Valid modes: {}'.format(','.join(lt_modes)))
exit(1)
self.db.mod_entry(PORT_TABLE_NAME, port, {PORT_LINK_TRAINING_CONFIG_FIELD_NAME: mode})

def set_autoneg(self, port, mode):
if self.verbose:
print("Setting autoneg %s on port %s" % (mode, port))
Expand Down Expand Up @@ -266,6 +278,8 @@ def main():
help = 'port interface type', default=None)
parser.add_argument('-T', '--adv-interface-types', type = str, required = False,
help = 'port advertised interface types', default=None)
parser.add_argument('-lt', '--link-training', type = str, required = False,
help = 'port link training mode', default=None)
args = parser.parse_args()

# Load database config files
Expand All @@ -274,13 +288,15 @@ def main():
port = portconfig(args.verbose, args.port, args.namespace)
if args.list:
port.list_params(args.port)
elif args.speed or args.fec or args.mtu or args.autoneg or args.adv_speeds or args.interface_type or args.adv_interface_types or args.tpid:
elif args.speed or args.fec or args.mtu or args.link_training or args.autoneg or args.adv_speeds or args.interface_type or args.adv_interface_types or args.tpid:
if args.speed:
port.set_speed(args.port, args.speed)
if args.fec:
port.set_fec(args.port, args.fec)
if args.mtu:
port.set_mtu(args.port, args.mtu)
if args.link_training:
port.set_link_training(args.port, args.link_training)
if args.autoneg:
port.set_autoneg(args.port, args.autoneg)
if args.adv_speeds:
Expand Down
33 changes: 33 additions & 0 deletions show/interfaces/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -639,3 +639,36 @@ def autoneg_status(interfacename, namespace, display, verbose):
cmd += " -n {}".format(namespace)

clicommon.run_command(cmd, display_cmd=verbose)

#
# link-training group (show interfaces link-training ...)
#
@interfaces.group(name='link-training', cls=clicommon.AliasedGroup)
def link_training():
"""Show interface link-training information"""
pass

# 'link-training status' subcommand ("show interfaces link-training status")
@link_training.command(name='status')
@click.argument('interfacename', required=False)
@multi_asic_util.multi_asic_click_options
@click.option('--verbose', is_flag=True, help="Enable verbose output")
def link_training_status(interfacename, namespace, display, verbose):
"""Show interface link-training status"""

ctx = click.get_current_context()

cmd = "intfutil -c link_training"

#ignore the display option when interface name is passed
if interfacename is not None:
interfacename = try_convert_interfacename_from_alias(ctx, interfacename)

cmd += " -i {}".format(interfacename)
else:
cmd += " -d {}".format(display)

if namespace is not None:
cmd += " -n {}".format(namespace)

clicommon.run_command(cmd, display_cmd=verbose)
43 changes: 43 additions & 0 deletions tests/config_lt_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import click
import config.main as config
import operator
import os
import pytest
import sys

from click.testing import CliRunner
from utilities_common.db import Db

test_path = os.path.dirname(os.path.abspath(__file__))
modules_path = os.path.dirname(test_path)
scripts_path = os.path.join(modules_path, "scripts")
sys.path.insert(0, modules_path)


@pytest.fixture(scope='module')
def ctx(scope='module'):
db = Db()
obj = {'config_db':db.cfgdb, 'namespace': ''}
yield obj


class TestConfigInterface(object):
@classmethod
def setup_class(cls):
print("SETUP")
os.environ["PATH"] += os.pathsep + scripts_path
os.environ["UTILITIES_UNIT_TESTING"] = "1"

def test_config_link_training(self, ctx):
self.basic_check("link-training", ["Ethernet0", "on"], ctx)
self.basic_check("link-training", ["Ethernet0", "off"], ctx)
self.basic_check("link-training", ["Invalid", "on"], ctx, operator.ne)
self.basic_check("link-training", ["Invalid", "off"], ctx, operator.ne)
self.basic_check("link-training", ["Ethernet0", "invalid"], ctx, operator.ne)

def basic_check(self, command_name, para_list, ctx, op=operator.eq, expect_result=0):
runner = CliRunner()
result = runner.invoke(config.config.commands["interface"].commands[command_name], para_list, obj = ctx)
print(result.output)
assert op(result.exit_code, expect_result)
return result
37 changes: 19 additions & 18 deletions tests/dump_tests/dump_state_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,22 @@ def compare_json_output(exp_json, rec, exclude_paths=None):


table_display_output = '''\
+-------------+-----------+----------------------------------------------------------------------------+
| port_name | DB_NAME | DUMP |
+=============+===========+============================================================================+
| Ethernet0 | STATE_DB | +----------------------+-------------------------------------------------+ |
| | | | Keys | field-value pairs | |
| | | +======================+=================================================+ |
| | | | PORT_TABLE|Ethernet0 | +------------------+--------------------------+ | |
| | | | | | field | value | | |
| | | | | |------------------+--------------------------| | |
| | | | | | rmt_adv_speeds | 10,100,1000 | | |
| | | | | | speed | 100000 | | |
| | | | | | supported_speeds | 10000,25000,40000,100000 | | |
| | | | | +------------------+--------------------------+ | |
| | | +----------------------+-------------------------------------------------+ |
+-------------+-----------+----------------------------------------------------------------------------+
+-------------+-----------+--------------------------------------------------------------------------------+
| port_name | DB_NAME | DUMP |
+=============+===========+================================================================================+
| Ethernet0 | STATE_DB | +----------------------+-----------------------------------------------------+ |
| | | | Keys | field-value pairs | |
| | | +======================+=====================================================+ |
| | | | PORT_TABLE|Ethernet0 | +----------------------+--------------------------+ | |
| | | | | | field | value | | |
| | | | | |----------------------+--------------------------| | |
| | | | | | rmt_adv_speeds | 10,100,1000 | | |
| | | | | | speed | 100000 | | |
| | | | | | supported_speeds | 10000,25000,40000,100000 | | |
| | | | | | link_training_status | not_trained | | |
| | | | | +----------------------+--------------------------+ | |
| | | +----------------------+-----------------------------------------------------+ |
+-------------+-----------+--------------------------------------------------------------------------------+
'''


Expand Down Expand Up @@ -121,7 +122,7 @@ def test_identifier_single(self):
expected = {'Ethernet0': {'CONFIG_DB': {'keys': [{'PORT|Ethernet0': {'alias': 'etp1', 'description': 'etp1', 'index': '0', 'lanes': '25,26,27,28', 'mtu': '9100', 'pfc_asym': 'off', 'speed': '40000'}}], 'tables_not_found': []},
'APPL_DB': {'keys': [{'PORT_TABLE:Ethernet0': {'index': '0', 'lanes': '0', 'alias': 'Ethernet0', 'description': 'ARISTA01T2:Ethernet1', 'speed': '25000', 'oper_status': 'down', 'pfc_asym': 'off', 'mtu': '9100', 'fec': 'rs', 'admin_status': 'up'}}], 'tables_not_found': []},
'ASIC_DB': {'keys': [{'ASIC_STATE:SAI_OBJECT_TYPE_HOSTIF:oid:0xd00000000056d': {'SAI_HOSTIF_ATTR_NAME': 'Ethernet0', 'SAI_HOSTIF_ATTR_OBJ_ID': 'oid:0x10000000004a4', 'SAI_HOSTIF_ATTR_OPER_STATUS': 'true', 'SAI_HOSTIF_ATTR_TYPE': 'SAI_HOSTIF_TYPE_NETDEV', 'SAI_HOSTIF_ATTR_VLAN_TAG': 'SAI_HOSTIF_VLAN_TAG_STRIP'}}, {'ASIC_STATE:SAI_OBJECT_TYPE_PORT:oid:0x10000000004a4': {'NULL': 'NULL', 'SAI_PORT_ATTR_ADMIN_STATE': 'true', 'SAI_PORT_ATTR_MTU': '9122', 'SAI_PORT_ATTR_SPEED': '100000'}}], 'tables_not_found': [], 'vidtorid': {'oid:0xd00000000056d': 'oid:0xd', 'oid:0x10000000004a4': 'oid:0x1690000000001'}},
'STATE_DB': {'keys': [{'PORT_TABLE|Ethernet0': {'rmt_adv_speeds': '10,100,1000', 'speed': '100000', 'supported_speeds': '10000,25000,40000,100000'}}], 'tables_not_found': []}}}
'STATE_DB': {'keys': [{'PORT_TABLE|Ethernet0': {'rmt_adv_speeds': '10,100,1000', 'speed': '100000', 'supported_speeds': '10000,25000,40000,100000', 'link_training_status': 'not_trained'}}], 'tables_not_found': []}}}

assert result.exit_code == 0, "exit code: {}, Exception: {}, Traceback: {}".format(result.exit_code, result.exception, result.exc_info)
# Cause other tests depend and change these paths in the mock_db, this test would fail everytime when a field or a value in changed in this path, creating noise
Expand All @@ -138,7 +139,7 @@ def test_identifier_multiple(self):
{"CONFIG_DB": {"keys": [{"PORT|Ethernet0": {"alias": "etp1", "description": "etp1", "index": "0", "lanes": "25,26,27,28", "mtu": "9100", "pfc_asym": "off", "speed": "40000"}}], "tables_not_found": []},
"APPL_DB": {"keys": [{"PORT_TABLE:Ethernet0": {"index": "0", "lanes": "0", "alias": "Ethernet0", "description": "ARISTA01T2:Ethernet1", "speed": "25000", "oper_status": "down", "pfc_asym": "off", "mtu": "9100", "fec": "rs", "admin_status": "up"}}], "tables_not_found": []},
"ASIC_DB": {"keys": [{"ASIC_STATE:SAI_OBJECT_TYPE_HOSTIF:oid:0xd00000000056d": {"SAI_HOSTIF_ATTR_NAME": "Ethernet0", "SAI_HOSTIF_ATTR_OBJ_ID": "oid:0x10000000004a4", "SAI_HOSTIF_ATTR_OPER_STATUS": "true", "SAI_HOSTIF_ATTR_TYPE": "SAI_HOSTIF_TYPE_NETDEV", "SAI_HOSTIF_ATTR_VLAN_TAG": "SAI_HOSTIF_VLAN_TAG_STRIP"}}, {"ASIC_STATE:SAI_OBJECT_TYPE_PORT:oid:0x10000000004a4": {"NULL": "NULL", "SAI_PORT_ATTR_ADMIN_STATE": "true", "SAI_PORT_ATTR_MTU": "9122", "SAI_PORT_ATTR_SPEED": "100000"}}], "tables_not_found": [], "vidtorid": {"oid:0xd00000000056d": "oid:0xd", "oid:0x10000000004a4": "oid:0x1690000000001"}},
"STATE_DB": {"keys": [{"PORT_TABLE|Ethernet0": {"rmt_adv_speeds": "10,100,1000", "speed": "100000", "supported_speeds": "10000,25000,40000,100000"}}], "tables_not_found": []}},
"STATE_DB": {"keys": [{"PORT_TABLE|Ethernet0": {"rmt_adv_speeds": "10,100,1000", "speed": "100000", "supported_speeds": "10000,25000,40000,100000", "link_training_status": "not_trained"}}], "tables_not_found": []}},
"Ethernet4":
{"CONFIG_DB": {"keys": [{"PORT|Ethernet4": {"admin_status": "up", "alias": "etp2", "description": "Servers0:eth0", "index": "1", "lanes": "29,30,31,32", "mtu": "9100", "pfc_asym": "off", "speed": "40000"}}], "tables_not_found": []},
"APPL_DB": {"keys": [], "tables_not_found": ["PORT_TABLE"]},
Expand Down Expand Up @@ -167,7 +168,7 @@ def test_option_db_filtering(self):
result = runner.invoke(dump.state, ["port", "Ethernet0", "--db", "ASIC_DB", "--db", "STATE_DB"])
print(result.output)
expected = {"Ethernet0": {"ASIC_DB": {"keys": [{"ASIC_STATE:SAI_OBJECT_TYPE_HOSTIF:oid:0xd00000000056d": {"SAI_HOSTIF_ATTR_NAME": "Ethernet0", "SAI_HOSTIF_ATTR_OBJ_ID": "oid:0x10000000004a4", "SAI_HOSTIF_ATTR_OPER_STATUS": "true", "SAI_HOSTIF_ATTR_TYPE": "SAI_HOSTIF_TYPE_NETDEV", "SAI_HOSTIF_ATTR_VLAN_TAG": "SAI_HOSTIF_VLAN_TAG_STRIP"}}, {"ASIC_STATE:SAI_OBJECT_TYPE_PORT:oid:0x10000000004a4": {"NULL": "NULL", "SAI_PORT_ATTR_ADMIN_STATE": "true", "SAI_PORT_ATTR_MTU": "9122", "SAI_PORT_ATTR_SPEED": "100000"}}], "tables_not_found": [], "vidtorid": {"oid:0xd00000000056d": "oid:0xd", "oid:0x10000000004a4": "oid:0x1690000000001"}},
"STATE_DB": {"keys": [{"PORT_TABLE|Ethernet0": {"rmt_adv_speeds": "10,100,1000", "speed": "100000", "supported_speeds": "10000,25000,40000,100000"}}], "tables_not_found": []}}}
"STATE_DB": {"keys": [{"PORT_TABLE|Ethernet0": {"rmt_adv_speeds": "10,100,1000", "speed": "100000", "supported_speeds": "10000,25000,40000,100000", "link_training_status": "not_trained"}}], "tables_not_found": []}}}
assert result.exit_code == 0, "exit code: {}, Exception: {}, Traceback: {}".format(result.exit_code, result.exception, result.exc_info)
ddiff = compare_json_output(expected, result.output)
assert not ddiff, ddiff
Expand Down
Loading

0 comments on commit 9bdbfb8

Please sign in to comment.