Skip to content

Commit fec2687

Browse files
committed
[dhcp_relay]: sonic dhcp relay agent for IPv4
Changes for SONiC DHCPv4 Relay feature
1 parent 036e4e6 commit fec2687

File tree

6 files changed

+143
-0
lines changed

6 files changed

+143
-0
lines changed

config/main.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1514,6 +1514,23 @@ def config_file_yang_validation(filename):
15141514
return True
15151515

15161516

1517+
def check_dhcpv4_relay_dependencies(db, object_name, object_type):
1518+
"""Checks if to be deleted interface/VRF is used in DHCPV4_RELAY table."""
1519+
for vlan, data in db.get_table('DHCPV4_RELAY').items():
1520+
if object_type == 'interface':
1521+
1522+
source_intf = data.get('source_interface')
1523+
if source_intf == object_name:
1524+
raise ValueError(f"Interface '{object_name}' is in use by {vlan}")
1525+
1526+
elif object_type == 'vrf':
1527+
server_vrf = data.get('server_vrf')
1528+
if server_vrf == object_name:
1529+
raise ValueError(f"VRF '{object_name}' is in use for dhcp_relay configurations for {vlan}")
1530+
else:
1531+
raise ValueError("Unsupported object_type: {}".format(object_type))
1532+
1533+
15171534
# This is our main entrypoint - the main 'config' command
15181535
@click.group(cls=clicommon.AbbreviationGroup, context_settings=CONTEXT_SETTINGS)
15191536
@click.pass_context
@@ -2674,6 +2691,12 @@ def remove_portchannel(ctx, portchannel_name):
26742691
if len([(k, v) for k, v in db.get_table('PORTCHANNEL_MEMBER') if k == portchannel_name]) != 0: # TODO: MISSING CONSTRAINT IN YANG MODEL
26752692
ctx.fail("Error: Portchannel {} contains members. Remove members before deleting Portchannel!".format(portchannel_name))
26762693

2694+
# Dont proceed if the port channel is used in dhcpv4_relay
2695+
try:
2696+
check_dhcpv4_relay_dependencies(db, portchannel_name, 'interface')
2697+
except ValueError as e:
2698+
ctx.fail(str(e))
2699+
26772700
try:
26782701
db.set_entry('PORTCHANNEL', portchannel_name, None)
26792702
except JsonPatchConflict:
@@ -7205,6 +7228,12 @@ def del_vrf(ctx, vrf_name):
72057228
syslog_vrf = syslog_data.get("vrf")
72067229
if syslog_vrf == syslog_vrf_dev:
72077230
ctx.fail("Failed to remove VRF device: {} is in use by SYSLOG_SERVER|{}".format(syslog_vrf, syslog_entry))
7231+
# Dont proceed if the vrf is used in dhcpv4_relay
7232+
try:
7233+
check_dhcpv4_relay_dependencies(config_db, vrf_name, 'vrf')
7234+
except ValueError as e:
7235+
ctx.fail(str(e))
7236+
72087237
if not is_vrf_exists(config_db, vrf_name):
72097238
ctx.fail("VRF {} does not exist!".format(vrf_name))
72107239
elif (vrf_name == 'mgmt' or vrf_name == 'management'):
@@ -8309,6 +8338,12 @@ def del_loopback(ctx, loopback_name):
83098338
if loopback_name not in lo_intfs:
83108339
ctx.fail("{} does not exist".format(loopback_name))
83118340

8341+
# Dont proceed if the loopback is used in dhcpv4_relay
8342+
try:
8343+
check_dhcpv4_relay_dependencies(config_db, loopback_name, 'interface')
8344+
except ValueError as e:
8345+
ctx.fail(str(e))
8346+
83128347
ips = [ k[1] for k in lo_config_db if type(k) == tuple and k[0] == loopback_name ]
83138348
for ip in ips:
83148349
config_db.set_entry('LOOPBACK_INTERFACE', (loopback_name, ip), None)

config/vlan.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@ def del_vlan(db, vid, multiple, no_restart_dhcp_relay):
184184
"First remove vxlan mapping '{}' assigned to VLAN".format(
185185
vid, '|'.join(vxmap_key)))
186186

187+
if vlan in db.cfgdb.get_table('DHCPV4_RELAY'):
188+
ctx.fail(f"{vlan} cannot be removed as it is being used in DHCPV4_RELAY table.")
187189
# set dhcpv4_relay table
188190
set_dhcp_relay_table('VLAN', config_db, vlan, None)
189191

tests/config_test.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3078,6 +3078,31 @@ def test_del_nonexistent_loopback_adhoc_validation(self):
30783078
assert result.exit_code != 0
30793079
assert "Loopbax1 is invalid, name should have prefix 'Loopback' and suffix '<0-999>'" in result.output
30803080

3081+
def test_del_loopback_with_dhcpv4_relay_entry(self):
3082+
config.ADHOC_VALIDATION = True
3083+
runner = CliRunner()
3084+
db = Db()
3085+
obj = {'db': db.cfgdb}
3086+
3087+
result = runner.invoke(config.config.commands["loopback"].commands["add"], ["Loopback1"], obj=obj)
3088+
print(result.exit_code)
3089+
print(result.output)
3090+
assert result.exit_code == 0
3091+
assert db.cfgdb.get_entry("LOOPBACK_INTERFACE", "Loopback1") == {}
3092+
3093+
db.cfgdb.set_entry("DHCPV4_RELAY", "Vlan100", {
3094+
"dhcpv4_servers": ["192.0.2.100"],
3095+
"source_interface": "Loopback1"
3096+
})
3097+
3098+
result = runner.invoke(config.config.commands["loopback"].commands["del"], ["Loopback1"], obj=obj)
3099+
print(result.exit_code)
3100+
print(result.output)
3101+
assert result.exit_code != 0
3102+
assert "Error: Interface 'Loopback1' is in use by Vlan100" in result.output
3103+
3104+
db.cfgdb.set_entry("DHCPV4_RELAY", "Vlan200", None)
3105+
30813106
@patch("config.validated_config_db_connector.ValidatedConfigDBConnector.validated_set_entry", mock.Mock(return_value=True))
30823107
@patch("validated_config_db_connector.device_info.is_yang_config_validation_enabled", mock.Mock(return_value=True))
30833108
def test_add_loopback_yang_validation(self):

tests/portchannel_test.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,25 @@ def test_delete_portchannel_with_invalid_name_adhoc_validation(self):
8484
assert result.exit_code != 0
8585
assert "Error: PortChan005 is invalid!, name should have prefix 'PortChannel' and suffix '<0-9999>'" in result.output
8686

87+
def test_delete_portchannel_in_use_by_dhcpv4_relay(self):
88+
config.ADHOC_VALIDATION = True
89+
runner = CliRunner()
90+
db = Db()
91+
obj = {'db': db.cfgdb}
92+
93+
result = runner.invoke(config.config.commands["portchannel"].commands["add"], ["PortChannel10"], obj=obj)
94+
95+
db.cfgdb.set_entry("DHCPV4_RELAY", "Vlan100", {"source_interface": "PortChannel10"})
96+
97+
result = runner.invoke(config.config.commands["portchannel"].commands["del"], ["PortChannel10"], obj=obj)
98+
print(result.exit_code)
99+
print(result.output)
100+
assert result.exit_code != 0
101+
assert f"Interface 'PortChannel10' is in use by Vlan100" in result.output
102+
103+
db.cfgdb.set_entry("DHCPV4_RELAY", "Vlan100", None)
104+
105+
87106
def test_add_existing_portchannel_again(self):
88107
runner = CliRunner()
89108
db = Db()

tests/vlan_test.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1552,6 +1552,42 @@ def test_config_add_del_vlan_dhcp_relay_with_non_empty_entry(self, ip_version, m
15521552
mock_handle_restart.assert_called_once()
15531553
assert "Restart service dhcp_relay failed with error" not in result.output
15541554

1555+
def test_config_add_del_vlan_dhcpv4_relay_with_non_empty_entry(self, mock_restart_dhcp_relay_service):
1556+
runner = CliRunner()
1557+
db = Db()
1558+
1559+
result = runner.invoke(config.config.commands["vlan"].commands["add"], ["999"], obj=db)
1560+
print(result.exit_code)
1561+
print(result.output)
1562+
assert result.exit_code == 0
1563+
assert db.cfgdb.get_entry("VLAN", "Vlan999") == {"vlanid": "999"}
1564+
1565+
# Add a DHCPV4_RELAY entry for Vlan999
1566+
db.cfgdb.set_entry("DHCPV4_RELAY", "Vlan999", {
1567+
"dhcpv4_servers": ["192.0.2.1"],
1568+
"source_interface": "Ethernet4"
1569+
})
1570+
1571+
# Deleting Vlan999 which is being used in DHCPv4 Relay Configuration
1572+
with mock.patch("utilities_common.dhcp_relay_util.handle_restart_dhcp_relay_service") as mock_handle_restart:
1573+
result = runner.invoke(config.config.commands["vlan"].commands["del"], ["999"], obj=db)
1574+
print(result.exit_code)
1575+
print(result.output)
1576+
1577+
assert result.exit_code != 0
1578+
assert "Vlan999 cannot be removed as it is being used in DHCPV4_RELAY table." in result.output
1579+
1580+
# Remove DHCPV4_RELAY entry and try deletion again
1581+
db.cfgdb.set_entry("DHCPV4_RELAY", "Vlan999", None)
1582+
1583+
with mock.patch("utilities_common.dhcp_relay_util.handle_restart_dhcp_relay_service") as mock_handle_restart:
1584+
result = runner.invoke(config.config.commands["vlan"].commands["del"], ["999"], obj=db)
1585+
print(result.exit_code)
1586+
print(result.output)
1587+
1588+
assert result.exit_code == 0
1589+
assert "Vlan999" not in db.cfgdb.get_keys("VLAN")
1590+
15551591
@pytest.mark.parametrize("ip_version", ["ipv4", "ipv6"])
15561592
def test_config_add_del_vlan_with_dhcp_relay_not_running(self, ip_version):
15571593
runner = CliRunner()

tests/vrf_test.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,32 @@ def test_vrf_add_del(self):
219219
assert ('Vrf100') in db.cfgdb.get_table('VRF')
220220
assert result.exit_code == 0
221221

222+
# Add dummy VLAN and DHCP relay config using the VRF
223+
vlan = "Vlan100"
224+
server_ip = "192.0.2.1"
225+
db.cfgdb.mod_entry("VLAN", vlan, {})
226+
227+
db.cfgdb.set_entry("DHCPV4_RELAY", vlan, {
228+
"dhcpv4_servers": [server_ip],
229+
"server_vrf": "Vrf100",
230+
"link_selection": "enable",
231+
"vrf_selection": "enable",
232+
"server_id_override": "enable"
233+
})
234+
235+
assert result.exit_code == 0
236+
237+
# Attempt to delete the VRF in use by DHCPv4_RELAY ὀ~T should failfa
238+
result = runner.invoke(config.config.commands["vrf"].commands["del"], ["Vrf100"], obj=vrf_obj)
239+
assert result.exit_code != 0
240+
assert "VRF 'Vrf100' is in use for dhcp_relay configurations for Vlan100" in result.output
241+
242+
# Clean up the DHCP config to allow VRF deletion
243+
db.cfgdb.set_entry("DHCPV4_RELAY", vlan, None)
244+
result = runner.invoke(config.config.commands["vrf"].commands["del"], ["Vrf100"], obj=vrf_obj)
245+
assert result.exit_code == 0
246+
assert "Vrf100" not in db.cfgdb.get_table("VRF")
247+
222248
result = runner.invoke(config.config.commands["vrf"].commands["add"], ["Vrf1"], obj=vrf_obj)
223249
assert "VRF Vrf1 already exists!" in result.output
224250
assert ('Vrf1') in db.cfgdb.get_table('VRF')

0 commit comments

Comments
 (0)