Skip to content

Commit c3963c5

Browse files
author
maksymbelei95
authored
Fix remove ip rif (#1535)
*Added checking of static routes, related to the interface, before deleting of the last IP entry to prevent deleting the RIF if a static route is present in the system. Signed-off-by: Maksym Belei <[email protected]>
1 parent 41d8ddc commit c3963c5

File tree

7 files changed

+288
-1
lines changed

7 files changed

+288
-1
lines changed

config/main.py

+20
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from swsscommon.swsscommon import SonicV2Connector, ConfigDBConnector, SonicDBConfig
2323
from utilities_common.db import Db
2424
from utilities_common.intf_filter import parse_interface_in_filter
25+
from utilities_common import bgp_util
2526
import utilities_common.cli as clicommon
2627
from .utils import log
2728

@@ -2787,6 +2788,25 @@ def remove(ctx, interface_name, ip_addr):
27872788
table_name = get_interface_table_name(interface_name)
27882789
if table_name == "":
27892790
ctx.fail("'interface_name' is not valid. Valid names [Ethernet/PortChannel/Vlan/Loopback]")
2791+
interface_dependent = interface_ipaddr_dependent_on_interface(config_db, interface_name)
2792+
# If we deleting the last IP entry of the interface, check whether a static route present for the RIF
2793+
# before deleting the entry and also the RIF.
2794+
if len(interface_dependent) == 1 and interface_dependent[0][1] == ip_addr:
2795+
# Check both IPv4 and IPv6 routes.
2796+
ip_versions = [ "ip", "ipv6"]
2797+
for ip_ver in ip_versions:
2798+
# Compete the command and ask Zebra to return the routes.
2799+
# Scopes of all VRFs will be checked.
2800+
cmd = "show {} route vrf all static".format(ip_ver)
2801+
if multi_asic.is_multi_asic():
2802+
output = bgp_util.run_bgp_command(cmd, ctx.obj['namespace'])
2803+
else:
2804+
output = bgp_util.run_bgp_command(cmd)
2805+
# If there is output data, check is there a static route,
2806+
# bound to the interface.
2807+
if output != "":
2808+
if any(interface_name in output_line for output_line in output.splitlines()):
2809+
ctx.fail("Cannot remove the last IP entry of interface {}. A static {} route is still bound to the RIF.".format(interface_name, ip_ver))
27902810
config_db.set_entry(table_name, (interface_name, ip_addr), None)
27912811
interface_dependent = interface_ipaddr_dependent_on_interface(config_db, interface_name)
27922812
if len(interface_dependent) == 0 and is_interface_bind_to_vrf(config_db, interface_name) is False:

tests/config_int_ip_common.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
show_ip_route_with_static_expected_output = """\
2+
Codes: K - kernel route, C - connected, S - static, R - RIP,
3+
O - OSPF, I - IS-IS, B - BGP, E - EIGRP, N - NHRP,
4+
T - Table, v - VNC, V - VNC-Direct, A - Babel, D - SHARP,
5+
F - PBR, f - OpenFabric,
6+
> - selected route, * - FIB route, q - queued, r - rejected, b - backup
7+
8+
VRF Vrf11:
9+
S>* 20.0.0.1/32 [1/0] is directly connected, Ethernet2, weight 1, 00:40:18
10+
11+
VRF default:
12+
S>* 0.0.0.0/0 [200/0] via 192.168.111.3, eth0, weight 1, 19:51:57
13+
S>* 20.0.0.1/32 [1/0] is directly connected, Ethernet4 (vrf Vrf11), weight 1, 00:38:52
14+
S>* 20.0.0.4/32 [1/0] is directly connected, PortChannel2, weight 1, 00:38:52
15+
S>* 20.0.0.8/32 [1/0] is directly connected, Vlan2, weight 1, 00:38:52
16+
"""
17+
18+
show_ipv6_route_with_static_expected_output = """\
19+
Codes: K - kernel route, C - connected, S - static, R - RIPng,
20+
O - OSPFv3, I - IS-IS, B - BGP, N - NHRP, T - Table,
21+
v - VNC, V - VNC-Direct, A - Babel, D - SHARP, F - PBR,
22+
f - OpenFabric,
23+
> - selected route, * - FIB route, q - queued, r - rejected, b - backup
24+
25+
VRF Vrf11:
26+
S>* fe80::/24 [1/0] is directly connected, Vlan4, weight 1, 00:00:04
27+
28+
VRF default:
29+
S>* 20c0:a800:0:21::/64 [20/0] is directly connected, PortChannel4, 2d22h02m
30+
S>* fe80::/32 [1/0] is directly connected, Ethernet8 (vrf Vrf11), weight 1, 00:00:04
31+
"""

tests/config_int_ip_test.py

+158
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import os
2+
import sys
3+
import pytest
4+
import mock
5+
from importlib import reload
6+
7+
from click.testing import CliRunner
8+
9+
from utilities_common.db import Db
10+
11+
modules_path = os.path.join(os.path.dirname(__file__), "..")
12+
test_path = os.path.join(modules_path, "tests")
13+
sys.path.insert(0, modules_path)
14+
sys.path.insert(0, test_path)
15+
mock_db_path = os.path.join(test_path, "int_ip_input")
16+
17+
18+
class TestIntIp(object):
19+
@pytest.fixture(scope="class", autouse=True)
20+
def setup_class(cls):
21+
print("SETUP")
22+
os.environ['UTILITIES_UNIT_TESTING'] = "1"
23+
import config.main as config
24+
reload(config)
25+
yield
26+
print("TEARDOWN")
27+
os.environ["UTILITIES_UNIT_TESTING"] = "0"
28+
from .mock_tables import dbconnector
29+
dbconnector.dedicated_dbs = {}
30+
31+
@pytest.mark.parametrize('setup_single_bgp_instance',
32+
['ip_route_for_int_ip'], indirect=['setup_single_bgp_instance'])
33+
def test_config_int_ip_rem(
34+
self,
35+
get_cmd_module,
36+
setup_single_bgp_instance):
37+
(config, show) = get_cmd_module
38+
jsonfile_config = os.path.join(mock_db_path, "config_db.json")
39+
from .mock_tables import dbconnector
40+
dbconnector.dedicated_dbs['CONFIG_DB'] = jsonfile_config
41+
42+
runner = CliRunner()
43+
db = Db()
44+
obj = {'config_db': db.cfgdb}
45+
46+
# remove vlan IP`s
47+
with mock.patch('utilities_common.cli.run_command') as mock_run_command:
48+
result = runner.invoke(config.config.commands["interface"].commands["ip"].commands["remove"],
49+
["Ethernet16", "192.168.10.1/24"], obj=obj)
50+
print(result.exit_code, result.output)
51+
assert result.exit_code == 0
52+
assert mock_run_command.call_count == 1
53+
54+
@pytest.mark.parametrize('setup_single_bgp_instance',
55+
['ip_route_for_int_ip'], indirect=['setup_single_bgp_instance'])
56+
def test_config_int_ip_rem_static(
57+
self,
58+
get_cmd_module,
59+
setup_single_bgp_instance):
60+
(config, show) = get_cmd_module
61+
jsonfile_config = os.path.join(mock_db_path, "config_db")
62+
from .mock_tables import dbconnector
63+
dbconnector.dedicated_dbs['CONFIG_DB'] = jsonfile_config
64+
65+
runner = CliRunner()
66+
db = Db()
67+
obj = {'config_db': db.cfgdb}
68+
69+
with mock.patch('utilities_common.cli.run_command') as mock_run_command:
70+
result = runner.invoke(config.config.commands["interface"].commands["ip"].commands["remove"],
71+
["Ethernet2", "192.168.0.1/24"], obj=obj)
72+
print(result.exit_code, result.output)
73+
assert result.exit_code != 0
74+
assert "Error: Cannot remove the last IP entry of interface Ethernet2. A static ip route is still bound to the RIF." in result.output
75+
assert mock_run_command.call_count == 0
76+
77+
with mock.patch('utilities_common.cli.run_command') as mock_run_command:
78+
result = runner.invoke(config.config.commands["interface"].commands["ip"].commands["remove"],
79+
["Ethernet8", "192.168.3.1/24"], obj=obj)
80+
print(result.exit_code, result.output)
81+
assert result.exit_code != 0
82+
assert "Error: Cannot remove the last IP entry of interface Ethernet8. A static ipv6 route is still bound to the RIF." in result.output
83+
assert mock_run_command.call_count == 0
84+
85+
with mock.patch('utilities_common.cli.run_command') as mock_run_command:
86+
result = runner.invoke(config.config.commands["interface"].commands["ip"].commands["remove"],
87+
["Vlan2", "192.168.1.1/21"], obj=obj)
88+
print(result.exit_code, result.output)
89+
assert result.exit_code != 0
90+
assert "Error: Cannot remove the last IP entry of interface Vlan2. A static ip route is still bound to the RIF." in result.output
91+
assert mock_run_command.call_count == 0
92+
93+
with mock.patch('utilities_common.cli.run_command') as mock_run_command:
94+
result = runner.invoke(config.config.commands["interface"].commands["ip"].commands["remove"],
95+
["PortChannel2", "10.0.0.56/31"], obj=obj)
96+
print(result.exit_code, result.output)
97+
assert result.exit_code != 0
98+
assert "Error: Cannot remove the last IP entry of interface PortChannel2. A static ip route is still bound to the RIF." in result.output
99+
assert mock_run_command.call_count == 0
100+
101+
with mock.patch('utilities_common.cli.run_command') as mock_run_command:
102+
result = runner.invoke(config.config.commands["interface"].commands["ip"].commands["remove"],
103+
["Ethernet4", "192.168.4.1/24"], obj=obj)
104+
print(result.exit_code, result.output)
105+
assert result.exit_code == 0
106+
assert mock_run_command.call_count == 1
107+
108+
class TestIntIpMultiasic(object):
109+
@pytest.fixture(scope="class", autouse=True)
110+
def setup_class(cls):
111+
print("SETUP")
112+
os.environ['UTILITIES_UNIT_TESTING'] = "1"
113+
os.environ["UTILITIES_UNIT_TESTING_TOPOLOGY"] = "multi_asic"
114+
from .mock_tables import dbconnector
115+
from .mock_tables import mock_multi_asic
116+
reload(mock_multi_asic)
117+
dbconnector.load_namespace_config()
118+
yield
119+
print("TEARDOWN")
120+
os.environ["UTILITIES_UNIT_TESTING"] = "0"
121+
os.environ["UTILITIES_UNIT_TESTING_TOPOLOGY"] = ""
122+
# change back to single asic config
123+
from .mock_tables import dbconnector
124+
from .mock_tables import mock_single_asic
125+
reload(mock_single_asic)
126+
dbconnector.dedicated_dbs = {}
127+
dbconnector.load_namespace_config()
128+
129+
@pytest.mark.parametrize('setup_multi_asic_bgp_instance',
130+
['ip_route_for_int_ip'], indirect=['setup_multi_asic_bgp_instance'])
131+
def test_config_int_ip_rem_static_multiasic(
132+
self,
133+
get_cmd_module,
134+
setup_multi_asic_bgp_instance):
135+
(config, show) = get_cmd_module
136+
jsonfile_config = os.path.join(mock_db_path, "config_db")
137+
from .mock_tables import dbconnector
138+
dbconnector.dedicated_dbs['CONFIG_DB'] = jsonfile_config
139+
140+
runner = CliRunner()
141+
db = Db()
142+
obj = {'config_db': db.cfgdb, 'namespace': 'test_ns'}
143+
144+
with mock.patch('utilities_common.cli.run_command') as mock_run_command:
145+
result = runner.invoke(config.config.commands["interface"].commands["ip"].commands["remove"],
146+
["Ethernet2", "192.168.0.1/24"], obj=obj)
147+
print(result.exit_code, result.output)
148+
assert result.exit_code != 0
149+
assert "Error: Cannot remove the last IP entry of interface Ethernet2. A static ip route is still bound to the RIF." in result.output
150+
assert mock_run_command.call_count == 0
151+
152+
with mock.patch('utilities_common.cli.run_command') as mock_run_command:
153+
result = runner.invoke(config.config.commands["interface"].commands["ip"].commands["remove"],
154+
["Ethernet8", "192.168.3.1/24"], obj=obj)
155+
print(result.exit_code, result.output)
156+
assert result.exit_code != 0
157+
assert "Error: Cannot remove the last IP entry of interface Ethernet8. A static ipv6 route is still bound to the RIF." in result.output
158+
assert mock_run_command.call_count == 0

tests/conftest.py

+31-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from .mock_tables import dbconnector
1111
from . import show_ip_route_common
12+
from . import config_int_ip_common
1213

1314
test_path = os.path.dirname(os.path.abspath(__file__))
1415
modules_path = os.path.dirname(test_path)
@@ -124,6 +125,14 @@ def mock_run_bgp_command(vtysh_cmd, bgp_namespace):
124125
mock_frr_data = json_data.read()
125126
return mock_frr_data
126127
return ""
128+
129+
def mock_run_bgp_command_for_static(vtysh_cmd, bgp_namespace=""):
130+
if vtysh_cmd == "show ip route vrf all static":
131+
return config_int_ip_common.show_ip_route_with_static_expected_output
132+
elif vtysh_cmd == "show ipv6 route vrf all static":
133+
return config_int_ip_common.show_ipv6_route_with_static_expected_output
134+
else:
135+
return ""
127136

128137
def mock_run_show_ip_route_commands(request):
129138
if request.param == 'ipv6_route_err':
@@ -147,10 +156,18 @@ def mock_run_show_ip_route_commands(request):
147156
request.param == 'ipv6_route', request.param == 'ipv6_specific_route']):
148157
bgp_util.run_bgp_command = mock.MagicMock(
149158
return_value=mock_run_show_ip_route_commands(request))
159+
elif request.param == 'ip_route_for_int_ip':
160+
_old_run_bgp_command = bgp_util.run_bgp_command
161+
bgp_util.run_bgp_command = mock_run_bgp_command_for_static
150162
else:
151163
bgp_util.run_bgp_command = mock.MagicMock(
152164
return_value=mock_run_bgp_command("", ""))
153165

166+
yield
167+
168+
if request.param == 'ip_route_for_int_ip':
169+
bgp_util.run_bgp_command = _old_run_bgp_command
170+
154171

155172
@pytest.fixture
156173
def setup_multi_asic_bgp_instance(request):
@@ -178,6 +195,16 @@ def setup_multi_asic_bgp_instance(request):
178195
m_asic_json_file = os.path.join(
179196
test_path, 'mock_tables', 'dummy.json')
180197

198+
def mock_run_bgp_command_for_static(vtysh_cmd, bgp_namespace=""):
199+
if bgp_namespace != 'test_ns':
200+
return ""
201+
if vtysh_cmd == "show ip route vrf all static":
202+
return config_int_ip_common.show_ip_route_with_static_expected_output
203+
elif vtysh_cmd == "show ipv6 route vrf all static":
204+
return config_int_ip_common.show_ipv6_route_with_static_expected_output
205+
else:
206+
return ""
207+
181208
def mock_run_bgp_command(vtysh_cmd, bgp_namespace):
182209
bgp_mocked_json = os.path.join(
183210
test_path, 'mock_tables', bgp_namespace, m_asic_json_file)
@@ -189,7 +216,10 @@ def mock_run_bgp_command(vtysh_cmd, bgp_namespace):
189216
return ""
190217

191218
_old_run_bgp_command = bgp_util.run_bgp_command
192-
bgp_util.run_bgp_command = mock_run_bgp_command
219+
if request.param == 'ip_route_for_int_ip':
220+
bgp_util.run_bgp_command = mock_run_bgp_command_for_static
221+
else:
222+
bgp_util.run_bgp_command = mock_run_bgp_command
193223

194224
yield
195225

tests/crm_test.py

+1
Original file line numberDiff line numberDiff line change
@@ -1216,6 +1216,7 @@ def setup_class(cls):
12161216
os.environ["UTILITIES_UNIT_TESTING_TOPOLOGY"] = "multi_asic"
12171217
from .mock_tables import dbconnector
12181218
from .mock_tables import mock_multi_asic
1219+
importlib.reload(mock_multi_asic)
12191220
dbconnector.load_namespace_config()
12201221

12211222
def test_crm_show_summary(self):

tests/int_ip_input/config_db.json

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"INTERFACE|Ethernet16": {
3+
"NULL": "NULL"
4+
},
5+
"INTERFACE|Ethernet16|192.168.10.1/24": {
6+
"NULL": "NULL"
7+
},
8+
"INTERFACE|Ethernet2": {
9+
"NULL": "NULL"
10+
},
11+
"INTERFACE|Ethernet2|192.168.0.1/24": {
12+
"NULL": "NULL"
13+
},
14+
"INTERFACE|Ethernet4": {
15+
"NULL": "NULL"
16+
},
17+
"INTERFACE|Ethernet4|192.168.4.1/24": {
18+
"NULL": "NULL"
19+
},
20+
"INTERFACE|Ethernet4|192.168.100.1/24": {
21+
"NULL": "NULL"
22+
},
23+
"INTERFACE|Ethernet8": {
24+
"NULL": "NULL"
25+
},
26+
"INTERFACE|Ethernet8|192.168.3.1/24": {
27+
"NULL": "NULL"
28+
},
29+
"PORTCHANNEL_INTERFACE|PortChannel2": {
30+
"NULL": "NULL"
31+
},
32+
"PORTCHANNEL_INTERFACE|PortChannel2|10.0.0.56/31": {
33+
"NULL": "NULL"
34+
},
35+
"VLAN_INTERFACE|Vlan2": {
36+
"proxy_arp": "enabled"
37+
},
38+
"VLAN_INTERFACE|Vlan2|192.168.1.1/21": {
39+
"NULL": "NULL"
40+
}
41+
}

tests/vlan_test.py

+6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import config.main as config
88
import show.main as show
99
from utilities_common.db import Db
10+
from importlib import reload
1011

1112
show_vlan_brief_output="""\
1213
+-----------+-----------------+-----------------+----------------+-----------------------+-------------+
@@ -188,6 +189,11 @@ class TestVlan(object):
188189
@classmethod
189190
def setup_class(cls):
190191
os.environ['UTILITIES_UNIT_TESTING'] = "1"
192+
# ensure that we are working with single asic config
193+
from .mock_tables import dbconnector
194+
from .mock_tables import mock_single_asic
195+
reload(mock_single_asic)
196+
dbconnector.load_namespace_config()
191197
print("SETUP")
192198

193199
def test_show_vlan(self):

0 commit comments

Comments
 (0)