Skip to content

Commit

Permalink
[dhcp_server] Add dhcprelayd for dhcp_server feature (sonic-net#16947)
Browse files Browse the repository at this point in the history
Add support in dhcp_relay container for dhcp_server_ipv4 feature. HLD: sonic-net/SONiC#1282
  • Loading branch information
yaqiangz authored Nov 2, 2023
1 parent c85c12b commit 274d320
Show file tree
Hide file tree
Showing 38 changed files with 1,219 additions and 84 deletions.
20 changes: 20 additions & 0 deletions dockers/docker-dhcp-relay/Dockerfile.j2
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ ENV IMAGE_VERSION=$image_version
# Update apt's cache of available packages
RUN apt-get update

RUN apt-get install -y libjsoncpp-dev {%- if INCLUDE_DHCP_SERVER == "y" %}\
python3-dev \
build-essential{%- endif %}

{% if INCLUDE_DHCP_SERVER == "y" -%}
RUN pip3 install psutil
{%- endif %}

RUN apt-get install -y libjsoncpp-dev

{% if docker_dhcp_relay_debs.strip() -%}
Expand All @@ -23,7 +31,19 @@ RUN apt-get install -y libjsoncpp-dev
{{ install_debian_packages(docker_dhcp_relay_debs.split(' ')) }}
{%- endif %}

{% if docker_dhcp_relay_whls.strip() %}
# Copy locally-built Python wheel dependencies
{{ copy_files("python-wheels/", docker_dhcp_relay_whls.split(' '), "/python-wheels/") }}

# Install locally-built Python wheel dependencies
{{ install_python_wheels(docker_dhcp_relay_whls.split(' ')) }}
{% endif %}

# Clean up
{% if INCLUDE_DHCP_SERVER == "y" -%}
RUN apt-get remove -y build-essential \
python3-dev
{%- endif %}
RUN apt-get clean -y && \
apt-get autoclean -y && \
apt-get autoremove -y && \
Expand Down
40 changes: 40 additions & 0 deletions dockers/docker-dhcp-relay/cli-plugin-tests/mock_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,46 @@
}
}
}
],
[
"ipv4_with_disabled_dhcp_server_with_header",
{
"config_db": {
"VLAN": {
"Vlan1000": {
"dhcp_servers": [
"192.0.0.1",
"192.0.0.2"
]
}
},
"FEATURE": {
"dhcp_server": {
"state": "disabled"
}
}
}
}
],
[
"ipv4_with_enabled_dhcp_server_with_header",
{
"config_db": {
"VLAN": {
"Vlan1000": {
"dhcp_servers": [
"192.0.0.1",
"192.0.0.2"
]
}
},
"FEATURE": {
"dhcp_server": {
"state": "enabled"
}
}
}
}
]
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,36 @@ def test_config_add_del_dhcp_relay(self, mock_cfgdb, ip_version):
db.cfgdb.set_entry.assert_called_once_with(config_db_table, "Vlan1000",
expected_dhcp_relay_del_config_db_output[ip_version])

def test_config_add_del_dhcp_relay_with_enable_dhcp_server(self, mock_cfgdb):
runner = CliRunner()
db = Db()
db.cfgdb = mock_cfgdb
ip_version = "ipv4"
test_ip = IP_VER_TEST_PARAM_MAP[ip_version]["ips"][0]

with mock.patch("utilities_common.cli.run_command"), \
mock.patch.object(dhcp_relay, "is_dhcp_server_enabled", return_value=True):
# add new dhcp relay
result = runner.invoke(dhcp_relay.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 "Cannot change ipv4 dhcp_relay configuration when dhcp_server feature is enabled" in result.output

db.cfgdb.set_entry.reset_mock()
# del dhcp relay
with mock.patch("utilities_common.cli.run_command"), \
mock.patch.object(dhcp_relay, "is_dhcp_server_enabled", return_value=True):
result = runner.invoke(dhcp_relay.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 "Cannot change ipv4 dhcp_relay configuration when dhcp_server feature is enabled" in result.output

def test_config_add_del_multiple_dhcp_relay(self, mock_cfgdb, ip_version):
runner = CliRunner()
db = Db()
Expand Down
54 changes: 50 additions & 4 deletions dockers/docker-dhcp-relay/cli-plugin-tests/test_show_dhcp_relay.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pytest
import sys
import click
import os
sys.path.append('../cli/show/plugins/')
import show_dhcp_relay as show
Expand Down Expand Up @@ -35,6 +36,14 @@
+-------------+----------------------+
"""

expected_ipv4_table_with_enabled_dhcp_server_with_header = """\
+-------------+----------------------+
| Interface | DHCP Relay Address |
+=============+======================+
| Vlan1000 | N/A |
+-------------+----------------------+
"""

expected_ipv6_table_without_header = """\
-------- ------------
Vlan1000 fc02:2000::1
Expand Down Expand Up @@ -86,12 +95,18 @@ def test_plugin_registration():
assert 'DHCP Helper Address' in dict(vlan.VlanBrief.COLUMNS)


def test_dhcp_relay_column_output():
@pytest.mark.parametrize("feature_table", [{}, {"dhcp_server": {"state": "disabled"}},
{"dhcp_server": {"state": "enabled"}}, {"dhcp_server": {}}])
def test_dhcp_relay_column_output(feature_table):
ctx = (
({'Vlan1001': {'dhcp_servers': ['192.0.0.1', '192.168.0.2']}}, {}, {}),
(),
(MockDb({"FEATURE": feature_table})),
)
assert show.get_dhcp_helper_address(ctx, 'Vlan1001') == '192.0.0.1\n192.168.0.2'
if "dhcp_server" in feature_table and "state" in feature_table["dhcp_server"] and \
feature_table["dhcp_server"]["state"] == "enabled":
assert show.get_dhcp_helper_address(ctx, 'Vlan1001') == 'N/A'
else:
assert show.get_dhcp_helper_address(ctx, 'Vlan1001') == '192.0.0.1\n192.168.0.2'


@parameterized.expand(COMMON_TEST_DATA)
Expand All @@ -103,7 +118,7 @@ def test_show_dhcp_relay(test_name, test_data, fs):
config_db = MockConfigDb()
ip_version = "ipv4" if "ipv4" in test_name else "ipv6"
table = config_db.get_table(IP_VER_TEST_PARAM_MAP[ip_version]["table"])
if test_name == "ipv4_with_header":
if test_name in ["ipv4_with_header", "ipv4_with_disabled_dhcp_server_with_header"]:
result = show.get_dhcp_relay_data_with_header(table, IP_VER_TEST_PARAM_MAP[ip_version]["entry"])
expected_output = expected_ipv4_table_with_header
elif test_name == "ipv6_with_header":
Expand All @@ -112,6 +127,9 @@ def test_show_dhcp_relay(test_name, test_data, fs):
elif test_name == "ipv6_without_header":
result = show.get_data(table, "Vlan1000")
expected_output = expected_ipv6_table_without_header
elif test_name == "ipv4_with_enabled_dhcp_server_with_header":
result = show.get_dhcp_relay_data_with_header(table, IP_VER_TEST_PARAM_MAP[ip_version]["entry"], True)
expected_output = expected_ipv4_table_with_enabled_dhcp_server_with_header
assert result == expected_output


Expand Down Expand Up @@ -153,3 +171,31 @@ def test_show_multi_dhcp_relay(test_name, test_data, fs):
else:
expected_output = expected_ipv6_table_multi_with_header
assert result == expected_output


def test_show_dhcp_relay_ipv4_counter_with_enabled_dhcp_server():
with mock.patch.object(show, "is_dhcp_server_enabled", return_value=True), \
mock.patch.object(swsscommon.ConfigDBConnector, "connect", return_value=None), \
mock.patch.object(swsscommon.ConfigDBConnector, "get_table", return_value=None), \
mock.patch.object(click, "echo", return_value=None) as mock_echo:
show.ipv4_counters("Etherner1")
expected_param = "Unsupport to check dhcp_relay ipv4 counter when dhcp_server feature is enabled"
mock_echo.assert_called_once_with(expected_param)


@pytest.mark.parametrize("enable_dhcp_server", [True, False])
def test_is_dhcp_server_enabled(enable_dhcp_server):
result = show.is_dhcp_server_enabled({"dhcp_server": {"state": "enabled" if enable_dhcp_server else "disabled"}})
assert result == enable_dhcp_server


class MockDb(object):
class MockCfgDb(object):
def __init__(self, mock_cfgdb):
self.mock_cfgdb = mock_cfgdb

def get_table(self, table_name):
return self.mock_cfgdb.get(table_name, {})

def __init__(self, mock_cfgdb):
self.cfgdb = self.MockCfgDb(mock_cfgdb)
17 changes: 17 additions & 0 deletions dockers/docker-dhcp-relay/cli/config/plugins/dhcp_relay.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ def del_dhcp_relay(vid, dhcp_relay_ips, db, ip_version):
ctx.fail("Restart service dhcp_relay failed with error {}".format(e))


def is_dhcp_server_enabled(db):
dhcp_server_feature_entry = db.cfgdb.get_entry("FEATURE", "dhcp_server")
return "state" in dhcp_server_feature_entry and dhcp_server_feature_entry["state"] == "enabled"


@click.group(cls=clicommon.AbbreviationGroup, name="dhcp_relay")
def dhcp_relay():
"""config DHCP_Relay information"""
Expand Down Expand Up @@ -163,6 +168,9 @@ def dhcp_relay_ipv4_helper():
@click.argument("dhcp_relay_helpers", nargs=-1, required=True)
@clicommon.pass_db
def add_dhcp_relay_ipv4_helper(db, vid, dhcp_relay_helpers):
if is_dhcp_server_enabled(db):
click.echo("Cannot change ipv4 dhcp_relay configuration when dhcp_server feature is enabled")
return
add_dhcp_relay(vid, dhcp_relay_helpers, db, IPV4)


Expand All @@ -171,6 +179,9 @@ def add_dhcp_relay_ipv4_helper(db, vid, dhcp_relay_helpers):
@click.argument("dhcp_relay_helpers", nargs=-1, required=True)
@clicommon.pass_db
def del_dhcp_relay_ipv4_helper(db, vid, dhcp_relay_helpers):
if is_dhcp_server_enabled(db):
click.echo("Cannot change ipv4 dhcp_relay configuration when dhcp_server feature is enabled")
return
del_dhcp_relay(vid, dhcp_relay_helpers, db, IPV4)


Expand Down Expand Up @@ -207,6 +218,9 @@ def add_vlan_dhcp_relay_destination(db, vid, dhcp_relay_destination_ips):
click.echo("{} is already a DHCP relay destination for {}".format(ip_addr, vlan_name))
continue
if clicommon.ipaddress_type(ip_addr) == 4:
if is_dhcp_server_enabled(db):
click.echo("Cannot change dhcp_relay configuration when dhcp_server feature is enabled")
return
dhcp_servers.append(ip_addr)
else:
dhcpv6_servers.append(ip_addr)
Expand Down Expand Up @@ -253,6 +267,9 @@ def del_vlan_dhcp_relay_destination(db, vid, dhcp_relay_destination_ips):
if (ip_addr not in dhcp_servers) and (ip_addr not in dhcpv6_servers):
ctx.fail("{} is not a DHCP relay destination for {}".format(ip_addr, vlan_name))
if clicommon.ipaddress_type(ip_addr) == 4:
if is_dhcp_server_enabled(db):
click.echo("Cannot change dhcp_relay configuration when dhcp_server feature is enabled")
return
dhcp_servers.remove(ip_addr)
else:
dhcpv6_servers.remove(ip_addr)
Expand Down
34 changes: 28 additions & 6 deletions dockers/docker-dhcp-relay/cli/show/plugins/show_dhcp_relay.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,15 @@


def get_dhcp_helper_address(ctx, vlan):
cfg, _ = ctx
cfg, db = ctx
vlan_dhcp_helper_data, _, _ = cfg
vlan_config = vlan_dhcp_helper_data.get(vlan)
if not vlan_config:
return ""

dhcp_helpers = vlan_config.get('dhcp_servers', [])
feature_data = db.cfgdb.get_table("FEATURE")
dhcp_server_enabled = is_dhcp_server_enabled(feature_data)
dhcp_helpers = ["N/A"] if dhcp_server_enabled else vlan_config.get('dhcp_servers', [])

return '\n'.join(natsorted(dhcp_helpers))

Expand Down Expand Up @@ -96,6 +98,11 @@ def dhcp4relay_counters():


def ipv4_counters(interface):
config_db.connect()
feature_tbl = config_db.get_table("FEATURE")
if is_dhcp_server_enabled(feature_tbl):
click.echo("Unsupport to check dhcp_relay ipv4 counter when dhcp_server feature is enabled")
return
counter = DHCPv4_Counter()
counter_intf = counter.get_interface()

Expand Down Expand Up @@ -193,7 +200,7 @@ def dhcp_relay_helper():
pass


def get_dhcp_relay_data_with_header(table_data, entry_name):
def get_dhcp_relay_data_with_header(table_data, entry_name, dhcp_server_enabled=False):
vlan_relay = {}
vlans = table_data.keys()
for vlan in vlans:
Expand All @@ -203,15 +210,25 @@ def get_dhcp_relay_data_with_header(table_data, entry_name):
continue

vlan_relay[vlan] = []
for address in dhcp_relay_data:
vlan_relay[vlan].append(address)
if dhcp_server_enabled:
vlan_relay[vlan].append("N/A")
else:
for address in dhcp_relay_data:
vlan_relay[vlan].append(address)

dhcp_relay_vlan_keys = vlan_relay.keys()
relay_address_list = ["\n".join(vlan_relay[key]) for key in dhcp_relay_vlan_keys]
data = {"Interface": dhcp_relay_vlan_keys, "DHCP Relay Address": relay_address_list}
return tabulate(data, tablefmt='grid', stralign='right', headers='keys') + '\n'


def is_dhcp_server_enabled(feature_tbl):
if feature_tbl is not None and "dhcp_server" in feature_tbl and "state" in feature_tbl["dhcp_server"] and \
feature_tbl["dhcp_server"]["state"] == "enabled":
return True
return False


def get_dhcp_relay(table_name, entry_name, with_header):
if config_db is None:
return
Expand All @@ -221,8 +238,13 @@ def get_dhcp_relay(table_name, entry_name, with_header):
if table_data is None:
return

dhcp_server_enabled = False
if table_name == VLAN:
feature_tbl = config_db.get_table("FEATURE")
dhcp_server_enabled = is_dhcp_server_enabled(feature_tbl)

if with_header:
output = get_dhcp_relay_data_with_header(table_data, entry_name)
output = get_dhcp_relay_data_with_header(table_data, entry_name, dhcp_server_enabled)
print(output)
else:
vlans = config_db.get_keys(table_name)
Expand Down
6 changes: 5 additions & 1 deletion dockers/docker-dhcp-relay/dhcp-relay.programs.j2
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
programs=
{%- set relay_for_ipv6 = { 'flag': False } %}
{%- set add_preceding_comma = { 'flag': False } %}
{% if dhcp_server_ipv4_enabled %}
{% set _dummy = add_preceding_comma.update({'flag': True}) %}
dhcprelayd
{%- endif %}
{% for vlan_name in VLAN_INTERFACE %}
{# Append DHCPv4 agents #}
{% if VLAN and vlan_name in VLAN and 'dhcp_servers' in VLAN[vlan_name] and VLAN[vlan_name]['dhcp_servers']|length > 0 %}
{% if not dhcp_server_ipv4_enabled and VLAN and vlan_name in VLAN and 'dhcp_servers' in VLAN[vlan_name] and VLAN[vlan_name]['dhcp_servers']|length > 0 %}
{% if add_preceding_comma.flag %},{% endif %}
{% set _dummy = add_preceding_comma.update({'flag': True}) %}
isc-dhcpv4-relay-{{ vlan_name }}
Expand Down
Loading

0 comments on commit 274d320

Please sign in to comment.