From 29f4a1642024050a104d07316c15e012905f7d32 Mon Sep 17 00:00:00 2001 From: Akhilesh Samineni <47657796+AkhileshSamineni@users.noreply.github.com> Date: Thu, 19 Aug 2021 00:10:44 +0530 Subject: [PATCH] Global and Interface commands for IPv6 Link local address enhancements (#1159) * Global and Interface commands for IPv6 Link local feature * SONiC CLI per interface configuration command to enable and disable the IPv6 link-local address mode when addresses are not configured manually. Signed-off-by: Akhilesh Samineni --- config/main.py | 253 ++++++++++++++++++++++++++++++- doc/Command-Reference.md | 93 ++++++++++++ show/main.py | 29 ++++ tests/ipv6_link_local_test.py | 153 +++++++++++++++++++ tests/mock_tables/config_db.json | 4 +- 5 files changed, 529 insertions(+), 3 deletions(-) create mode 100644 tests/ipv6_link_local_test.py diff --git a/config/main.py b/config/main.py index 14aae25e62bc..c4782ebfe0f4 100644 --- a/config/main.py +++ b/config/main.py @@ -498,6 +498,16 @@ def set_interface_naming_mode(mode): f.close() click.echo("Please logout and log back in for changes take effect.") +def get_intf_ipv6_link_local_mode(ctx, interface_name, table_name): + config_db = ctx.obj["config_db"] + intf = config_db.get_table(table_name) + if interface_name in intf: + if 'ipv6_use_link_local_only' in intf[interface_name]: + return intf[interface_name]['ipv6_use_link_local_only'] + else: + return "disable" + else: + return "" def _is_neighbor_ipaddress(config_db, ipaddress): """Returns True if a neighbor has the IP address , False if not @@ -3698,7 +3708,7 @@ def remove(ctx, interface_name, ip_addr): ctx.fail("Cannot remove the last IP entry of interface {}. A static {} route is still bound to the RIF.".format(interface_name, ip_ver)) config_db.set_entry(table_name, (interface_name, ip_addr), None) interface_dependent = interface_ipaddr_dependent_on_interface(config_db, interface_name) - if len(interface_dependent) == 0 and is_interface_bind_to_vrf(config_db, interface_name) is False: + if len(interface_dependent) == 0 and is_interface_bind_to_vrf(config_db, interface_name) is False and get_intf_ipv6_link_local_mode(ctx, interface_name, table_name) != "enable": config_db.set_entry(table_name, interface_name, None) if multi_asic.is_multi_asic(): @@ -4133,6 +4143,130 @@ def unbind(ctx, interface_name): config_db.set_entry(table_name, interface_name, None) +# +# 'ipv6' subgroup ('config interface ipv6 ...') +# + +@interface.group() +@click.pass_context +def ipv6(ctx): + """Enable or Disable IPv6 processing on interface""" + pass + +@ipv6.group('enable') +def enable(): + """Enable IPv6 processing on interface""" + pass + +@ipv6.group('disable') +def disable(): + """Disble IPv6 processing on interface""" + pass + +# +# 'config interface ipv6 enable use-link-local-only ' +# + +@enable.command('use-link-local-only') +@click.pass_context +@click.argument('interface_name', metavar='', required=True) +def enable_use_link_local_only(ctx, interface_name): + """Enable IPv6 link local address on interface""" + config_db = ConfigDBConnector() + config_db.connect() + ctx.obj = {} + ctx.obj['config_db'] = config_db + db = ctx.obj["config_db"] + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(db, interface_name) + if interface_name is None: + ctx.fail("'interface_name' is None!") + + if interface_name.startswith("Ethernet"): + interface_type = "INTERFACE" + elif interface_name.startswith("PortChannel"): + interface_type = "PORTCHANNEL_INTERFACE" + elif interface_name.startswith("Vlan"): + interface_type = "VLAN_INTERFACE" + else: + ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + + if (interface_type == "INTERFACE" ) or (interface_type == "PORTCHANNEL_INTERFACE"): + if interface_name_is_valid(db, interface_name) is False: + ctx.fail("Interface name %s is invalid. Please enter a valid interface name!!" %(interface_name)) + + if (interface_type == "VLAN_INTERFACE"): + if not clicommon.is_valid_vlan_interface(db, interface_name): + ctx.fail("Interface name %s is invalid. Please enter a valid interface name!!" %(interface_name)) + + portchannel_member_table = db.get_table('PORTCHANNEL_MEMBER') + + if interface_is_in_portchannel(portchannel_member_table, interface_name): + ctx.fail("{} is configured as a member of portchannel. Cannot configure the IPv6 link local mode!" + .format(interface_name)) + + vlan_member_table = db.get_table('VLAN_MEMBER') + + if interface_is_in_vlan(vlan_member_table, interface_name): + ctx.fail("{} is configured as a member of vlan. Cannot configure the IPv6 link local mode!" + .format(interface_name)) + + interface_dict = db.get_table(interface_type) + set_ipv6_link_local_only_on_interface(db, interface_dict, interface_type, interface_name, "enable") + +# +# 'config interface ipv6 disable use-link-local-only ' +# + +@disable.command('use-link-local-only') +@click.pass_context +@click.argument('interface_name', metavar='', required=True) +def disable_use_link_local_only(ctx, interface_name): + """Disable IPv6 link local address on interface""" + config_db = ConfigDBConnector() + config_db.connect() + ctx.obj = {} + ctx.obj['config_db'] = config_db + db = ctx.obj["config_db"] + + if clicommon.get_interface_naming_mode() == "alias": + interface_name = interface_alias_to_name(db, interface_name) + if interface_name is None: + ctx.fail("'interface_name' is None!") + + interface_type = "" + if interface_name.startswith("Ethernet"): + interface_type = "INTERFACE" + elif interface_name.startswith("PortChannel"): + interface_type = "PORTCHANNEL_INTERFACE" + elif interface_name.startswith("Vlan"): + interface_type = "VLAN_INTERFACE" + else: + ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan]") + + if (interface_type == "INTERFACE" ) or (interface_type == "PORTCHANNEL_INTERFACE"): + if interface_name_is_valid(db, interface_name) is False: + ctx.fail("Interface name %s is invalid. Please enter a valid interface name!!" %(interface_name)) + + if (interface_type == "VLAN_INTERFACE"): + if not clicommon.is_valid_vlan_interface(db, interface_name): + ctx.fail("Interface name %s is invalid. Please enter a valid interface name!!" %(interface_name)) + + portchannel_member_table = db.get_table('PORTCHANNEL_MEMBER') + + if interface_is_in_portchannel(portchannel_member_table, interface_name): + ctx.fail("{} is configured as a member of portchannel. Cannot configure the IPv6 link local mode!" + .format(interface_name)) + + vlan_member_table = db.get_table('VLAN_MEMBER') + if interface_is_in_vlan(vlan_member_table, interface_name): + ctx.fail("{} is configured as a member of vlan. Cannot configure the IPv6 link local mode!" + .format(interface_name)) + + interface_dict = db.get_table(interface_type) + set_ipv6_link_local_only_on_interface(db, interface_dict, interface_type, interface_name, "disable") + # # 'vrf' group ('config vrf ...') # @@ -5554,6 +5688,123 @@ def delete(ctx): sflow_tbl['global'].pop('agent_id') config_db.set_entry('SFLOW', 'global', sflow_tbl['global']) +# +# set ipv6 link local mode on a given interface +# +def set_ipv6_link_local_only_on_interface(config_db, interface_dict, interface_type, interface_name, mode): + + curr_mode = config_db.get_entry(interface_type, interface_name).get('ipv6_use_link_local_only') + if curr_mode is not None: + if curr_mode == mode: + return + else: + if mode == "disable": + return + + if mode == "enable": + config_db.mod_entry(interface_type, interface_name, {"ipv6_use_link_local_only": mode}) + return + + # If we are disabling the ipv6 link local on an interface, and if no other interface + # attributes/ip addresses are configured on the interface, delete the interface from the interface table + exists = False + for key in interface_dict.keys(): + if not isinstance(key, tuple): + if interface_name == key: + #Interface bound to non-default-vrf do not delete the entry + if 'vrf_name' in interface_dict[key]: + if len(interface_dict[key]['vrf_name']) > 0: + exists = True + break + continue + if interface_name in key: + exists = True + break + + if exists: + config_db.mod_entry(interface_type, interface_name, {"ipv6_use_link_local_only": mode}) + else: + config_db.set_entry(interface_type, interface_name, None) + +# +# 'ipv6' group ('config ipv6 ...') +# + +@config.group() +@click.pass_context +def ipv6(ctx): + """IPv6 configuration""" + +# +# 'enable' command ('config ipv6 enable ...') +# +@ipv6.group() +@click.pass_context +def enable(ctx): + """Enable IPv6 on all interfaces """ + +# +# 'link-local' command ('config ipv6 enable link-local') +# +@enable.command('link-local') +@click.pass_context +def enable_link_local(ctx): + """Enable IPv6 link-local on all interfaces """ + config_db = ConfigDBConnector() + config_db.connect() + vlan_member_table = config_db.get_table('VLAN_MEMBER') + portchannel_member_table = config_db.get_table('PORTCHANNEL_MEMBER') + + mode = "enable" + + # Enable ipv6 link local on VLANs + vlan_dict = config_db.get_table('VLAN') + for key in vlan_dict.keys(): + set_ipv6_link_local_only_on_interface(config_db, vlan_dict, 'VLAN_INTERFACE', key, mode) + + # Enable ipv6 link local on PortChannels + portchannel_dict = config_db.get_table('PORTCHANNEL') + for key in portchannel_dict.keys(): + if interface_is_in_vlan(vlan_member_table, key): + continue + set_ipv6_link_local_only_on_interface(config_db, portchannel_dict, 'PORTCHANNEL_INTERFACE', key, mode) + + port_dict = config_db.get_table('PORT') + for key in port_dict.keys(): + if interface_is_in_portchannel(portchannel_member_table, key) or interface_is_in_vlan(vlan_member_table, key): + continue + set_ipv6_link_local_only_on_interface(config_db, port_dict, 'INTERFACE', key, mode) + +# +# 'disable' command ('config ipv6 disable ...') +# +@ipv6.group() +@click.pass_context +def disable(ctx): + """Disable IPv6 on all interfaces """ + +# +# 'link-local' command ('config ipv6 disable link-local') +# +@disable.command('link-local') +@click.pass_context +def disable_link_local(ctx): + """Disable IPv6 link local on all interfaces """ + config_db = ConfigDBConnector() + config_db.connect() + + mode = "disable" + + tables = ['INTERFACE', 'VLAN_INTERFACE', 'PORTCHANNEL_INTERFACE'] + + for table_type in tables: + table_dict = config_db.get_table(table_type) + if table_dict: + for key in table_dict.keys(): + if isinstance(key, str) is False: + continue + set_ipv6_link_local_only_on_interface(config_db, table_dict, table_type, key, mode) + # Load plugins and register them helper = util_base.UtilHelper() diff --git a/doc/Command-Reference.md b/doc/Command-Reference.md index b6248a7816ac..5582bc9b5629 100644 --- a/doc/Command-Reference.md +++ b/doc/Command-Reference.md @@ -65,6 +65,9 @@ * [IP / IPv6](#ip--ipv6) * [IP show commands](#ip-show-commands) * [IPv6 show commands](#ipv6-show-commands) +* [IPv6 Link Local](#ipv6-link-local) + * [IPv6 Link Local config commands](#ipv6-link-local-config-commands) + * [IPv6 Link Local show commands](#ipv6-link-local-show-commands) * [Kubernetes](#Kubernetes) * [Kubernetes show commands](#Kubernetes-show-commands) * [Kubernetes config commands](#Kubernetes-config-commands) @@ -4407,6 +4410,96 @@ Refer the routing stack [Quagga Command Reference](https://www.quagga.net/docs/q Go Back To [Beginning of the document](#) or [Beginning of this section](#ip--ipv6) +## IPv6 Link Local + +### IPv6 Link Local config commands + +This section explains all the commands that are supported in SONiC to configure IPv6 Link-local. + +**config interface ipv6 enable use-link-local-only ** + +This command enables user to enable an interface to forward L3 traffic with out configuring an address. This command creates the routing interface based on the auto generated IPv6 link-local address. This command can be used even if an address is configured on the interface. + +- Usage: + ``` + config interface ipv6 enable use-link-local-only + ``` + +- Example: + ``` + admin@sonic:~$ sudo config interface ipv6 enable use-link-local-only Vlan206 + admin@sonic:~$ sudo config interface ipv6 enable use-link-local-only PortChannel007 + admin@sonic:~$ sudo config interface ipv6 enable use-link-local-only Ethernet52 + ``` + +**config interface ipv6 disable use-link-local-only ** + +This command enables user to disable use-link-local-only configuration on an interface. + +- Usage: + ``` + config interface ipv6 disable use-link-local-only + ``` + +- Example: + ``` + admin@sonic:~$ sudo config interface ipv6 disable use-link-local-only Vlan206 + admin@sonic:~$ sudo config interface ipv6 disable use-link-local-only PortChannel007 + admin@sonic:~$ sudo config interface ipv6 disable use-link-local-only Ethernet52 + ``` + +**config ipv6 enable link-local** + +This command enables user to enable use-link-local-only command on all the interfaces globally. + +- Usage: + ``` + sudo config ipv6 enable link-local + ``` + +- Example: + ``` + admin@sonic:~$ sudo config ipv6 enable link-local + ``` + +**config ipv6 disable link-local** + +This command enables user to disable use-link-local-only command on all the interfaces globally. + +- Usage: + ``` + sudo config ipv6 disable link-local + ``` + +- Example: + ``` + admin@sonic:~$ sudo config ipv6 disable link-local + ``` + +### IPv6 Link Local show commands + +**show ipv6 link-local-mode** + +This command displays the link local mode of all the interfaces. + +- Usage: + ``` + show ipv6 link-local-mode + ``` + +- Example: + ``` + root@sonic:/home/admin# show ipv6 link-local-mode + +------------------+----------+ + | Interface Name | Mode | + +==================+==========+ + | Ethernet16 | Disabled | + +------------------+----------+ + | Ethernet18 | Enabled | + +------------------+----------+ + ``` + +Go Back To [Beginning of the document](#) or [Beginning of this section](#ipv6-link-local) ## Kubernetes diff --git a/show/main.py b/show/main.py index 024f48801031..7840e9c83c40 100755 --- a/show/main.py +++ b/show/main.py @@ -905,6 +905,35 @@ def protocol(verbose): from .bgp_frr_v6 import bgp ipv6.add_command(bgp) +# +# 'link-local-mode' subcommand ("show ipv6 link-local-mode") +# + +@ipv6.command('link-local-mode') +@click.option('--verbose', is_flag=True, help="Enable verbose output") +def link_local_mode(verbose): + """show ipv6 link-local-mode""" + header = ['Interface Name', 'Mode'] + body = [] + interfaces = ['INTERFACE', 'PORTCHANNEL_INTERFACE', 'VLAN_INTERFACE'] + config_db = ConfigDBConnector() + config_db.connect() + + for i in interfaces: + interface_dict = config_db.get_table(i) + link_local_data = {} + + if interface_dict: + for interface,value in interface_dict.items(): + if 'ipv6_use_link_local_only' in value: + link_local_data[interface] = interface_dict[interface]['ipv6_use_link_local_only'] + if link_local_data[interface] == 'enable': + body.append([interface, 'Enabled']) + else: + body.append([interface, 'Disabled']) + + click.echo(tabulate(body, header, tablefmt="grid")) + # # 'lldp' group ("show lldp ...") # diff --git a/tests/ipv6_link_local_test.py b/tests/ipv6_link_local_test.py new file mode 100644 index 000000000000..c7e614baccd2 --- /dev/null +++ b/tests/ipv6_link_local_test.py @@ -0,0 +1,153 @@ +import os + +from click.testing import CliRunner + +import config.main as config +import show.main as show +from utilities_common.db import Db + +show_ipv6_link_local_mode_output="""\ ++------------------+----------+ +| Interface Name | Mode | ++==================+==========+ +| Ethernet0 | Disabled | ++------------------+----------+ +| PortChannel0001 | Disabled | ++------------------+----------+ +""" + +class TestIPv6LinkLocal(object): + @classmethod + def setup_class(cls): + os.environ['UTILITIES_UNIT_TESTING'] = "1" + print("SETUP") + + def test_show_ipv6_link_local_mode(self): + runner = CliRunner() + db = Db() + obj = {'db':db.cfgdb} + + # show ipv6 link-local-mode output + result = runner.invoke(show.cli.commands["ipv6"].commands["link-local-mode"], [], obj=obj) + print(result.output) + assert result.output == show_ipv6_link_local_mode_output + + def test_config_enable_disable_ipv6_link_local_on_physical_interface(self): + runner = CliRunner() + db = Db() + obj = {'db':db.cfgdb} + + # Enable ipv6 link local on Ethernet0 + result = runner.invoke(config.config.commands["interface"].commands["ipv6"].commands["enable"].commands["use-link-local-only"], ["Ethernet0"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + assert result.output == '' + + # Disable ipv6 link local on Ethernet0 + result = runner.invoke(config.config.commands["interface"].commands["ipv6"].commands["disable"].commands["use-link-local-only"], ["Ethernet0"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + assert result.output == '' + + def test_config_enable_disable_ipv6_link_local_on_portchannel_interface(self): + runner = CliRunner() + db = Db() + obj = {'db':db.cfgdb} + + # Enable ipv6 link local on PortChannel0001 + result = runner.invoke(config.config.commands["interface"].commands["ipv6"].commands["enable"].commands["use-link-local-only"], ["PortChannel0001"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + assert result.output == '' + + # Disable ipv6 link local on PortChannel0001 + result = runner.invoke(config.config.commands["interface"].commands["ipv6"].commands["disable"].commands["use-link-local-only"], ["PortChannel0001"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + assert result.output == '' + + def test_config_enable_disable_ipv6_link_local_on_invalid_interface(self): + runner = CliRunner() + db = Db() + obj = {'db':db.cfgdb} + + # Enable ipv6 link local on PortChannel1 + result = runner.invoke(config.config.commands["interface"].commands["ipv6"].commands["enable"].commands["use-link-local-only"], ["PortChannel1"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code != 0 + assert 'Error: Interface name PortChannel1 is invalid. Please enter a valid interface name!!' in result.output + + # Disable ipv6 link local on Ethernet500 + result = runner.invoke(config.config.commands["interface"].commands["ipv6"].commands["disable"].commands["use-link-local-only"], ["Ethernet500"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code != 0 + assert 'Error: Interface name Ethernet500 is invalid. Please enter a valid interface name!!' in result.output + + def test_config_enable_disable_ipv6_link_local_on_interface_which_is_member_of_vlan(self): + runner = CliRunner() + db = Db() + obj = {'db':db.cfgdb} + + # Enable ipv6 link local on Ethernet16 + result = runner.invoke(config.config.commands["interface"].commands["ipv6"].commands["enable"].commands["use-link-local-only"], ["Ethernet16"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code != 0 + assert 'Error: Ethernet16 is configured as a member of vlan. Cannot configure the IPv6 link local mode!' in result.output + + # Disable ipv6 link local on Ethernet16 + result = runner.invoke(config.config.commands["interface"].commands["ipv6"].commands["disable"].commands["use-link-local-only"], ["Ethernet16"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code != 0 + assert 'Error: Ethernet16 is configured as a member of vlan. Cannot configure the IPv6 link local mode!' in result.output + + def test_config_enable_disable_ipv6_link_local_on_interface_which_is_member_of_portchannel(self): + runner = CliRunner() + db = Db() + obj = {'db':db.cfgdb} + + # Enable ipv6 link local on Ethernet32 + result = runner.invoke(config.config.commands["interface"].commands["ipv6"].commands["enable"].commands["use-link-local-only"], ["Ethernet32"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code != 0 + assert 'Error: Ethernet32 is configured as a member of portchannel. Cannot configure the IPv6 link local mode!' in result.output + + # Disable ipv6 link local on Ethernet32 + result = runner.invoke(config.config.commands["interface"].commands["ipv6"].commands["disable"].commands["use-link-local-only"], ["Ethernet32"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code != 0 + assert 'Error: Ethernet32 is configured as a member of portchannel. Cannot configure the IPv6 link local mode!' in result.output + + def test_config_enable_disable_ipv6_link_local_on_all_valid_interfaces(self): + runner = CliRunner() + db = Db() + obj = {'db':db.cfgdb} + + # Enable ipv6 link local on all interfaces + result = runner.invoke(config.config.commands["ipv6"].commands["enable"].commands["link-local"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + assert result.output == '' + + # Disable ipv6 link local on all interfaces + result = runner.invoke(config.config.commands["ipv6"].commands["disable"].commands["link-local"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + assert result.output == '' + + @classmethod + def teardown_class(cls): + os.environ['UTILITIES_UNIT_TESTING'] = "0" + print("TEARDOWN") + diff --git a/tests/mock_tables/config_db.json b/tests/mock_tables/config_db.json index 4c60cf992a2e..4fe3ecc36b05 100644 --- a/tests/mock_tables/config_db.json +++ b/tests/mock_tables/config_db.json @@ -622,7 +622,7 @@ "NULL": "NULL" }, "PORTCHANNEL_INTERFACE|PortChannel0001": { - "NULL": "NULL" + "ipv6_use_link_local_only": "disable" }, "PORTCHANNEL_INTERFACE|PortChannel0002": { "NULL": "NULL" @@ -658,7 +658,7 @@ "NULL": "NULL" }, "INTERFACE|Ethernet0": { - "NULL": "NULL" + "ipv6_use_link_local_only": "disable" }, "INTERFACE|Ethernet0|14.14.0.1/24": { "NULL": "NULL"