diff --git a/config/main.py b/config/main.py index a18a43be1d..cfc9c263e1 100644 --- a/config/main.py +++ b/config/main.py @@ -4048,7 +4048,7 @@ def fec(ctx, interface_name, interface_fec, verbose): @interface.group(cls=clicommon.AbbreviationGroup) @click.pass_context def ip(ctx): - """Add or remove IP address""" + """Set IP interface attributes""" pass # @@ -4179,6 +4179,32 @@ def remove(ctx, interface_name, ip_addr): command = "ip neigh flush dev {} {}".format(interface_name, str(ip_address)) clicommon.run_command(command) +# +# 'loopback-action' subcommand +# + +@ip.command() +@click.argument('interface_name', metavar='', required=True) +@click.argument('action', metavar='', required=True) +@click.pass_context +def loopback_action(ctx, interface_name, action): + """Set IP interface loopback action""" + 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 {} is invalid'.format(interface_name)) + + if not clicommon.is_interface_in_config_db(config_db, interface_name): + ctx.fail('Interface {} is not an IP interface'.format(interface_name)) + + allowed_actions = ['drop', 'forward'] + if action not in allowed_actions: + ctx.fail('Invalid action') + + table_name = get_interface_table_name(interface_name) + config_db.mod_entry(table_name, interface_name, {"loopback_action": action}) # # buffer commands and utilities @@ -4700,7 +4726,6 @@ def unbind(ctx, interface_name): remove_router_interface_ip_address(config_db, interface_name, ipaddress) config_db.set_entry(table_name, interface_name, None) - # # 'ipv6' subgroup ('config interface ipv6 ...') # diff --git a/doc/Command-Reference.md b/doc/Command-Reference.md index 4b78da135b..2c873235e1 100644 --- a/doc/Command-Reference.md +++ b/doc/Command-Reference.md @@ -3657,6 +3657,25 @@ This command is used to display the configured MPLS state for the list of config Ethernet4 enable ``` +**show interfaces loopback-action** + +This command displays the configured loopback action + +- Usage: + ``` + show ip interfaces loopback-action + ``` + +- Example: + ``` + root@sonic:~# show ip interfaces loopback-action + Interface Action + ------------ ---------- + Ethernet232 drop + Vlan100 forward + ``` + + **show interfaces tpid** This command displays the key fields of the interfaces such as Operational Status, Administrative Status, Alias and TPID. @@ -3803,6 +3822,7 @@ This sub-section explains the following list of configuration on the interfaces. 9) advertised-types - to set interface advertised types 10) type - to set interface type 11) mpls - To add or remove MPLS operation for the interface +12) loopback-action - to set action for packet that ingress and gets routed on the same IP interface From 201904 release onwards, the “config interface” command syntax is changed and the format is as follows: @@ -4336,6 +4356,29 @@ MPLS operation for either physical, portchannel, or VLAN interface can be config admin@sonic:~$ sudo config interface mpls remove Ethernet4 ``` +**config interface ip loopback-action (Versions >= 202205)** + +This command is used for setting the action being taken on packets that ingress and get routed on the same IP interface. +Loopback action can be set on IP interface from type physical, portchannel, VLAN interface and VLAN subinterface. +Loopback action can be drop or forward. + +- Usage: + ``` + config interface ip loopback-action --help + Usage: config interface ip loopback-action [OPTIONS] + + Set IP interface loopback action + + Options: + -?, -h, --help Show this message and exit. + ``` + +- Example: + ``` + admin@sonic:~$ config interface ip loopback-action Ethernet0 drop + admin@sonic:~$ config interface ip loopback-action Ethernet0 forward + + ``` Go Back To [Beginning of the document](#) or [Beginning of this section](#interfaces) ## Interface Naming Mode diff --git a/show/main.py b/show/main.py index 6520130f7c..24c4dd7077 100755 --- a/show/main.py +++ b/show/main.py @@ -805,15 +805,49 @@ def ip(): # Addresses from all scopes are included. Interfaces with no addresses are # excluded. # -@ip.command() + +@ip.group(invoke_without_command=True) @multi_asic_util.multi_asic_click_options -def interfaces(namespace, display): - cmd = "sudo ipintutil -a ipv4" - if namespace is not None: - cmd += " -n {}".format(namespace) +@click.pass_context +def interfaces(ctx, namespace, display): + if ctx.invoked_subcommand is None: + cmd = "sudo ipintutil -a ipv4" + if namespace is not None: + cmd += " -n {}".format(namespace) - cmd += " -d {}".format(display) - clicommon.run_command(cmd) + cmd += " -d {}".format(display) + clicommon.run_command(cmd) + +# +# 'show ip interfaces loopback-action' command +# + +@interfaces.command() +def loopback_action(): + """show ip interfaces loopback-action""" + config_db = ConfigDBConnector() + config_db.connect() + header = ['Interface', 'Action'] + body = [] + + if_tbl = config_db.get_table('INTERFACE') + vlan_if_tbl = config_db.get_table('VLAN_INTERFACE') + po_if_tbl = config_db.get_table('PORTCHANNEL_INTERFACE') + sub_if_tbl = config_db.get_table('VLAN_SUB_INTERFACE') + + all_tables = {} + for tbl in [if_tbl, vlan_if_tbl, po_if_tbl, sub_if_tbl]: + all_tables.update(tbl) + + if all_tables: + ifs_action = [] + ifs = list(all_tables.keys()) + for iface in ifs: + if 'loopback_action' in all_tables[iface]: + action = all_tables[iface]['loopback_action'] + ifs_action.append([iface, action]) + body = natsorted(ifs_action) + click.echo(tabulate(body, header)) # # 'route' subcommand ("show ip route") diff --git a/tests/loopback_action_test.py b/tests/loopback_action_test.py new file mode 100644 index 0000000000..58942b0c4b --- /dev/null +++ b/tests/loopback_action_test.py @@ -0,0 +1,139 @@ +import os +from click.testing import CliRunner +import config.main as config +import show.main as show +from utilities_common.db import Db + +show_ip_interfaces_loopback_action_output="""\ +Interface Action +--------------- -------- +Eth32.10 drop +Ethernet0 forward +PortChannel0001 drop +Vlan3000 forward +""" + +class TestLoopbackAction(object): + @classmethod + def setup_class(cls): + print("\nSETUP") + os.environ['UTILITIES_UNIT_TESTING'] = "1" + + def test_config_loopback_action_on_physical_interface(self): + runner = CliRunner() + db = Db() + obj = {'config_db':db.cfgdb} + action = 'drop' + iface = 'Ethernet0' + + result = runner.invoke(config.config.commands['interface'].commands["ip"].commands['loopback-action'], [iface, action], obj=obj) + + table = db.cfgdb.get_table('INTERFACE') + assert(table[iface]['loopback_action'] == action) + + print(result.exit_code, result.output) + assert result.exit_code == 0 + + def test_config_loopback_action_on_physical_interface_alias(self): + runner = CliRunner() + db = Db() + obj = {'config_db':db.cfgdb} + action = 'forward' + iface = 'Ethernet0' + iface_alias = 'etp1' + + os.environ['SONIC_CLI_IFACE_MODE'] = "alias" + result = runner.invoke(config.config.commands['interface'].commands["ip"].commands['loopback-action'], [iface_alias, action], obj=obj) + os.environ['SONIC_CLI_IFACE_MODE'] = "default" + + table = db.cfgdb.get_table('INTERFACE') + assert(table[iface]['loopback_action'] == action) + + print(result.exit_code, result.output) + assert result.exit_code == 0 + + def test_config_loopback_action_on_port_channel_interface(self): + runner = CliRunner() + db = Db() + obj = {'config_db':db.cfgdb} + action = 'forward' + iface = 'PortChannel0002' + + result = runner.invoke(config.config.commands['interface'].commands["ip"].commands['loopback-action'], [iface, action], obj=obj) + + table = db.cfgdb.get_table('PORTCHANNEL_INTERFACE') + assert(table[iface]['loopback_action'] == action) + + print(result.exit_code, result.output) + assert result.exit_code == 0 + + def test_config_loopback_action_on_vlan_interface(self): + runner = CliRunner() + db = Db() + obj = {'config_db':db.cfgdb} + action = 'drop' + iface = 'Vlan1000' + + result = runner.invoke(config.config.commands['interface'].commands["ip"].commands['loopback-action'], [iface, action], obj=obj) + + table = db.cfgdb.get_table('VLAN_INTERFACE') + assert(table[iface]['loopback_action'] == action) + + print(result.exit_code, result.output) + assert result.exit_code == 0 + + def test_config_loopback_action_on_subinterface(self): + runner = CliRunner() + db = Db() + obj = {'config_db':db.cfgdb} + action = 'forward' + iface = 'Ethernet0.10' + + result = runner.invoke(config.config.commands['interface'].commands["ip"].commands['loopback-action'], [iface, action], obj=obj) + + table = db.cfgdb.get_table('VLAN_SUB_INTERFACE') + assert(table[iface]['loopback_action'] == action) + + print(result.exit_code, result.output) + assert result.exit_code == 0 + + def test_show_ip_interfaces_loopback_action(self): + runner = CliRunner() + result = runner.invoke(show.cli.commands["ip"].commands["interfaces"].commands["loopback-action"], []) + + print(result.exit_code, result.output) + assert result.exit_code == 0 + assert result.output == show_ip_interfaces_loopback_action_output + + def test_config_loopback_action_on_non_ip_interface(self): + runner = CliRunner() + db = Db() + obj = {'config_db':db.cfgdb} + action = 'forward' + iface = 'Ethernet0.11' + ERROR_MSG = "Error: Interface {} is not an IP interface".format(iface) + + result = runner.invoke(config.config.commands['interface'].commands["ip"].commands['loopback-action'], [iface, action], obj=obj) + + print(result.exit_code, result.output) + assert result.exit_code != 0 + assert ERROR_MSG in result.output + + def test_config_loopback_action_invalid_action(self): + runner = CliRunner() + db = Db() + obj = {'config_db':db.cfgdb} + action = 'xforwardx' + iface = 'Ethernet0' + ERROR_MSG = "Error: Invalid action" + + result = runner.invoke(config.config.commands['interface'].commands["ip"].commands['loopback-action'], [iface, action], obj=obj) + + print(result.exit_code, result.output) + assert result.exit_code != 0 + assert ERROR_MSG in result.output + + @classmethod + def teardown_class(cls): + print("\nTEARDOWN") + os.environ['UTILITIES_UNIT_TESTING'] = "0" diff --git a/tests/mock_tables/config_db.json b/tests/mock_tables/config_db.json index 557cc124b9..be69cfc342 100644 --- a/tests/mock_tables/config_db.json +++ b/tests/mock_tables/config_db.json @@ -378,6 +378,7 @@ }, "VLAN_SUB_INTERFACE|Eth32.10": { "admin_status": "up", + "loopback_action": "drop", "vlan": "100" }, "VLAN_SUB_INTERFACE|Eth32.10|32.10.11.12/24": { @@ -558,6 +559,9 @@ "VLAN_INTERFACE|Vlan2000": { "proxy_arp": "enabled" }, + "VLAN_INTERFACE|Vlan3000": { + "loopback_action": "forward" + }, "VLAN_INTERFACE|Vlan1000|192.168.0.1/21": { "NULL": "NULL" }, @@ -642,7 +646,8 @@ "NULL": "NULL" }, "PORTCHANNEL_INTERFACE|PortChannel0001": { - "ipv6_use_link_local_only": "disable" + "ipv6_use_link_local_only": "disable", + "loopback_action": "drop" }, "PORTCHANNEL_INTERFACE|PortChannel0002": { "NULL": "NULL" @@ -678,7 +683,8 @@ "NULL": "NULL" }, "INTERFACE|Ethernet0": { - "ipv6_use_link_local_only": "disable" + "ipv6_use_link_local_only": "disable", + "loopback_action": "forward" }, "INTERFACE|Ethernet0|14.14.0.1/24": { "NULL": "NULL" diff --git a/utilities_common/cli.py b/utilities_common/cli.py index c05069adcf..3872a47877 100644 --- a/utilities_common/cli.py +++ b/utilities_common/cli.py @@ -593,6 +593,7 @@ def is_interface_in_config_db(config_db, interface_name): if (not interface_name in config_db.get_keys('VLAN_INTERFACE') and not interface_name in config_db.get_keys('INTERFACE') and not interface_name in config_db.get_keys('PORTCHANNEL_INTERFACE') and + not interface_name in config_db.get_keys('VLAN_SUB_INTERFACE') and not interface_name == 'null'): return False