diff --git a/config/main.py b/config/main.py index d72edbf7b6..2b2dc287d9 100644 --- a/config/main.py +++ b/config/main.py @@ -1619,6 +1619,28 @@ def config_file_yang_validation(filename): return True +def check_dhcpv4_relay_dependencies(db, object_name, object_type): + """Checks if to be deleted interface/VRF is used in DHCPV4_RELAY table.""" + # Check if has_sonic_dhcpv4_relay flag is enabled + feature_table = db.get_table("DEVICE_METADATA") + dhcp_relay_feature = feature_table.get("localhost", {}) + if dhcp_relay_feature.get("has_sonic_dhcpv4_relay") != "True": + return + + for relay_vlan, data in db.get_table('DHCPV4_RELAY').items(): + if object_type == 'interface': + source_intf = data.get('source_interface') + if source_intf == object_name: + raise ValueError(f"Interface '{object_name}' is in use by {relay_vlan}") + + elif object_type == 'vrf': + server_vrf = data.get('server_vrf') + if server_vrf == object_name: + raise ValueError(f"VRF '{object_name}' is in use for dhcp_relay configurations for {relay_vlan}") + else: + raise ValueError("Unsupported object_type: {}".format(object_type)) + + # This is our main entrypoint - the main 'config' command @click.group(cls=clicommon.AbbreviationGroup, context_settings=CONTEXT_SETTINGS) @click.pass_context @@ -2797,6 +2819,12 @@ def remove_portchannel(ctx, portchannel_name): if len([(k, v) for k, v in db.get_table('PORTCHANNEL_MEMBER') if k == portchannel_name]) != 0: # TODO: MISSING CONSTRAINT IN YANG MODEL ctx.fail("Error: Portchannel {} contains members. Remove members before deleting Portchannel!".format(portchannel_name)) + # Dont proceed if the port channel is used in dhcpv4_relay + try: + check_dhcpv4_relay_dependencies(db, portchannel_name, 'interface') + except ValueError as e: + ctx.fail(str(e)) + try: db.set_entry('PORTCHANNEL', portchannel_name, None) except JsonPatchConflict: @@ -7365,6 +7393,12 @@ def del_vrf(ctx, vrf_name): syslog_vrf = syslog_data.get("vrf") if syslog_vrf == syslog_vrf_dev: ctx.fail("Failed to remove VRF device: {} is in use by SYSLOG_SERVER|{}".format(syslog_vrf, syslog_entry)) + # Dont proceed if the vrf is used in dhcpv4_relay + try: + check_dhcpv4_relay_dependencies(config_db, vrf_name, 'vrf') + except ValueError as e: + ctx.fail(str(e)) + if not is_vrf_exists(config_db, vrf_name): ctx.fail("VRF {} does not exist!".format(vrf_name)) elif (vrf_name == 'mgmt' or vrf_name == 'management'): @@ -8543,6 +8577,12 @@ def del_loopback(ctx, loopback_name): if loopback_name not in lo_intfs: ctx.fail("{} does not exist".format(loopback_name)) + # Dont proceed if the loopback is used in dhcpv4_relay + try: + check_dhcpv4_relay_dependencies(config_db, loopback_name, 'interface') + except ValueError as e: + ctx.fail(str(e)) + ips = [ k[1] for k in lo_config_db if type(k) == tuple and k[0] == loopback_name ] for ip in ips: config_db.set_entry('LOOPBACK_INTERFACE', (loopback_name, ip), None) diff --git a/config/vlan.py b/config/vlan.py index eae51eb312..7886ce11cd 100644 --- a/config/vlan.py +++ b/config/vlan.py @@ -184,6 +184,8 @@ def del_vlan(db, vid, multiple, no_restart_dhcp_relay): "First remove vxlan mapping '{}' assigned to VLAN".format( vid, '|'.join(vxmap_key))) + if vlan in db.cfgdb.get_table('DHCPV4_RELAY'): + ctx.fail(f"{vlan} cannot be removed as it is being used in DHCPV4_RELAY table.") # set dhcpv4_relay table set_dhcp_relay_table('VLAN', config_db, vlan, None) diff --git a/scripts/db_migrator.py b/scripts/db_migrator.py index 1c055537ce..9ca2fd2d3a 100755 --- a/scripts/db_migrator.py +++ b/scripts/db_migrator.py @@ -902,6 +902,40 @@ def migrate_aaa(self): if keys: self.configDB.delete(self.configDB.CONFIG_DB, authorization_key) + def migrate_dhcp_servers_to_dhcpv4_relay(self): + try: + vlan_table = self.configDB.get_table("VLAN") + except Exception as e: + log.log_error(f"Failed to read VLAN table: {str(e)}") + return + + for vlan_key, vlan_data in vlan_table.items(): + if "dhcp_servers" not in vlan_data: + continue + try: + dhcp_servers = vlan_data.get("dhcp_servers") + relay_data = self.configDB.get_entry("DHCPV4_RELAY", vlan_key) or {} + if "dhcpv4_servers" not in relay_data: + relay_data["dhcpv4_servers"] = dhcp_servers + self.configDB.set_entry("DHCPV4_RELAY", vlan_key, relay_data) + migrated_entry = self.configDB.get_entry("DHCPV4_RELAY", vlan_key) + if migrated_entry.get("dhcpv4_servers") == dhcp_servers: + log.log_notice(f"Migrated DHCP servers for {vlan_key} to DHCPV4_RELAY table") + else: + log.log_error(f"Verification failed for {vlan_key}: Migration did not persist correctly") + continue + else: + log.log_notice(f"Skipping migration for {vlan_key}: dhcpv4_servers already present in DHCPV4_RELAY") + updated_vlan_data = vlan_data.copy() + del updated_vlan_data["dhcp_servers"] + self.configDB.set_entry("VLAN", vlan_key, updated_vlan_data) + + log.log_notice(f"Migrated DHCP servers for {vlan_key} to DHCPV4_RELAY table") + + except Exception as e: + log.log_error(f"Failed to migrate DHCP servers for {vlan_key}: {str(e)}") + + def version_unknown(self): """ version_unknown tracks all SONiC versions that doesn't have a version @@ -1231,12 +1265,23 @@ def version_4_0_3(self): self.set_version('version_202305_01') return 'version_202305_01' + def check_has_sonic_dhcpv4_relay_flag(self): + device_metadata_table = self.configDB.get_table("DEVICE_METADATA") + dhcp_relay_feature = device_metadata_table.get("localhost", {}) + if dhcp_relay_feature.get("has_sonic_dhcpv4_relay") == "True": + return True + return False + def version_202305_01(self): """ Version 202305_01. This is current last erversion for 202305 branch """ log.log_info('Handling version_202305_01') + if self.check_has_sonic_dhcpv4_relay_flag(): + log.log_info("Triggering migrate_dhcp_servers_to_dhcpv4_relay()") + self.migrate_dhcp_servers_to_dhcpv4_relay() + self.set_version('version_202311_01') return 'version_202311_01' @@ -1250,6 +1295,10 @@ def version_202311_01(self): self.migrate_dns_nameserver() self.migrate_sflow_table() + if self.check_has_sonic_dhcpv4_relay_flag(): + log.log_info("Triggering migrate_dhcp_servers_to_dhcpv4_relay()") + self.migrate_dhcp_servers_to_dhcpv4_relay() + self.set_version('version_202311_02') return 'version_202311_02' @@ -1260,6 +1309,9 @@ def version_202311_02(self): log.log_info('Handling version_202311_02') # Update GNMI table self.migrate_gnmi() + if self.check_has_sonic_dhcpv4_relay_flag(): + log.log_info("Triggering migrate_dhcp_servers_to_dhcpv4_relay()") + self.migrate_dhcp_servers_to_dhcpv4_relay() self.set_version('version_202311_03') return 'version_202311_03' @@ -1270,6 +1322,10 @@ def version_202311_03(self): This is current last erversion for 202311 branch """ log.log_info('Handling version_202311_03') + if self.check_has_sonic_dhcpv4_relay_flag(): + log.log_info("Triggering migrate_dhcp_servers_to_dhcpv4_relay()") + self.migrate_dhcp_servers_to_dhcpv4_relay() + self.set_version('version_202405_01') return 'version_202405_01' @@ -1278,6 +1334,10 @@ def version_202405_01(self): Version 202405_01. """ log.log_info('Handling version_202405_01') + if self.check_has_sonic_dhcpv4_relay_flag(): + log.log_info("Triggering migrate_dhcp_servers_to_dhcpv4_relay()") + self.migrate_dhcp_servers_to_dhcpv4_relay() + self.set_version('version_202405_02') return 'version_202405_02' @@ -1286,6 +1346,10 @@ def version_202405_02(self): Version 202405_02. """ log.log_info('Handling version_202405_02') + if self.check_has_sonic_dhcpv4_relay_flag(): + log.log_info("Triggering migrate_dhcp_servers_to_dhcpv4_relay()") + self.migrate_dhcp_servers_to_dhcpv4_relay() + self.migrate_ipinip_tunnel() self.set_version('version_202411_01') return 'version_202411_01' @@ -1295,6 +1359,10 @@ def version_202411_01(self): Version 202411_01. """ log.log_info('Handling version_202411_01') + if self.check_has_sonic_dhcpv4_relay_flag(): + log.log_info("Triggering migrate_dhcp_servers_to_dhcpv4_relay()") + self.migrate_dhcp_servers_to_dhcpv4_relay() + self.set_version('version_202411_02') return 'version_202411_02' @@ -1303,6 +1371,10 @@ def version_202411_02(self): Version 202411_02. """ log.log_info('Handling version_202411_02') + if self.check_has_sonic_dhcpv4_relay_flag(): + log.log_info("Triggering migrate_dhcp_servers_to_dhcpv4_relay()") + self.migrate_dhcp_servers_to_dhcpv4_relay() + self.set_version('version_202505_01') return 'version_202505_01' @@ -1312,6 +1384,10 @@ def version_202505_01(self): master branch until 202505 branch is created. """ log.log_info('Handling version_202505_01') + if self.check_has_sonic_dhcpv4_relay_flag(): + log.log_info("Triggering migrate_dhcp_servers_to_dhcpv4_relay()") + self.migrate_dhcp_servers_to_dhcpv4_relay() + self.migrate_flex_counter_delay_status_removal() return None diff --git a/tests/config_test.py b/tests/config_test.py index 6a02e93e88..d8d9836cc9 100644 --- a/tests/config_test.py +++ b/tests/config_test.py @@ -3218,6 +3218,34 @@ def test_del_nonexistent_loopback_adhoc_validation(self): assert result.exit_code != 0 assert "Loopbax1 is invalid, name should have prefix 'Loopback' and suffix '<0-999>'" in result.output + def test_del_loopback_with_dhcpv4_relay_entry(self): + config.ADHOC_VALIDATION = True + runner = CliRunner() + db = Db() + obj = {'db': db.cfgdb} + + result = runner.invoke(config.config.commands["loopback"].commands["add"], ["Loopback1"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + assert db.cfgdb.get_entry("LOOPBACK_INTERFACE", "Loopback1") == {} + + # Enable has_sonic_dhcpv4_relay flag + db.cfgdb.set_entry("DEVICE_METADATA", "localhost", {"has_sonic_dhcpv4_relay": "True"}) + + db.cfgdb.set_entry("DHCPV4_RELAY", "Vlan100", { + "dhcpv4_servers": ["192.0.2.100"], + "source_interface": "Loopback1" + }) + + result = runner.invoke(config.config.commands["loopback"].commands["del"], ["Loopback1"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code != 0 + assert "Error: Interface 'Loopback1' is in use by Vlan100" in result.output + + db.cfgdb.set_entry("DHCPV4_RELAY", "Vlan200", None) + @patch("config.validated_config_db_connector.ValidatedConfigDBConnector.validated_set_entry", mock.Mock(return_value=True)) @patch("validated_config_db_connector.device_info.is_yang_config_validation_enabled", mock.Mock(return_value=True)) def test_add_loopback_yang_validation(self): diff --git a/tests/db_migrator_test.py b/tests/db_migrator_test.py index 3cb81e09dc..dd2161a643 100644 --- a/tests/db_migrator_test.py +++ b/tests/db_migrator_test.py @@ -1054,3 +1054,173 @@ def test_tunnel_migrator(self): expected_keys = expected_appl_db.get_all(expected_appl_db.APPL_DB, key) diff = DeepDiff(resulting_keys, expected_keys, ignore_order=True) assert not diff + + +class TestDhcpv4RelayMigrator(object): + @classmethod + def setup_class(cls): + os.environ['UTILITIES_UNIT_TESTING'] = "2" + + @classmethod + def teardown_class(cls): + os.environ['UTILITIES_UNIT_TESTING'] = "0" + dbconnector.dedicated_dbs['CONFIG_DB'] = None + + def test_check_has_sonic_dhcpv4_relay_flag_true(self): + dbconnector.dedicated_dbs['CONFIG_DB'] = os.path.join(mock_db_path, 'config_db', 'dns_nameserver_expected') + import db_migrator + dbmgtr = db_migrator.DBMigrator(None) + + # Set the flag to True + dbmgtr.configDB.set_entry("DEVICE_METADATA", "localhost", { + "has_sonic_dhcpv4_relay": "True" + }) + + assert dbmgtr.check_has_sonic_dhcpv4_relay_flag() is True + + def test_check_has_sonic_dhcpv4_relay_flag_false(self): + dbconnector.dedicated_dbs['CONFIG_DB'] = os.path.join(mock_db_path, 'config_db', 'dns_nameserver_expected') + import db_migrator + dbmgtr = db_migrator.DBMigrator(None) + + # Set the flag to False + dbmgtr.configDB.set_entry("DEVICE_METADATA", "localhost", { + "has_sonic_dhcpv4_relay": "False" + }) + + assert dbmgtr.check_has_sonic_dhcpv4_relay_flag() is False + + def test_check_has_sonic_dhcpv4_relay_flag_not_set(self): + dbconnector.dedicated_dbs['CONFIG_DB'] = os.path.join(mock_db_path, 'config_db', 'dns_nameserver_expected') + import db_migrator + dbmgtr = db_migrator.DBMigrator(None) + + # Ensure flag is not set + dbmgtr.configDB.set_entry("DEVICE_METADATA", "localhost", {}) + + assert dbmgtr.check_has_sonic_dhcpv4_relay_flag() is False + + def test_migrate_dhcp_servers_to_dhcpv4_relay_success(self): + dbconnector.dedicated_dbs['CONFIG_DB'] = os.path.join(mock_db_path, 'config_db', 'dns_nameserver_expected') + import db_migrator + dbmgtr = db_migrator.DBMigrator(None) + + # Setup initial VLAN with dhcp_servers + dbmgtr.configDB.set_entry("VLAN", "Vlan100", { + "vlanid": "100", + "dhcp_servers": ["192.0.2.1", "192.0.2.2"] + }) + + # Run migration + dbmgtr.migrate_dhcp_servers_to_dhcpv4_relay() + + # Verify DHCPV4_RELAY entry created + relay_entry = dbmgtr.configDB.get_entry("DHCPV4_RELAY", "Vlan100") + assert relay_entry.get("dhcpv4_servers") == ["192.0.2.1", "192.0.2.2"] + + # Verify dhcp_servers removed from VLAN + vlan_entry = dbmgtr.configDB.get_entry("VLAN", "Vlan100") + assert "dhcp_servers" not in vlan_entry + assert vlan_entry.get("vlanid") == "100" + + def test_migrate_dhcp_servers_skip_if_no_dhcp_servers(self): + dbconnector.dedicated_dbs['CONFIG_DB'] = os.path.join(mock_db_path, 'config_db', 'dns_nameserver_expected') + import db_migrator + dbmgtr = db_migrator.DBMigrator(None) + + # Setup VLAN without dhcp_servers + dbmgtr.configDB.set_entry("VLAN", "Vlan200", {"vlanid": "200"}) + + # Run migration + dbmgtr.migrate_dhcp_servers_to_dhcpv4_relay() + + # Verify no DHCPV4_RELAY entry created + relay_entry = dbmgtr.configDB.get_entry("DHCPV4_RELAY", "Vlan200") + assert not relay_entry or "dhcpv4_servers" not in relay_entry + + def test_migrate_dhcp_servers_skip_if_already_migrated(self): + dbconnector.dedicated_dbs['CONFIG_DB'] = os.path.join(mock_db_path, 'config_db', 'dns_nameserver_expected') + import db_migrator + dbmgtr = db_migrator.DBMigrator(None) + + # Setup VLAN with dhcp_servers + dbmgtr.configDB.set_entry("VLAN", "Vlan300", { + "vlanid": "300", + "dhcp_servers": ["192.0.2.1"] + }) + + # Setup existing DHCPV4_RELAY entry + dbmgtr.configDB.set_entry("DHCPV4_RELAY", "Vlan300", { + "dhcpv4_servers": ["10.0.0.1"] + }) + + # Run migration + dbmgtr.migrate_dhcp_servers_to_dhcpv4_relay() + + # Verify existing entry not overwritten + relay_entry = dbmgtr.configDB.get_entry("DHCPV4_RELAY", "Vlan300") + assert relay_entry.get("dhcpv4_servers") == ["10.0.0.1"] + + vlan_entry = dbmgtr.configDB.get_entry("VLAN", "Vlan300") + assert "dhcp_servers" not in vlan_entry + + def test_migrate_dhcp_servers_multiple_vlans(self): + dbconnector.dedicated_dbs['CONFIG_DB'] = os.path.join(mock_db_path, 'config_db', 'dns_nameserver_expected') + import db_migrator + dbmgtr = db_migrator.DBMigrator(None) + + # Setup multiple VLANs with dhcp_servers + dbmgtr.configDB.set_entry("VLAN", "Vlan400", { + "vlanid": "400", + "dhcp_servers": ["192.0.2.10"] + }) + dbmgtr.configDB.set_entry("VLAN", "Vlan500", { + "vlanid": "500", + "dhcp_servers": ["192.0.2.20"] + }) + + # Run migration + dbmgtr.migrate_dhcp_servers_to_dhcpv4_relay() + + # Verify both VLANs migrated + relay_entry_400 = dbmgtr.configDB.get_entry("DHCPV4_RELAY", "Vlan400") + assert relay_entry_400.get("dhcpv4_servers") == ["192.0.2.10"] + + relay_entry_500 = dbmgtr.configDB.get_entry("DHCPV4_RELAY", "Vlan500") + assert relay_entry_500.get("dhcpv4_servers") == ["192.0.2.20"] + + # Verify dhcp_servers removed from both VLANs + vlan_entry_400 = dbmgtr.configDB.get_entry("VLAN", "Vlan400") + assert "dhcp_servers" not in vlan_entry_400 + + vlan_entry_500 = dbmgtr.configDB.get_entry("VLAN", "Vlan500") + assert "dhcp_servers" not in vlan_entry_500 + + @pytest.mark.parametrize("version_method,vlan_id,dhcp_server", [ + ("version_202305_01", "100", "192.0.2.1"), + ("version_202311_01", "200", "192.0.2.2"), + ("version_202311_02", "300", "192.0.2.3"), + ("version_202311_03", "400", "192.0.2.4"), + ("version_202405_01", "500", "192.0.2.5"), + ("version_202405_02", "600", "192.0.2.6"), + ("version_202411_01", "700", "192.0.2.7"), + ("version_202411_02", "800", "192.0.2.8"), + ("version_202505_01", "900", "192.0.2.9"), + ]) + def test_version_methods_with_dhcp_migration(self, version_method, vlan_id, dhcp_server): + """Test all version methods call dhcp migration when flag is True""" + dbconnector.dedicated_dbs['CONFIG_DB'] = os.path.join(mock_db_path, 'config_db', 'dns_nameserver_expected') + import db_migrator + dbmgtr = db_migrator.DBMigrator(None) + + # Set flag to True and setup VLAN with dhcp_servers + dbmgtr.configDB.set_entry("DEVICE_METADATA", "localhost", {"has_sonic_dhcpv4_relay": "True"}) + vlan_name = f"Vlan{vlan_id}" + dbmgtr.configDB.set_entry("VLAN", vlan_name, {"vlanid": vlan_id, "dhcp_servers": [dhcp_server]}) + + # Call the version method dynamically + getattr(dbmgtr, version_method)() + + # Verify migration was executed + relay_entry = dbmgtr.configDB.get_entry("DHCPV4_RELAY", vlan_name) + assert relay_entry.get("dhcpv4_servers") == [dhcp_server] diff --git a/tests/portchannel_test.py b/tests/portchannel_test.py index d1223bd771..a365879e11 100644 --- a/tests/portchannel_test.py +++ b/tests/portchannel_test.py @@ -84,6 +84,28 @@ def test_delete_portchannel_with_invalid_name_adhoc_validation(self): assert result.exit_code != 0 assert "Error: PortChan005 is invalid!, name should have prefix 'PortChannel' and suffix '<0-9999>'" in result.output + def test_delete_portchannel_in_use_by_dhcpv4_relay(self): + config.ADHOC_VALIDATION = True + runner = CliRunner() + db = Db() + obj = {'db': db.cfgdb} + + result = runner.invoke(config.config.commands["portchannel"].commands["add"], ["PortChannel10"], obj=obj) + + # Enable has_sonic_dhcpv4_relay flag + db.cfgdb.set_entry("DEVICE_METADATA", "localhost", {"has_sonic_dhcpv4_relay": "True"}) + + db.cfgdb.set_entry("DHCPV4_RELAY", "Vlan100", {"source_interface": "PortChannel10"}) + + result = runner.invoke(config.config.commands["portchannel"].commands["del"], ["PortChannel10"], obj=obj) + print(result.exit_code) + print(result.output) + assert result.exit_code != 0 + assert "Interface 'PortChannel10' is in use by Vlan100" in result.output + + db.cfgdb.set_entry("DHCPV4_RELAY", "Vlan100", None) + + def test_add_existing_portchannel_again(self): runner = CliRunner() db = Db() diff --git a/tests/vlan_test.py b/tests/vlan_test.py index 59bfb15718..573fd09726 100644 --- a/tests/vlan_test.py +++ b/tests/vlan_test.py @@ -1551,6 +1551,42 @@ def test_config_add_del_vlan_dhcp_relay_with_non_empty_entry(self, ip_version, m mock_handle_restart.assert_called_once() assert "Restart service dhcp_relay failed with error" not in result.output + def test_config_add_del_vlan_dhcpv4_relay_with_non_empty_entry(self, mock_restart_dhcp_relay_service): + runner = CliRunner() + db = Db() + + result = runner.invoke(config.config.commands["vlan"].commands["add"], ["999"], obj=db) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + assert db.cfgdb.get_entry("VLAN", "Vlan999") == {"vlanid": "999"} + + # Add a DHCPV4_RELAY entry for Vlan999 + db.cfgdb.set_entry("DHCPV4_RELAY", "Vlan999", { + "dhcpv4_servers": ["192.0.2.1"], + "source_interface": "Ethernet4" + }) + + # Deleting Vlan999 which is being used in DHCPv4 Relay Configuration + with mock.patch("utilities_common.dhcp_relay_util.handle_restart_dhcp_relay_service"): + result = runner.invoke(config.config.commands["vlan"].commands["del"], ["999"], obj=db) + print(result.exit_code) + print(result.output) + + assert result.exit_code != 0 + assert "Vlan999 cannot be removed as it is being used in DHCPV4_RELAY table." in result.output + + # Remove DHCPV4_RELAY entry and try deletion again + db.cfgdb.set_entry("DHCPV4_RELAY", "Vlan999", None) + + with mock.patch("utilities_common.dhcp_relay_util.handle_restart_dhcp_relay_service"): + result = runner.invoke(config.config.commands["vlan"].commands["del"], ["999"], obj=db) + print(result.exit_code) + print(result.output) + + assert result.exit_code == 0 + assert "Vlan999" not in db.cfgdb.get_keys("VLAN") + @pytest.mark.parametrize("ip_version", ["ipv4", "ipv6"]) def test_config_add_del_vlan_with_dhcp_relay_not_running(self, ip_version): runner = CliRunner() diff --git a/tests/vrf_test.py b/tests/vrf_test.py index 1f0a20c12a..539e7c7717 100644 --- a/tests/vrf_test.py +++ b/tests/vrf_test.py @@ -219,6 +219,35 @@ def test_vrf_add_del(self): assert ('Vrf100') in db.cfgdb.get_table('VRF') assert result.exit_code == 0 + # Add dummy VLAN and DHCP relay config using the VRF + vlan = "Vlan100" + server_ip = "192.0.2.1" + db.cfgdb.mod_entry("VLAN", vlan, {}) + + # Enable has_sonic_dhcpv4_relay flag + db.cfgdb.set_entry("DEVICE_METADATA", "localhost", {"has_sonic_dhcpv4_relay": "True"}) + + db.cfgdb.set_entry("DHCPV4_RELAY", vlan, { + "dhcpv4_servers": [server_ip], + "server_vrf": "Vrf100", + "link_selection": "enable", + "vrf_selection": "enable", + "server_id_override": "enable" + }) + + assert result.exit_code == 0 + + # Attempt to delete the VRF in use by DHCPv4_RELAY ὀ~T should failfa + result = runner.invoke(config.config.commands["vrf"].commands["del"], ["Vrf100"], obj=vrf_obj) + assert result.exit_code != 0 + assert "VRF 'Vrf100' is in use for dhcp_relay configurations for Vlan100" in result.output + + # Clean up the DHCP config to allow VRF deletion + db.cfgdb.set_entry("DHCPV4_RELAY", vlan, None) + result = runner.invoke(config.config.commands["vrf"].commands["del"], ["Vrf100"], obj=vrf_obj) + assert result.exit_code == 0 + assert "Vrf100" not in db.cfgdb.get_table("VRF") + result = runner.invoke(config.config.commands["vrf"].commands["add"], ["Vrf1"], obj=vrf_obj) assert "VRF Vrf1 already exists!" in result.output assert ('Vrf1') in db.cfgdb.get_table('VRF')