Skip to content

Commit ca728b8

Browse files
[config] fix interface IPv6 address removal. (#1819)
Current code takes an input from the user, converts the IPv6 string to lower letters and removes lower case IPv6 string from CONFIG DB. This is a bug since, according to the schema CONFIG DB is case insensitive for IPv6 address. - What I did Fixed CLI for removing IPv6 address. Issue is that below command does not work if IP address is written in upper case in CONFIG DB, like this: FC00::1/64. 'config interface ip remove Ethernet0 FC00::1/64' - How I did it Make it case insensitive Relaxed the validation of IP address, a built-in validation from ipaddress package in python is used. Refactored interface_ipaddr_dependent_on_interface -> get_interface_ipaddresses Separated some functions (has_static_routes_attached, flush_ip_neigh_in_kernel, can_remove_router_interface, remove_router_interface_ip_address, remove_router_interface, is_management_interface) - How to verify it Run UT. Try to reproduce the scenario described above. Signed-off-by: Stepan Blyschak <[email protected]>
1 parent 0665d6f commit ca728b8

File tree

2 files changed

+134
-136
lines changed

2 files changed

+134
-136
lines changed

config/main.py

+109-123
Original file line numberDiff line numberDiff line change
@@ -343,18 +343,22 @@ def interface_name_to_alias(config_db, interface_name):
343343

344344
return None
345345

346-
def interface_ipaddr_dependent_on_interface(config_db, interface_name):
347-
"""Get table keys including ipaddress
346+
def get_interface_ipaddresses(config_db, interface_name):
347+
"""Get IP addresses attached to interface
348348
"""
349-
data = []
349+
ipaddresses = set()
350350
table_name = get_interface_table_name(interface_name)
351-
if table_name == "":
352-
return data
351+
if not table_name:
352+
return ipaddresses
353+
353354
keys = config_db.get_keys(table_name)
354355
for key in keys:
355-
if interface_name in key and len(key) == 2:
356-
data.append(key)
357-
return data
356+
if isinstance(key, tuple) and len(key) == 2:
357+
iface, interface_ip = key
358+
if iface == interface_name:
359+
ipaddresses.add(ipaddress.ip_interface(interface_ip))
360+
361+
return ipaddresses
358362

359363
def is_interface_bind_to_vrf(config_db, interface_name):
360364
"""Get interface if bind to vrf or not
@@ -448,9 +452,9 @@ def del_interface_bind_to_vrf(config_db, vrf_name):
448452
if interface_dict:
449453
for interface_name in interface_dict:
450454
if 'vrf_name' in interface_dict[interface_name] and vrf_name == interface_dict[interface_name]['vrf_name']:
451-
interface_dependent = interface_ipaddr_dependent_on_interface(config_db, interface_name)
452-
for interface_del in interface_dependent:
453-
config_db.set_entry(table_name, interface_del, None)
455+
interface_ipaddresses = get_interface_ipaddresses(config_db, interface_name)
456+
for ipaddress in interface_ipaddresses:
457+
remove_router_interface_ip_address(config_db, interface_name, ipaddress)
454458
config_db.set_entry(table_name, interface_name, None)
455459

456460
def set_interface_naming_mode(mode):
@@ -854,27 +858,6 @@ def validate_mirror_session_config(config_db, session_name, dst_port, src_port,
854858

855859
return True
856860

857-
def is_valid_ip_interface(ctx, ip_addr):
858-
split_ip_mask = ip_addr.split("/")
859-
if len(split_ip_mask) < 2:
860-
return False
861-
862-
# Check if the IP address is correct or if there are leading zeros.
863-
ip_obj = ipaddress.ip_address(split_ip_mask[0])
864-
865-
if isinstance(ip_obj, ipaddress.IPv4Address):
866-
# Since the IP address is used as a part of a key in Redis DB,
867-
# do not tolerate extra zeros in IPv4.
868-
if str(ip_obj) != split_ip_mask[0]:
869-
return False
870-
871-
# Check if the mask is correct
872-
net = ipaddress.ip_network(ip_addr, strict=False)
873-
if str(net.prefixlen) != split_ip_mask[1] or net.prefixlen == 0:
874-
return False
875-
876-
return True
877-
878861
def cli_sroute_to_config(ctx, command_str, strict_nh = True):
879862
if len(command_str) < 2 or len(command_str) > 9:
880863
ctx.fail("argument is not in pattern prefix [vrf <vrf_name>] <A.B.C.D/M> nexthop <[vrf <vrf_name>] <A.B.C.D>>|<dev <dev_name>>!")
@@ -985,6 +968,20 @@ def cache_arp_entries():
985968
open(restore_flag_file, 'w').close()
986969
return success
987970

971+
def remove_router_interface_ip_address(config_db, interface_name, ipaddress_to_remove):
972+
table_name = get_interface_table_name(interface_name)
973+
keys = config_db.get_keys(table_name)
974+
975+
for key in keys:
976+
if not isinstance(key, tuple) or len(key) != 2:
977+
continue
978+
979+
iface, ipaddress_string = key
980+
if iface != interface_name:
981+
continue
982+
983+
if ipaddress.ip_interface(ipaddress_string) == ipaddress_to_remove:
984+
config_db.set_entry(table_name, (interface_name, ipaddress_string), None)
988985

989986
def validate_ipv4_address(ctx, param, ip_addr):
990987
"""Helper function to validate ipv4 address
@@ -3749,50 +3746,44 @@ def add(ctx, interface_name, ip_addr, gw):
37493746
return
37503747

37513748
try:
3752-
net = ipaddress.ip_network(ip_addr, strict=False)
3753-
if '/' not in ip_addr:
3754-
ip_addr += '/' + str(net.prefixlen)
3755-
3756-
if not is_valid_ip_interface(ctx, ip_addr):
3757-
raise ValueError('')
3758-
3759-
if interface_name == 'eth0':
3760-
3761-
# Configuring more than 1 IPv4 or more than 1 IPv6 address fails.
3762-
# Allow only one IPv4 and only one IPv6 address to be configured for IPv6.
3763-
# If a row already exist, overwrite it (by doing delete and add).
3764-
mgmtintf_key_list = _get_all_mgmtinterface_keys()
3765-
3766-
for key in mgmtintf_key_list:
3767-
# For loop runs for max 2 rows, once for IPv4 and once for IPv6.
3768-
# No need to capture the exception since the ip_addr is already validated earlier
3769-
ip_input = ipaddress.ip_interface(ip_addr)
3770-
current_ip = ipaddress.ip_interface(key[1])
3771-
if (ip_input.version == current_ip.version):
3772-
# If user has configured IPv4/v6 address and the already available row is also IPv4/v6, delete it here.
3773-
config_db.set_entry("MGMT_INTERFACE", ("eth0", key[1]), None)
3774-
3775-
# Set the new row with new value
3776-
if not gw:
3777-
config_db.set_entry("MGMT_INTERFACE", (interface_name, ip_addr), {"NULL": "NULL"})
3778-
else:
3779-
config_db.set_entry("MGMT_INTERFACE", (interface_name, ip_addr), {"gwaddr": gw})
3780-
mgmt_ip_restart_services()
3749+
ip_address = ipaddress.ip_interface(ip_addr)
3750+
except ValueError as err:
3751+
ctx.fail("IP address is not valid: {}".format(err))
3752+
3753+
if interface_name == 'eth0':
3754+
3755+
# Configuring more than 1 IPv4 or more than 1 IPv6 address fails.
3756+
# Allow only one IPv4 and only one IPv6 address to be configured for IPv6.
3757+
# If a row already exist, overwrite it (by doing delete and add).
3758+
mgmtintf_key_list = _get_all_mgmtinterface_keys()
3759+
3760+
for key in mgmtintf_key_list:
3761+
# For loop runs for max 2 rows, once for IPv4 and once for IPv6.
3762+
# No need to capture the exception since the ip_addr is already validated earlier
3763+
current_ip = ipaddress.ip_interface(key[1])
3764+
if (ip_address.version == current_ip.version):
3765+
# If user has configured IPv4/v6 address and the already available row is also IPv4/v6, delete it here.
3766+
config_db.set_entry("MGMT_INTERFACE", ("eth0", key[1]), None)
3767+
3768+
# Set the new row with new value
3769+
if not gw:
3770+
config_db.set_entry("MGMT_INTERFACE", (interface_name, str(ip_address)), {"NULL": "NULL"})
3771+
else:
3772+
config_db.set_entry("MGMT_INTERFACE", (interface_name, str(ip_address)), {"gwaddr": gw})
3773+
mgmt_ip_restart_services()
37813774

3782-
return
3775+
return
37833776

3784-
table_name = get_interface_table_name(interface_name)
3785-
if table_name == "":
3786-
ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan/Loopback]")
3787-
interface_entry = config_db.get_entry(table_name, interface_name)
3788-
if len(interface_entry) == 0:
3789-
if table_name == "VLAN_SUB_INTERFACE":
3790-
config_db.set_entry(table_name, interface_name, {"admin_status": "up"})
3791-
else:
3792-
config_db.set_entry(table_name, interface_name, {"NULL": "NULL"})
3793-
config_db.set_entry(table_name, (interface_name, ip_addr), {"NULL": "NULL"})
3794-
except ValueError:
3795-
ctx.fail("ip address or mask is not valid.")
3777+
table_name = get_interface_table_name(interface_name)
3778+
if table_name == "":
3779+
ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan/Loopback]")
3780+
interface_entry = config_db.get_entry(table_name, interface_name)
3781+
if len(interface_entry) == 0:
3782+
if table_name == "VLAN_SUB_INTERFACE":
3783+
config_db.set_entry(table_name, interface_name, {"admin_status": "up"})
3784+
else:
3785+
config_db.set_entry(table_name, interface_name, {"NULL": "NULL"})
3786+
config_db.set_entry(table_name, (interface_name, str(ip_address)), {"NULL": "NULL"})
37963787

37973788
#
37983789
# 'del' subcommand
@@ -3813,52 +3804,47 @@ def remove(ctx, interface_name, ip_addr):
38133804
ctx.fail("'interface_name' is None!")
38143805

38153806
try:
3816-
net = ipaddress.ip_network(ip_addr, strict=False)
3817-
if '/' not in ip_addr:
3818-
ip_addr += '/' + str(net.prefixlen)
3807+
ip_address = ipaddress.ip_interface(ip_addr)
3808+
except ValueError as err:
3809+
ctx.fail("IP address is not valid: {}".format(err))
38193810

3820-
if not is_valid_ip_interface(ctx, ip_addr):
3821-
raise ValueError('')
3811+
if interface_name == 'eth0':
3812+
config_db.set_entry("MGMT_INTERFACE", (interface_name, str(ip_address)), None)
3813+
mgmt_ip_restart_services()
3814+
return
38223815

3823-
if interface_name == 'eth0':
3824-
config_db.set_entry("MGMT_INTERFACE", (interface_name, ip_addr), None)
3825-
mgmt_ip_restart_services()
3826-
return
3816+
table_name = get_interface_table_name(interface_name)
3817+
if table_name == "":
3818+
ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan/Loopback]")
3819+
interface_addresses = get_interface_ipaddresses(config_db, interface_name)
3820+
# If we deleting the last IP entry of the interface, check whether a static route present for the RIF
3821+
# before deleting the entry and also the RIF.
3822+
if interface_addresses == {ip_address}:
3823+
# Check both IPv4 and IPv6 routes.
3824+
ip_versions = [ "ip", "ipv6"]
3825+
for ip_ver in ip_versions:
3826+
# Compete the command and ask Zebra to return the routes.
3827+
# Scopes of all VRFs will be checked.
3828+
cmd = "show {} route vrf all static".format(ip_ver)
3829+
if multi_asic.is_multi_asic():
3830+
output = bgp_util.run_bgp_command(cmd, ctx.obj['namespace'])
3831+
else:
3832+
output = bgp_util.run_bgp_command(cmd)
3833+
# If there is output data, check is there a static route,
3834+
# bound to the interface.
3835+
if output != "":
3836+
if any(interface_name in output_line for output_line in output.splitlines()):
3837+
ctx.fail("Cannot remove the last IP entry of interface {}. A static {} route is still bound to the RIF.".format(interface_name, ip_ver))
3838+
remove_router_interface_ip_address(config_db, interface_name, ip_address)
3839+
interface_addresses = get_interface_ipaddresses(config_db, interface_name)
3840+
if len(interface_addresses) == 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":
3841+
config_db.set_entry(table_name, interface_name, None)
38273842

3828-
table_name = get_interface_table_name(interface_name)
3829-
if table_name == "":
3830-
ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan/Loopback]")
3831-
interface_dependent = interface_ipaddr_dependent_on_interface(config_db, interface_name)
3832-
# If we deleting the last IP entry of the interface, check whether a static route present for the RIF
3833-
# before deleting the entry and also the RIF.
3834-
if len(interface_dependent) == 1 and interface_dependent[0][1] == ip_addr:
3835-
# Check both IPv4 and IPv6 routes.
3836-
ip_versions = [ "ip", "ipv6"]
3837-
for ip_ver in ip_versions:
3838-
# Compete the command and ask Zebra to return the routes.
3839-
# Scopes of all VRFs will be checked.
3840-
cmd = "show {} route vrf all static".format(ip_ver)
3841-
if multi_asic.is_multi_asic():
3842-
output = bgp_util.run_bgp_command(cmd, ctx.obj['namespace'])
3843-
else:
3844-
output = bgp_util.run_bgp_command(cmd)
3845-
# If there is output data, check is there a static route,
3846-
# bound to the interface.
3847-
if output != "":
3848-
if any(interface_name in output_line for output_line in output.splitlines()):
3849-
ctx.fail("Cannot remove the last IP entry of interface {}. A static {} route is still bound to the RIF.".format(interface_name, ip_ver))
3850-
config_db.set_entry(table_name, (interface_name, ip_addr), None)
3851-
interface_dependent = interface_ipaddr_dependent_on_interface(config_db, interface_name)
3852-
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":
3853-
config_db.set_entry(table_name, interface_name, None)
3854-
3855-
if multi_asic.is_multi_asic():
3856-
command = "sudo ip netns exec {} ip neigh flush dev {} {}".format(ctx.obj['namespace'], interface_name, ip_addr)
3857-
else:
3858-
command = "ip neigh flush dev {} {}".format(interface_name, ip_addr)
3859-
clicommon.run_command(command)
3860-
except ValueError:
3861-
ctx.fail("ip address or mask is not valid.")
3843+
if multi_asic.is_multi_asic():
3844+
command = "sudo ip netns exec {} ip neigh flush dev {} {}".format(ctx.obj['namespace'], interface_name, str(ip_address))
3845+
else:
3846+
command = "ip neigh flush dev {} {}".format(interface_name, str(ip_address))
3847+
clicommon.run_command(command)
38623848

38633849

38643850
#
@@ -4243,9 +4229,9 @@ def bind(ctx, interface_name, vrf_name):
42434229
config_db.get_entry(table_name, interface_name).get('vrf_name') == vrf_name:
42444230
return
42454231
# Clean ip addresses if interface configured
4246-
interface_dependent = interface_ipaddr_dependent_on_interface(config_db, interface_name)
4247-
for interface_del in interface_dependent:
4248-
config_db.set_entry(table_name, interface_del, None)
4232+
interface_addresses = get_interface_ipaddresses(config_db, interface_name)
4233+
for ipaddress in interface_addresses:
4234+
remove_router_interface_ip_address(config_db, interface_name, ipaddress)
42494235
config_db.set_entry(table_name, interface_name, None)
42504236
# When config_db del entry and then add entry with same key, the DEL will lost.
42514237
if ctx.obj['namespace'] is DEFAULT_NAMESPACE:
@@ -4281,9 +4267,9 @@ def unbind(ctx, interface_name):
42814267
ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan/Loopback]")
42824268
if is_interface_bind_to_vrf(config_db, interface_name) is False:
42834269
return
4284-
interface_dependent = interface_ipaddr_dependent_on_interface(config_db, interface_name)
4285-
for interface_del in interface_dependent:
4286-
config_db.set_entry(table_name, interface_del, None)
4270+
interface_ipaddresses = get_interface_ipaddresses(config_db, interface_name)
4271+
for ipaddress in interface_ipaddresses:
4272+
remove_router_interface_ip_address(config_db, interface_name, ipaddress)
42874273
config_db.set_entry(table_name, interface_name, None)
42884274

42894275

tests/ip_config_test.py

+25-13
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@
88
import show.main as show
99
from utilities_common.db import Db
1010

11-
ERROR_MSG = '''
12-
Error: ip address or mask is not valid.
13-
'''
11+
ERROR_MSG = "Error: IP address is not valid"
1412

1513
class TestConfigIP(object):
1614
@classmethod
@@ -65,7 +63,7 @@ def test_add_interface_ipv4_with_leading_zeros(self):
6563
obj = {'config_db':db.cfgdb}
6664

6765
# config int ip add Ethernet68 10.10.10.002/24
68-
result = runner.invoke(config.config.commands["interface"].commands["ip"].commands["add"], ["Ethernet68", "10.10.10.002/24"], obj=obj)
66+
result = runner.invoke(config.config.commands["interface"].commands["ip"].commands["add"], ["Ethernet68", "10.10.10.0002/24"], obj=obj)
6967
print(result.exit_code, result.output)
7068
assert result.exit_code != 0
7169
assert ERROR_MSG in result.output
@@ -88,7 +86,21 @@ def test_add_del_interface_valid_ipv6(self):
8886
print(result.exit_code, result.output)
8987
assert result.exit_code != 0
9088
assert ('Ethernet72', '2001:1db8:11a3:19d7:1f34:8a2e:17a0:765d/34') not in db.cfgdb.get_table('INTERFACE')
91-
89+
90+
def test_del_interface_case_sensitive_ipv6(self):
91+
db = Db()
92+
runner = CliRunner()
93+
obj = {'config_db':db.cfgdb}
94+
95+
obj['config_db'].set_entry('INTERFACE', ('Ethernet72', 'FC00::1/126'), {})
96+
assert ('Ethernet72', 'FC00::1/126') in db.cfgdb.get_table('INTERFACE')
97+
98+
# config int ip remove Ethernet72 FC00::1/126
99+
result = runner.invoke(config.config.commands["interface"].commands["ip"].commands["remove"], ["Ethernet72", "FC00::1/126"], obj=obj)
100+
print(result.exit_code, result.output)
101+
assert result.exit_code != 0
102+
assert ('Ethernet72', 'FC00::1/126') not in db.cfgdb.get_table('INTERFACE')
103+
92104
def test_add_interface_invalid_ipv6(self):
93105
db = Db()
94106
runner = CliRunner()
@@ -120,14 +132,14 @@ def test_add_del_interface_ipv6_with_leading_zeros(self):
120132
result = runner.invoke(config.config.commands["interface"].commands["ip"].commands["add"], ["Ethernet68", "2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d/34"], obj=obj)
121133
print(result.exit_code, result.output)
122134
assert result.exit_code == 0
123-
assert ('Ethernet68', '2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d/34') in db.cfgdb.get_table('INTERFACE')
124-
135+
assert ('Ethernet68', '2001:db8:11a3:9d7:1f34:8a2e:7a0:765d/34') in db.cfgdb.get_table('INTERFACE')
136+
125137
# config int ip remove Ethernet68 2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d/34
126138
result = runner.invoke(config.config.commands["interface"].commands["ip"].commands["remove"], ["Ethernet68", "2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d/34"], obj=obj)
127139
print(result.exit_code, result.output)
128140
assert result.exit_code != 0
129-
assert ('Ethernet68', '2001:0db8:11a3:09d7:1f34:8a2e:07a0:765d/34') not in db.cfgdb.get_table('INTERFACE')
130-
141+
assert ('Ethernet68', '2001:db8:11a3:9d7:1f34:8a2e:7a0:765d/34') not in db.cfgdb.get_table('INTERFACE')
142+
131143
def test_add_del_interface_shortened_ipv6_with_leading_zeros(self):
132144
db = Db()
133145
runner = CliRunner()
@@ -137,14 +149,14 @@ def test_add_del_interface_shortened_ipv6_with_leading_zeros(self):
137149
result = runner.invoke(config.config.commands["interface"].commands["ip"].commands["add"], ["Ethernet68", "3000::001/64"], obj=obj)
138150
print(result.exit_code, result.output)
139151
assert result.exit_code == 0
140-
assert ('Ethernet68', '3000::001/64') in db.cfgdb.get_table('INTERFACE')
141-
152+
assert ('Ethernet68', '3000::1/64') in db.cfgdb.get_table('INTERFACE')
153+
142154
# config int ip remove Ethernet68 3000::001/64
143155
result = runner.invoke(config.config.commands["interface"].commands["ip"].commands["remove"], ["Ethernet68", "3000::001/64"], obj=obj)
144156
print(result.exit_code, result.output)
145157
assert result.exit_code != 0
146-
assert ('Ethernet68', '3000::001/64') not in db.cfgdb.get_table('INTERFACE')
147-
158+
assert ('Ethernet68', '3000::1/64') not in db.cfgdb.get_table('INTERFACE')
159+
148160
@classmethod
149161
def teardown_class(cls):
150162
os.environ['UTILITIES_UNIT_TESTING'] = "0"

0 commit comments

Comments
 (0)