Skip to content

Commit cca0499

Browse files
cshivashgitYairRaviv
authored andcommitted
[dhcp_relay]: sonic dhcp relay agent for IPv4 (sonic-net#4004)
Currently SONiC uses the 'isc-dhcp-relay' package to allow DHCP relay functionality on IPv4 networks. With this PR we are adding sonic dhcp relay agent for IPv4 as described in this HLD(sonic-net/SONiC#1938). What I did Added dependency checks in SONiC config CLI to prevent deletion of interfaces, VRFs, and VLANs that are in use by DHCPv4 relay configurations. Added/updated unit tests. How I did it Implemented a new function check_dhcpv4_relay_dependencies to check if an interface or VRF is referenced in the DHCPV4_RELAY table. Called this function before allowing deletion of portchannels, loopbacks, VRFs, and VLANs. Modified the VLAN deletion logic to block removal if the VLAN is present in the DHCPV4_RELAY table. Extended the test suite to verify that deletion is blocked when dependencies exist and allowed after cleanup. How to verify it Run the updated unit tests: they now include cases where deletion of interfaces, VRFs, or VLANs in use by DHCPv4 relay is attempted and should fail. Manually test by configuring DHCPv4 relay on an interface/VRF/VLAN, then attempt to delete it using the CLI; the command should fail with an appropriate error message. After removing the DHCPv4 relay configuration, deletion should succeed.
1 parent fc3b9a9 commit cca0499

8 files changed

Lines changed: 403 additions & 0 deletions

File tree

config/main.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1625,6 +1625,28 @@ def config_file_yang_validation(filename):
16251625
return True
16261626

16271627

1628+
def check_dhcpv4_relay_dependencies(db, object_name, object_type):
1629+
"""Checks if to be deleted interface/VRF is used in DHCPV4_RELAY table."""
1630+
# Check if has_sonic_dhcpv4_relay flag is enabled
1631+
feature_table = db.get_table("DEVICE_METADATA")
1632+
dhcp_relay_feature = feature_table.get("localhost", {})
1633+
if dhcp_relay_feature.get("has_sonic_dhcpv4_relay") != "True":
1634+
return
1635+
1636+
for relay_vlan, data in db.get_table('DHCPV4_RELAY').items():
1637+
if object_type == 'interface':
1638+
source_intf = data.get('source_interface')
1639+
if source_intf == object_name:
1640+
raise ValueError(f"Interface '{object_name}' is in use by {relay_vlan}")
1641+
1642+
elif object_type == 'vrf':
1643+
server_vrf = data.get('server_vrf')
1644+
if server_vrf == object_name:
1645+
raise ValueError(f"VRF '{object_name}' is in use for dhcp_relay configurations for {relay_vlan}")
1646+
else:
1647+
raise ValueError("Unsupported object_type: {}".format(object_type))
1648+
1649+
16281650
# This is our main entrypoint - the main 'config' command
16291651
@click.group(cls=clicommon.AbbreviationGroup, context_settings=CONTEXT_SETTINGS)
16301652
@click.pass_context
@@ -2803,6 +2825,12 @@ def remove_portchannel(ctx, portchannel_name):
28032825
if len([(k, v) for k, v in db.get_table('PORTCHANNEL_MEMBER') if k == portchannel_name]) != 0: # TODO: MISSING CONSTRAINT IN YANG MODEL
28042826
ctx.fail("Error: Portchannel {} contains members. Remove members before deleting Portchannel!".format(portchannel_name))
28052827

2828+
# Dont proceed if the port channel is used in dhcpv4_relay
2829+
try:
2830+
check_dhcpv4_relay_dependencies(db, portchannel_name, 'interface')
2831+
except ValueError as e:
2832+
ctx.fail(str(e))
2833+
28062834
try:
28072835
db.set_entry('PORTCHANNEL', portchannel_name, None)
28082836
except JsonPatchConflict:
@@ -7371,6 +7399,12 @@ def del_vrf(ctx, vrf_name):
73717399
syslog_vrf = syslog_data.get("vrf")
73727400
if syslog_vrf == syslog_vrf_dev:
73737401
ctx.fail("Failed to remove VRF device: {} is in use by SYSLOG_SERVER|{}".format(syslog_vrf, syslog_entry))
7402+
# Dont proceed if the vrf is used in dhcpv4_relay
7403+
try:
7404+
check_dhcpv4_relay_dependencies(config_db, vrf_name, 'vrf')
7405+
except ValueError as e:
7406+
ctx.fail(str(e))
7407+
73747408
if not is_vrf_exists(config_db, vrf_name):
73757409
ctx.fail("VRF {} does not exist!".format(vrf_name))
73767410
elif (vrf_name == 'mgmt' or vrf_name == 'management'):
@@ -8549,6 +8583,12 @@ def del_loopback(ctx, loopback_name):
85498583
if loopback_name not in lo_intfs:
85508584
ctx.fail("{} does not exist".format(loopback_name))
85518585

8586+
# Dont proceed if the loopback is used in dhcpv4_relay
8587+
try:
8588+
check_dhcpv4_relay_dependencies(config_db, loopback_name, 'interface')
8589+
except ValueError as e:
8590+
ctx.fail(str(e))
8591+
85528592
ips = [ k[1] for k in lo_config_db if type(k) == tuple and k[0] == loopback_name ]
85538593
for ip in ips:
85548594
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

scripts/db_migrator.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,40 @@ def migrate_aaa(self):
902902
if keys:
903903
self.configDB.delete(self.configDB.CONFIG_DB, authorization_key)
904904

905+
def migrate_dhcp_servers_to_dhcpv4_relay(self):
906+
try:
907+
vlan_table = self.configDB.get_table("VLAN")
908+
except Exception as e:
909+
log.log_error(f"Failed to read VLAN table: {str(e)}")
910+
return
911+
912+
for vlan_key, vlan_data in vlan_table.items():
913+
if "dhcp_servers" not in vlan_data:
914+
continue
915+
try:
916+
dhcp_servers = vlan_data.get("dhcp_servers")
917+
relay_data = self.configDB.get_entry("DHCPV4_RELAY", vlan_key) or {}
918+
if "dhcpv4_servers" not in relay_data:
919+
relay_data["dhcpv4_servers"] = dhcp_servers
920+
self.configDB.set_entry("DHCPV4_RELAY", vlan_key, relay_data)
921+
migrated_entry = self.configDB.get_entry("DHCPV4_RELAY", vlan_key)
922+
if migrated_entry.get("dhcpv4_servers") == dhcp_servers:
923+
log.log_notice(f"Migrated DHCP servers for {vlan_key} to DHCPV4_RELAY table")
924+
else:
925+
log.log_error(f"Verification failed for {vlan_key}: Migration did not persist correctly")
926+
continue
927+
else:
928+
log.log_notice(f"Skipping migration for {vlan_key}: dhcpv4_servers already present in DHCPV4_RELAY")
929+
updated_vlan_data = vlan_data.copy()
930+
del updated_vlan_data["dhcp_servers"]
931+
self.configDB.set_entry("VLAN", vlan_key, updated_vlan_data)
932+
933+
log.log_notice(f"Migrated DHCP servers for {vlan_key} to DHCPV4_RELAY table")
934+
935+
except Exception as e:
936+
log.log_error(f"Failed to migrate DHCP servers for {vlan_key}: {str(e)}")
937+
938+
905939
def version_unknown(self):
906940
"""
907941
version_unknown tracks all SONiC versions that doesn't have a version
@@ -1231,12 +1265,23 @@ def version_4_0_3(self):
12311265
self.set_version('version_202305_01')
12321266
return 'version_202305_01'
12331267

1268+
def check_has_sonic_dhcpv4_relay_flag(self):
1269+
device_metadata_table = self.configDB.get_table("DEVICE_METADATA")
1270+
dhcp_relay_feature = device_metadata_table.get("localhost", {})
1271+
if dhcp_relay_feature.get("has_sonic_dhcpv4_relay") == "True":
1272+
return True
1273+
return False
1274+
12341275
def version_202305_01(self):
12351276
"""
12361277
Version 202305_01.
12371278
This is current last erversion for 202305 branch
12381279
"""
12391280
log.log_info('Handling version_202305_01')
1281+
if self.check_has_sonic_dhcpv4_relay_flag():
1282+
log.log_info("Triggering migrate_dhcp_servers_to_dhcpv4_relay()")
1283+
self.migrate_dhcp_servers_to_dhcpv4_relay()
1284+
12401285
self.set_version('version_202311_01')
12411286
return 'version_202311_01'
12421287

@@ -1250,6 +1295,10 @@ def version_202311_01(self):
12501295
self.migrate_dns_nameserver()
12511296

12521297
self.migrate_sflow_table()
1298+
if self.check_has_sonic_dhcpv4_relay_flag():
1299+
log.log_info("Triggering migrate_dhcp_servers_to_dhcpv4_relay()")
1300+
self.migrate_dhcp_servers_to_dhcpv4_relay()
1301+
12531302
self.set_version('version_202311_02')
12541303
return 'version_202311_02'
12551304

@@ -1260,6 +1309,9 @@ def version_202311_02(self):
12601309
log.log_info('Handling version_202311_02')
12611310
# Update GNMI table
12621311
self.migrate_gnmi()
1312+
if self.check_has_sonic_dhcpv4_relay_flag():
1313+
log.log_info("Triggering migrate_dhcp_servers_to_dhcpv4_relay()")
1314+
self.migrate_dhcp_servers_to_dhcpv4_relay()
12631315

12641316
self.set_version('version_202311_03')
12651317
return 'version_202311_03'
@@ -1270,6 +1322,10 @@ def version_202311_03(self):
12701322
This is current last erversion for 202311 branch
12711323
"""
12721324
log.log_info('Handling version_202311_03')
1325+
if self.check_has_sonic_dhcpv4_relay_flag():
1326+
log.log_info("Triggering migrate_dhcp_servers_to_dhcpv4_relay()")
1327+
self.migrate_dhcp_servers_to_dhcpv4_relay()
1328+
12731329
self.set_version('version_202405_01')
12741330
return 'version_202405_01'
12751331

@@ -1278,6 +1334,10 @@ def version_202405_01(self):
12781334
Version 202405_01.
12791335
"""
12801336
log.log_info('Handling version_202405_01')
1337+
if self.check_has_sonic_dhcpv4_relay_flag():
1338+
log.log_info("Triggering migrate_dhcp_servers_to_dhcpv4_relay()")
1339+
self.migrate_dhcp_servers_to_dhcpv4_relay()
1340+
12811341
self.set_version('version_202405_02')
12821342
return 'version_202405_02'
12831343

@@ -1286,6 +1346,10 @@ def version_202405_02(self):
12861346
Version 202405_02.
12871347
"""
12881348
log.log_info('Handling version_202405_02')
1349+
if self.check_has_sonic_dhcpv4_relay_flag():
1350+
log.log_info("Triggering migrate_dhcp_servers_to_dhcpv4_relay()")
1351+
self.migrate_dhcp_servers_to_dhcpv4_relay()
1352+
12891353
self.migrate_ipinip_tunnel()
12901354
self.set_version('version_202411_01')
12911355
return 'version_202411_01'
@@ -1295,6 +1359,10 @@ def version_202411_01(self):
12951359
Version 202411_01.
12961360
"""
12971361
log.log_info('Handling version_202411_01')
1362+
if self.check_has_sonic_dhcpv4_relay_flag():
1363+
log.log_info("Triggering migrate_dhcp_servers_to_dhcpv4_relay()")
1364+
self.migrate_dhcp_servers_to_dhcpv4_relay()
1365+
12981366
self.set_version('version_202411_02')
12991367
return 'version_202411_02'
13001368

@@ -1303,6 +1371,10 @@ def version_202411_02(self):
13031371
Version 202411_02.
13041372
"""
13051373
log.log_info('Handling version_202411_02')
1374+
if self.check_has_sonic_dhcpv4_relay_flag():
1375+
log.log_info("Triggering migrate_dhcp_servers_to_dhcpv4_relay()")
1376+
self.migrate_dhcp_servers_to_dhcpv4_relay()
1377+
13061378
self.set_version('version_202505_01')
13071379
return 'version_202505_01'
13081380

@@ -1312,6 +1384,10 @@ def version_202505_01(self):
13121384
master branch until 202505 branch is created.
13131385
"""
13141386
log.log_info('Handling version_202505_01')
1387+
if self.check_has_sonic_dhcpv4_relay_flag():
1388+
log.log_info("Triggering migrate_dhcp_servers_to_dhcpv4_relay()")
1389+
self.migrate_dhcp_servers_to_dhcpv4_relay()
1390+
13151391
self.migrate_flex_counter_delay_status_removal()
13161392
return None
13171393

tests/config_test.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3227,6 +3227,34 @@ def test_del_nonexistent_loopback_adhoc_validation(self):
32273227
assert result.exit_code != 0
32283228
assert "Loopbax1 is invalid, name should have prefix 'Loopback' and suffix '<0-999>'" in result.output
32293229

3230+
def test_del_loopback_with_dhcpv4_relay_entry(self):
3231+
config.ADHOC_VALIDATION = True
3232+
runner = CliRunner()
3233+
db = Db()
3234+
obj = {'db': db.cfgdb}
3235+
3236+
result = runner.invoke(config.config.commands["loopback"].commands["add"], ["Loopback1"], obj=obj)
3237+
print(result.exit_code)
3238+
print(result.output)
3239+
assert result.exit_code == 0
3240+
assert db.cfgdb.get_entry("LOOPBACK_INTERFACE", "Loopback1") == {}
3241+
3242+
# Enable has_sonic_dhcpv4_relay flag
3243+
db.cfgdb.set_entry("DEVICE_METADATA", "localhost", {"has_sonic_dhcpv4_relay": "True"})
3244+
3245+
db.cfgdb.set_entry("DHCPV4_RELAY", "Vlan100", {
3246+
"dhcpv4_servers": ["192.0.2.100"],
3247+
"source_interface": "Loopback1"
3248+
})
3249+
3250+
result = runner.invoke(config.config.commands["loopback"].commands["del"], ["Loopback1"], obj=obj)
3251+
print(result.exit_code)
3252+
print(result.output)
3253+
assert result.exit_code != 0
3254+
assert "Error: Interface 'Loopback1' is in use by Vlan100" in result.output
3255+
3256+
db.cfgdb.set_entry("DHCPV4_RELAY", "Vlan200", None)
3257+
32303258
@patch("config.validated_config_db_connector.ValidatedConfigDBConnector.validated_set_entry", mock.Mock(return_value=True))
32313259
@patch("validated_config_db_connector.device_info.is_yang_config_validation_enabled", mock.Mock(return_value=True))
32323260
def test_add_loopback_yang_validation(self):

0 commit comments

Comments
 (0)