diff --git a/config/dhcp_relay.py b/config/dhcp_relay.py new file mode 100644 index 0000000000..86d11bc82a --- /dev/null +++ b/config/dhcp_relay.py @@ -0,0 +1,156 @@ +import click +import ipaddress +import utilities_common.cli as clicommon +import utilities_common.dhcp_relay_util as dhcp_relay_util + + +DHCP_RELAY_TABLE = "DHCP_RELAY" +DHCPV6_SERVERS = "dhcpv6_servers" +IPV6 = 6 + +VLAN_TABLE = "VLAN" +DHCPV4_SERVERS = "dhcp_servers" +IPV4 = 4 + + +def validate_ips(ctx, ips, ip_version): + for ip in ips: + try: + ip_address = ipaddress.ip_address(ip) + except Exception: + ctx.fail("{} is invalid IP address".format(ip)) + + if ip_address.version != ip_version: + ctx.fail("{} is not IPv{} address".format(ip, ip_version)) + + +def get_dhcp_servers(db, vlan_name, ctx, table_name, dhcp_servers_str): + table = db.cfgdb.get_entry(table_name, vlan_name) + if len(table.keys()) == 0: + ctx.fail("{} doesn't exist".format(vlan_name)) + + dhcp_servers = table.get(dhcp_servers_str, []) + + return dhcp_servers, table + + +def get_dhcp_table_servers_key(ip_version): + table_name = DHCP_RELAY_TABLE if ip_version == 6 else VLAN_TABLE + dhcp_servers_str = DHCPV6_SERVERS if ip_version == 6 else DHCPV4_SERVERS + return table_name, dhcp_servers_str + + +def add_dhcp_relay(vid, dhcp_relay_ips, db, ip_version): + table_name, dhcp_servers_str = get_dhcp_table_servers_key(ip_version) + vlan_name = "Vlan{}".format(vid) + ctx = click.get_current_context() + # Verify ip addresses are valid + validate_ips(ctx, dhcp_relay_ips, ip_version) + dhcp_servers, table = get_dhcp_servers(db, vlan_name, ctx, table_name, dhcp_servers_str) + added_ips = [] + + for dhcp_relay_ip in dhcp_relay_ips: + # Verify ip addresses not duplicate in add list + if dhcp_relay_ip in added_ips: + ctx.fail("Error: Find duplicate DHCP relay ip {} in add list".format(dhcp_relay_ip)) + # Verify ip addresses not exist in DB + if dhcp_relay_ip in dhcp_servers: + click.echo("{} is already a DHCP relay for {}".format(dhcp_relay_ip, vlan_name)) + return + + dhcp_servers.append(dhcp_relay_ip) + added_ips.append(dhcp_relay_ip) + + table[dhcp_servers_str] = dhcp_servers + + db.cfgdb.set_entry(table_name, vlan_name, table) + click.echo("Added DHCP relay address [{}] to {}".format(",".join(dhcp_relay_ips), vlan_name)) + dhcp_relay_util.handle_restart_dhcp_relay_service() + + +def del_dhcp_relay(vid, dhcp_relay_ips, db, ip_version): + table_name, dhcp_servers_str = get_dhcp_table_servers_key(ip_version) + vlan_name = "Vlan{}".format(vid) + ctx = click.get_current_context() + # Verify ip addresses are valid + validate_ips(ctx, dhcp_relay_ips, ip_version) + dhcp_servers, table = get_dhcp_servers(db, vlan_name, ctx, table_name, dhcp_servers_str) + removed_ips = [] + + for dhcp_relay_ip in dhcp_relay_ips: + # Verify ip addresses not duplicate in del list + if dhcp_relay_ip in removed_ips: + ctx.fail("Error: Find duplicate DHCP relay ip {} in del list".format(dhcp_relay_ip)) + # Remove dhcp servers if they exist in the DB + if dhcp_relay_ip not in dhcp_servers: + ctx.fail("{} is not a DHCP relay for {}".format(dhcp_relay_ip, vlan_name)) + + dhcp_servers.remove(dhcp_relay_ip) + removed_ips.append(dhcp_relay_ip) + + if len(dhcp_servers) == 0: + del table[dhcp_servers_str] + else: + table[dhcp_servers_str] = dhcp_servers + + db.cfgdb.set_entry(table_name, vlan_name, table) + click.echo("Removed DHCP relay address [{}] from {}".format(",".join(dhcp_relay_ips), vlan_name)) + dhcp_relay_util.handle_restart_dhcp_relay_service() + + +@click.group(cls=clicommon.AbbreviationGroup, name="dhcp_relay") +def dhcp_relay(): + """config DHCP_Relay information""" + pass + + +@dhcp_relay.group(cls=clicommon.AbbreviationGroup, name="ipv6") +def dhcp_relay_ipv6(): + pass + + +@dhcp_relay_ipv6.group(cls=clicommon.AbbreviationGroup, name="destination") +def dhcp_relay_ipv6_destination(): + pass + + +@dhcp_relay_ipv6_destination.command("add") +@click.argument("vid", metavar="", required=True, type=int) +@click.argument("dhcp_relay_destinations", nargs=-1, required=True) +@clicommon.pass_db +def add_dhcp_relay_ipv6_destination(db, vid, dhcp_relay_destinations): + add_dhcp_relay(vid, dhcp_relay_destinations, db, IPV6) + + +@dhcp_relay_ipv6_destination.command("del") +@click.argument("vid", metavar="", required=True, type=int) +@click.argument("dhcp_relay_destinations", nargs=-1, required=True) +@clicommon.pass_db +def del_dhcp_relay_ipv6_destination(db, vid, dhcp_relay_destinations): + del_dhcp_relay(vid, dhcp_relay_destinations, db, IPV6) + + +@dhcp_relay.group(cls=clicommon.AbbreviationGroup, name="ipv4") +def dhcp_relay_ipv4(): + pass + + +@dhcp_relay_ipv4.group(cls=clicommon.AbbreviationGroup, name="helper") +def dhcp_relay_ipv4_helper(): + pass + + +@dhcp_relay_ipv4_helper.command("add") +@click.argument("vid", metavar="", required=True, type=int) +@click.argument("dhcp_relay_helpers", nargs=-1, required=True) +@clicommon.pass_db +def add_dhcp_relay_ipv4_helper(db, vid, dhcp_relay_helpers): + add_dhcp_relay(vid, dhcp_relay_helpers, db, IPV4) + + +@dhcp_relay_ipv4_helper.command("del") +@click.argument("vid", metavar="", required=True, type=int) +@click.argument("dhcp_relay_helpers", nargs=-1, required=True) +@clicommon.pass_db +def del_dhcp_relay_ipv4_helper(db, vid, dhcp_relay_helpers): + del_dhcp_relay(vid, dhcp_relay_helpers, db, IPV4) diff --git a/config/main.py b/config/main.py index 754ca46ae4..bdf911bf03 100644 --- a/config/main.py +++ b/config/main.py @@ -37,6 +37,7 @@ from . import nat from . import vlan from . import vxlan +from . import dhcp_relay from .config_mgmt import ConfigMgmtDPB # Using load_source to 'import /usr/local/bin/sonic-cfggen as sonic_cfggen' @@ -1016,6 +1017,7 @@ def config(ctx): config.add_command(nat.nat) config.add_command(vlan.vlan) config.add_command(vxlan.vxlan) +config.add_command(dhcp_relay.dhcp_relay) @config.command() @click.option('-y', '--yes', is_flag=True, callback=_abort_if_false, diff --git a/config/vlan.py b/config/vlan.py index d6332aa64e..163e1b02cf 100644 --- a/config/vlan.py +++ b/config/vlan.py @@ -248,13 +248,7 @@ def add_vlan_dhcp_relay_destination(db, vid, dhcp_relay_destination_ip): db.cfgdb.set_entry('VLAN', vlan_name, vlan) click.echo("Added DHCP relay destination address {} to {}".format(dhcp_relay_destination_ip, vlan_name)) - try: - click.echo("Restarting DHCP relay service...") - clicommon.run_command("systemctl stop dhcp_relay", display_cmd=False) - clicommon.run_command("systemctl reset-failed dhcp_relay", display_cmd=False) - clicommon.run_command("systemctl start dhcp_relay", display_cmd=False) - except SystemExit as e: - ctx.fail("Restart service dhcp_relay failed with error {}".format(e)) + dhcp_relay_util.handle_restart_dhcp_relay_service() @vlan_dhcp_relay.command('del') @click.argument('vid', metavar='', required=True, type=int) @@ -295,10 +289,4 @@ def del_vlan_dhcp_relay_destination(db, vid, dhcp_relay_destination_ip): db.cfgdb.set_entry('VLAN', vlan_name, vlan) click.echo("Removed DHCP relay destination address {} from {}".format(dhcp_relay_destination_ip, vlan_name)) - try: - click.echo("Restarting DHCP relay service...") - clicommon.run_command("systemctl stop dhcp_relay", display_cmd=False) - clicommon.run_command("systemctl reset-failed dhcp_relay", display_cmd=False) - clicommon.run_command("systemctl start dhcp_relay", display_cmd=False) - except SystemExit as e: - ctx.fail("Restart service dhcp_relay failed with error {}".format(e)) + dhcp_relay_util.handle_restart_dhcp_relay_service() diff --git a/doc/Command-Reference.md b/doc/Command-Reference.md index d384af295e..64bef753dc 100644 --- a/doc/Command-Reference.md +++ b/doc/Command-Reference.md @@ -2249,6 +2249,74 @@ This command is used to delete a configured DHCP Relay Destination IP address fr Restarting DHCP relay service... ``` +**config dhcp_relay ipv4 helper add/del** + +This command is used to add or delete IPv4 DHCP Relay helper addresses to a VLAN. Note that more than one DHCP Relay helper addresses can be operated on a VLAN interface. + +- Usage: + ``` + config dhcp_relay ipv4 helper (add | del) + ``` + +- Example: + ``` + admin@sonic:~$ sudo config dhcp_relay ipv4 helper add 1000 7.7.7.7 + Added DHCP relay address [7.7.7.7] to Vlan1000 + Restarting DHCP relay service... + ``` + + ``` + admin@sonic:~$ sudo config dhcp_relay ipv4 helper add 1000 7.7.7.7 1.1.1.1 + Added DHCP relay address [7.7.7.7, 1.1.1.1] to Vlan1000 + Restarting DHCP relay service... + ``` + + ``` + admin@sonic:~$ sudo config dhcp_relay ipv4 helper del 1000 7.7.7.7 + Removed DHCP relay address [7.7.7.7] from Vlan1000 + Restarting DHCP relay service... + ``` + + ``` + admin@sonic:~$ sudo config dhcp_relay ipv4 helper del 1000 7.7.7.7 1.1.1.1 + Removed DHCP relay address [7.7.7.7, 1.1.1.1] from Vlan1000 + Restarting DHCP relay service... + ``` + +**config dhcp_relay ipv6 destination add/del** + +This command is used to add or del IPv6 DHCP Relay destination addresses to a VLAN. Note that more than one DHCP Relay Destination addresses can be operated on a VLAN interface. + +- Usage: + ``` + config dhcp_relay ipv6 destination (add | del) + ``` + +- Example: + ``` + admin@sonic:~$ sudo config dhcp_relay ipv6 destination add 1000 fc02:2000::1 + Added DHCP relay address [fc02:2000::1] to Vlan1000 + Restarting DHCP relay service... + ``` + + ``` + admin@sonic:~$ sudo config dhcp_relay ipv6 destination add 1000 fc02:2000::1 fc02:2000::2 + Added DHCP relay address [fc02:2000::1, fc02:2000::2] to Vlan1000 + Restarting DHCP relay service... + ``` + + ``` + admin@sonic:~$ sudo config dhcp_relay ipv6 destination del 1000 fc02:2000::1 + Removed DHCP relay address [fc02:2000::1] from Vlan1000 + Restarting DHCP relay service... + ``` + + ``` + admin@sonic:~$ sudo config dhcp_relay ipv6 destination del 1000 fc02:2000::1 fc02:2000::2 + Removed DHCP relay address [fc02:2000::1, fc02:2000::2] from Vlan1000 + Restarting DHCP relay service... + ``` + ### DHCPv6 Relay show commands This sub-section of commands is used to show the DHCPv6 Relay Destination IPv6 address for VLAN interfaces. diff --git a/tests/config_dhcp_relay_test.py b/tests/config_dhcp_relay_test.py new file mode 100644 index 0000000000..b759d91162 --- /dev/null +++ b/tests/config_dhcp_relay_test.py @@ -0,0 +1,259 @@ +import os +import pytest + +import config.main as config +from utilities_common.db import Db +from unittest import mock +from click.testing import CliRunner + +config_dhcp_relay_add_output = """\ +Added DHCP relay address [{}] to Vlan1000 +Restarting DHCP relay service... +""" +config_dhcp_relay_del_output = """\ +Removed DHCP relay address [{}] from Vlan1000 +Restarting DHCP relay service... +""" +expected_dhcp_relay_add_config_db_output = { + "ipv4": { + "dhcp_servers": [ + "192.0.0.1", "192.0.0.2", "192.0.0.3", "192.0.0.4", "192.0.0.5" + ], + "dhcpv6_servers": [ + "fc02:2000::1", "fc02:2000::2" + ], + "vlanid": "1000" + }, + "ipv6": { + "dhcpv6_servers": [ + "fc02:2000::1", "fc02:2000::2", "fc02:2000::3" + ] + } +} +expected_dhcp_relay_del_config_db_output = { + "ipv4": { + "dhcp_servers": [ + "192.0.0.1", "192.0.0.2", "192.0.0.3", "192.0.0.4" + ], + "dhcpv6_servers": [ + "fc02:2000::1", "fc02:2000::2" + ], + "vlanid": "1000" + }, + "ipv6": { + "dhcpv6_servers": [ + "fc02:2000::1", "fc02:2000::2" + ] + } +} +expected_dhcp_relay_add_multi_config_db_output = { + "ipv4": { + "dhcp_servers": [ + "192.0.0.1", "192.0.0.2", "192.0.0.3", "192.0.0.4", "192.0.0.5", "192.0.0.6", "192.0.0.7" + ], + "dhcpv6_servers": [ + "fc02:2000::1", "fc02:2000::2" + ], + "vlanid": "1000" + }, + "ipv6": { + "dhcpv6_servers": [ + "fc02:2000::1", "fc02:2000::2", "fc02:2000::3", "fc02:2000::4", "fc02:2000::5" + ] + } +} + +IP_VER_TEST_PARAM_MAP = { + "ipv4": { + "command": "helper", + "ips": [ + "192.0.0.5", + "192.0.0.6", + "192.0.0.7" + ], + "exist_ip": "192.0.0.1", + "nonexist_ip": "192.0.0.8", + "invalid_ip": "192.0.0", + "table": "VLAN" + }, + "ipv6": { + "command": "destination", + "ips": [ + "fc02:2000::3", + "fc02:2000::4", + "fc02:2000::5" + ], + "exist_ip": "fc02:2000::1", + "nonexist_ip": "fc02:2000::6", + "invalid_ip": "fc02:2000:", + "table": "DHCP_RELAY" + } +} + + +@pytest.fixture(scope="module", params=["ipv4", "ipv6"]) +def ip_version(request): + """ + Parametrize Ip version + Args: + request: pytest request object + Returns: + Ip version needed for test case + """ + return request.param + + +@pytest.fixture(scope="module", params=["add", "del"]) +def op(request): + """ + Parametrize operate tpye + Args: + request: pytest request object + Returns: + Operate tpye + """ + return request.param + + +class TestConfigDhcpRelay(object): + @classmethod + def setup_class(cls): + os.environ['UTILITIES_UNIT_TESTING'] = "1" + print("SETUP") + + def test_config_dhcp_relay_add_del_with_nonexist_vlanid(self, ip_version, op): + runner = CliRunner() + + with mock.patch("utilities_common.cli.run_command") as mock_run_command: + result = runner.invoke(config.config.commands["dhcp_relay"].commands[ip_version] + .commands[IP_VER_TEST_PARAM_MAP[ip_version]["command"]] + .commands[op], ["1001", IP_VER_TEST_PARAM_MAP[ip_version]["ips"][0]]) + print(result.exit_code) + print(result.stdout) + assert result.exit_code != 0 + assert "Error: Vlan1001 doesn't exist" in result.output + assert mock_run_command.call_count == 0 + + def test_config_add_del_dhcp_relay_with_invalid_ip(self, ip_version, op): + runner = CliRunner() + invalid_ip = IP_VER_TEST_PARAM_MAP[ip_version]["invalid_ip"] + + with mock.patch("utilities_common.cli.run_command") as mock_run_command: + result = runner.invoke(config.config.commands["dhcp_relay"].commands[ip_version] + .commands[IP_VER_TEST_PARAM_MAP[ip_version]["command"]] + .commands[op], ["1000", invalid_ip]) + print(result.exit_code) + print(result.output) + assert result.exit_code != 0 + assert "Error: {} is invalid IP address".format(invalid_ip) in result.output + assert mock_run_command.call_count == 0 + + def test_config_add_dhcp_with_exist_ip(self, ip_version): + runner = CliRunner() + db = Db() + exist_ip = IP_VER_TEST_PARAM_MAP[ip_version]["exist_ip"] + + with mock.patch("utilities_common.cli.run_command") as mock_run_command: + result = runner.invoke(config.config.commands["dhcp_relay"].commands[ip_version] + .commands[IP_VER_TEST_PARAM_MAP[ip_version]["command"]] + .commands["add"], ["1000", exist_ip], obj=db) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + assert "{} is already a DHCP relay for Vlan1000".format(exist_ip) in result.output + assert mock_run_command.call_count == 0 + + def test_config_del_nonexist_dhcp_relay(self, ip_version): + runner = CliRunner() + db = Db() + nonexist_ip = IP_VER_TEST_PARAM_MAP[ip_version]["nonexist_ip"] + + with mock.patch("utilities_common.cli.run_command") as mock_run_command: + result = runner.invoke(config.config.commands["dhcp_relay"].commands[ip_version] + .commands[IP_VER_TEST_PARAM_MAP[ip_version]["command"]] + .commands["del"], ["1000", nonexist_ip], obj=db) + print(result.exit_code) + print(result.output) + assert result.exit_code != 0 + assert "Error: {} is not a DHCP relay for Vlan1000".format(nonexist_ip) in result.output + assert mock_run_command.call_count == 0 + + def test_config_add_del_dhcp_relay(self, ip_version): + runner = CliRunner() + db = Db() + test_ip = IP_VER_TEST_PARAM_MAP[ip_version]["ips"][0] + config_db_table = IP_VER_TEST_PARAM_MAP[ip_version]["table"] + + with mock.patch("utilities_common.cli.run_command") as mock_run_command: + # add new dhcp relay + result = runner.invoke(config.config.commands["dhcp_relay"].commands[ip_version] + .commands[IP_VER_TEST_PARAM_MAP[ip_version]["command"]] + .commands["add"], ["1000", test_ip], obj=db) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + assert result.output == config_dhcp_relay_add_output.format(test_ip) + assert db.cfgdb.get_entry(config_db_table, "Vlan1000") \ + == expected_dhcp_relay_add_config_db_output[ip_version] + assert mock_run_command.call_count == 3 + + # del dhcp relay + with mock.patch("utilities_common.cli.run_command") as mock_run_command: + result = runner.invoke(config.config.commands["dhcp_relay"].commands[ip_version] + .commands[IP_VER_TEST_PARAM_MAP[ip_version]["command"]] + .commands["del"], ["1000", test_ip], obj=db) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + assert result.output == config_dhcp_relay_del_output.format(test_ip) + assert mock_run_command.call_count == 3 + assert db.cfgdb.get_entry(config_db_table, "Vlan1000") \ + == expected_dhcp_relay_del_config_db_output[ip_version] + + def test_config_add_del_multiple_dhcp_relay(self, ip_version): + runner = CliRunner() + db = Db() + test_ips = IP_VER_TEST_PARAM_MAP[ip_version]["ips"] + config_db_table = IP_VER_TEST_PARAM_MAP[ip_version]["table"] + + with mock.patch("utilities_common.cli.run_command") as mock_run_command: + # add new dhcp relay + result = runner.invoke(config.config.commands["dhcp_relay"].commands[ip_version] + .commands[IP_VER_TEST_PARAM_MAP[ip_version]["command"]] + .commands["add"], ["1000"] + test_ips, obj=db) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + assert result.output == config_dhcp_relay_add_output.format(",".join(test_ips)) + assert db.cfgdb.get_entry(config_db_table, "Vlan1000") \ + == expected_dhcp_relay_add_multi_config_db_output[ip_version] + assert mock_run_command.call_count == 3 + + # del dhcp relay + with mock.patch("utilities_common.cli.run_command") as mock_run_command: + result = runner.invoke(config.config.commands["dhcp_relay"].commands[ip_version] + .commands[IP_VER_TEST_PARAM_MAP[ip_version]["command"]] + .commands["del"], ["1000"] + test_ips, obj=db) + print(result.exit_code) + print(result.output) + assert result.exit_code == 0 + assert result.output == config_dhcp_relay_del_output.format(",".join(test_ips)) + assert mock_run_command.call_count == 3 + assert db.cfgdb.get_entry(config_db_table, "Vlan1000") \ + == expected_dhcp_relay_del_config_db_output[ip_version] + + def test_config_add_del_duplicate_dhcp_relay(self, ip_version, op): + runner = CliRunner() + db = Db() + test_ip = IP_VER_TEST_PARAM_MAP[ip_version]["ips"][0] if op == "add" \ + else IP_VER_TEST_PARAM_MAP[ip_version]["exist_ip"] + + with mock.patch("utilities_common.cli.run_command") as mock_run_command: + result = runner.invoke(config.config.commands["dhcp_relay"].commands[ip_version] + .commands[IP_VER_TEST_PARAM_MAP[ip_version]["command"]] + .commands[op], ["1000", test_ip, test_ip], obj=db) + print(result.exit_code) + print(result.output) + assert result.exit_code != 0 + assert "Error: Find duplicate DHCP relay ip {} in {} list".format(test_ip, op) in result.output + assert mock_run_command.call_count == 0