[dhcp_server] Add dhcprelayd for dhcp_server feature (#16947)

Add support in dhcp_relay container for dhcp_server_ipv4 feature. HLD: sonic-net/SONiC#1282
This commit is contained in:
Yaqiang Zhu 2023-11-02 23:09:01 +08:00 committed by GitHub
parent c85c12bc75
commit 274d320443
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1219 additions and 84 deletions

View File

@ -13,6 +13,14 @@ ENV IMAGE_VERSION=$image_version
# Update apt's cache of available packages # Update apt's cache of available packages
RUN apt-get update 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 RUN apt-get install -y libjsoncpp-dev
{% if docker_dhcp_relay_debs.strip() -%} {% if docker_dhcp_relay_debs.strip() -%}
@ -23,7 +31,19 @@ RUN apt-get install -y libjsoncpp-dev
{{ install_debian_packages(docker_dhcp_relay_debs.split(' ')) }} {{ install_debian_packages(docker_dhcp_relay_debs.split(' ')) }}
{%- endif %} {%- 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 # Clean up
{% if INCLUDE_DHCP_SERVER == "y" -%}
RUN apt-get remove -y build-essential \
python3-dev
{%- endif %}
RUN apt-get clean -y && \ RUN apt-get clean -y && \
apt-get autoclean -y && \ apt-get autoclean -y && \
apt-get autoremove -y && \ apt-get autoremove -y && \

View File

@ -45,6 +45,46 @@ COMMON_TEST_DATA = [
} }
} }
} }
],
[
"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"
}
}
}
}
] ]
] ]

View File

@ -224,6 +224,36 @@ class TestConfigDhcpRelay(object):
db.cfgdb.set_entry.assert_called_once_with(config_db_table, "Vlan1000", db.cfgdb.set_entry.assert_called_once_with(config_db_table, "Vlan1000",
expected_dhcp_relay_del_config_db_output[ip_version]) 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): def test_config_add_del_multiple_dhcp_relay(self, mock_cfgdb, ip_version):
runner = CliRunner() runner = CliRunner()
db = Db() db = Db()

View File

@ -1,5 +1,6 @@
import pytest import pytest
import sys import sys
import click
import os import os
sys.path.append('../cli/show/plugins/') sys.path.append('../cli/show/plugins/')
import show_dhcp_relay as show import show_dhcp_relay as show
@ -35,6 +36,14 @@ expected_ipv4_table_with_header = """\
+-------------+----------------------+ +-------------+----------------------+
""" """
expected_ipv4_table_with_enabled_dhcp_server_with_header = """\
+-------------+----------------------+
| Interface | DHCP Relay Address |
+=============+======================+
| Vlan1000 | N/A |
+-------------+----------------------+
"""
expected_ipv6_table_without_header = """\ expected_ipv6_table_without_header = """\
-------- ------------ -------- ------------
Vlan1000 fc02:2000::1 Vlan1000 fc02:2000::1
@ -86,11 +95,17 @@ def test_plugin_registration():
assert 'DHCP Helper Address' in dict(vlan.VlanBrief.COLUMNS) 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 = ( ctx = (
({'Vlan1001': {'dhcp_servers': ['192.0.0.1', '192.168.0.2']}}, {}, {}), ({'Vlan1001': {'dhcp_servers': ['192.0.0.1', '192.168.0.2']}}, {}, {}),
(), (MockDb({"FEATURE": feature_table})),
) )
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' assert show.get_dhcp_helper_address(ctx, 'Vlan1001') == '192.0.0.1\n192.168.0.2'
@ -103,7 +118,7 @@ def test_show_dhcp_relay(test_name, test_data, fs):
config_db = MockConfigDb() config_db = MockConfigDb()
ip_version = "ipv4" if "ipv4" in test_name else "ipv6" ip_version = "ipv4" if "ipv4" in test_name else "ipv6"
table = config_db.get_table(IP_VER_TEST_PARAM_MAP[ip_version]["table"]) 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"]) result = show.get_dhcp_relay_data_with_header(table, IP_VER_TEST_PARAM_MAP[ip_version]["entry"])
expected_output = expected_ipv4_table_with_header expected_output = expected_ipv4_table_with_header
elif test_name == "ipv6_with_header": elif test_name == "ipv6_with_header":
@ -112,6 +127,9 @@ def test_show_dhcp_relay(test_name, test_data, fs):
elif test_name == "ipv6_without_header": elif test_name == "ipv6_without_header":
result = show.get_data(table, "Vlan1000") result = show.get_data(table, "Vlan1000")
expected_output = expected_ipv6_table_without_header 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 assert result == expected_output
@ -153,3 +171,31 @@ def test_show_multi_dhcp_relay(test_name, test_data, fs):
else: else:
expected_output = expected_ipv6_table_multi_with_header expected_output = expected_ipv6_table_multi_with_header
assert result == expected_output 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)

View File

@ -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)) 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") @click.group(cls=clicommon.AbbreviationGroup, name="dhcp_relay")
def dhcp_relay(): def dhcp_relay():
"""config DHCP_Relay information""" """config DHCP_Relay information"""
@ -163,6 +168,9 @@ def dhcp_relay_ipv4_helper():
@click.argument("dhcp_relay_helpers", nargs=-1, required=True) @click.argument("dhcp_relay_helpers", nargs=-1, required=True)
@clicommon.pass_db @clicommon.pass_db
def add_dhcp_relay_ipv4_helper(db, vid, dhcp_relay_helpers): 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) add_dhcp_relay(vid, dhcp_relay_helpers, db, IPV4)
@ -171,6 +179,9 @@ def add_dhcp_relay_ipv4_helper(db, vid, dhcp_relay_helpers):
@click.argument("dhcp_relay_helpers", nargs=-1, required=True) @click.argument("dhcp_relay_helpers", nargs=-1, required=True)
@clicommon.pass_db @clicommon.pass_db
def del_dhcp_relay_ipv4_helper(db, vid, dhcp_relay_helpers): 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) del_dhcp_relay(vid, dhcp_relay_helpers, db, IPV4)
@ -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)) click.echo("{} is already a DHCP relay destination for {}".format(ip_addr, vlan_name))
continue continue
if clicommon.ipaddress_type(ip_addr) == 4: 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) dhcp_servers.append(ip_addr)
else: else:
dhcpv6_servers.append(ip_addr) dhcpv6_servers.append(ip_addr)
@ -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): 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)) ctx.fail("{} is not a DHCP relay destination for {}".format(ip_addr, vlan_name))
if clicommon.ipaddress_type(ip_addr) == 4: 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) dhcp_servers.remove(ip_addr)
else: else:
dhcpv6_servers.remove(ip_addr) dhcpv6_servers.remove(ip_addr)

View File

@ -32,13 +32,15 @@ config_db = ConfigDBConnector()
def get_dhcp_helper_address(ctx, vlan): def get_dhcp_helper_address(ctx, vlan):
cfg, _ = ctx cfg, db = ctx
vlan_dhcp_helper_data, _, _ = cfg vlan_dhcp_helper_data, _, _ = cfg
vlan_config = vlan_dhcp_helper_data.get(vlan) vlan_config = vlan_dhcp_helper_data.get(vlan)
if not vlan_config: if not vlan_config:
return "" 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)) return '\n'.join(natsorted(dhcp_helpers))
@ -96,6 +98,11 @@ def dhcp4relay_counters():
def ipv4_counters(interface): 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 = DHCPv4_Counter()
counter_intf = counter.get_interface() counter_intf = counter.get_interface()
@ -193,7 +200,7 @@ def dhcp_relay_helper():
pass 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 = {} vlan_relay = {}
vlans = table_data.keys() vlans = table_data.keys()
for vlan in vlans: for vlan in vlans:
@ -203,6 +210,9 @@ def get_dhcp_relay_data_with_header(table_data, entry_name):
continue continue
vlan_relay[vlan] = [] vlan_relay[vlan] = []
if dhcp_server_enabled:
vlan_relay[vlan].append("N/A")
else:
for address in dhcp_relay_data: for address in dhcp_relay_data:
vlan_relay[vlan].append(address) vlan_relay[vlan].append(address)
@ -212,6 +222,13 @@ def get_dhcp_relay_data_with_header(table_data, entry_name):
return tabulate(data, tablefmt='grid', stralign='right', headers='keys') + '\n' 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): def get_dhcp_relay(table_name, entry_name, with_header):
if config_db is None: if config_db is None:
return return
@ -221,8 +238,13 @@ def get_dhcp_relay(table_name, entry_name, with_header):
if table_data is None: if table_data is None:
return 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: 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) print(output)
else: else:
vlans = config_db.get_keys(table_name) vlans = config_db.get_keys(table_name)

View File

@ -2,9 +2,13 @@
programs= programs=
{%- set relay_for_ipv6 = { 'flag': False } %} {%- set relay_for_ipv6 = { 'flag': False } %}
{%- set add_preceding_comma = { '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 %} {% for vlan_name in VLAN_INTERFACE %}
{# Append DHCPv4 agents #} {# 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 %} {% if add_preceding_comma.flag %},{% endif %}
{% set _dummy = add_preceding_comma.update({'flag': True}) %} {% set _dummy = add_preceding_comma.update({'flag': True}) %}
isc-dhcpv4-relay-{{ vlan_name }} isc-dhcpv4-relay-{{ vlan_name }}

View File

@ -39,6 +39,10 @@ stderr_logfile=syslog
dependent_startup=true dependent_startup=true
dependent_startup_wait_for=rsyslogd:running dependent_startup_wait_for=rsyslogd:running
{% set dhcp_server_ipv4_enabled = False %}
{% if FEATURE and 'dhcp_server' in FEATURE and 'state' in FEATURE['dhcp_server'] and FEATURE['dhcp_server']['state'] == 'enabled' %}
{% set dhcp_server_ipv4_enabled = True %}
{% endif %}
{# If our configuration has VLANs... #} {# If our configuration has VLANs... #}
{% if VLAN_INTERFACE %} {% if VLAN_INTERFACE %}
{# Count how many VLANs require a DHCP relay agent... #} {# Count how many VLANs require a DHCP relay agent... #}
@ -56,15 +60,28 @@ dependent_startup_wait_for=rsyslogd:running
{% if ipv4_num_relays.count > 0 or ipv6_num_relays.count > 0 %} {% if ipv4_num_relays.count > 0 or ipv6_num_relays.count > 0 %}
{% include 'dhcp-relay.programs.j2' %} {% include 'dhcp-relay.programs.j2' %}
{# Create a program entry for each DHCP relay agent instance #} {# Create a program entry for each DHCP relay agent instance #}
{% set relay_for_ipv4 = { 'flag': False } %} {% set relay_for_ipv4 = { 'flag': False } %}
{% set relay_for_ipv6 = { 'flag': False } %} {% set relay_for_ipv6 = { 'flag': False } %}
{% if not dhcp_server_ipv4_enabled %}
{% for vlan_name in VLAN_INTERFACE %} {% for vlan_name in VLAN_INTERFACE %}
{% include 'dhcpv4-relay.agents.j2' %} {% include 'dhcpv4-relay.agents.j2' %}
{% endfor %} {% endfor %}
{% endif %}
{% include 'dhcpv6-relay.agents.j2' %} {% include 'dhcpv6-relay.agents.j2' %}
{% if not dhcp_server_ipv4_enabled %}
{% include 'dhcp-relay.monitors.j2' %} {% include 'dhcp-relay.monitors.j2' %}
{% else %}
[program:dhcprelayd]
command=/usr/local/bin/dhcprelayd
priority=3
autostart=false
autorestart=false
stdout_logfile=syslog
stderr_logfile=syslog
dependent_startup=true
dependent_startup_wait_for=start:exited
{% endif %}
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

@ -3,3 +3,6 @@
{% for port, config in PORT.items() %} {% for port, config in PORT.items() %}
{{- port }} {% if "alias" in config %}{{ config["alias"] }}{% else %}{{ port }}{% endif %} {{- "\n" -}} {{- port }} {% if "alias" in config %}{{ config["alias"] }}{% else %}{{ port }}{% endif %} {{- "\n" -}}
{% endfor -%} {% endfor -%}
{% for pc, config in PORTCHANNEL.items() %}
{{- pc }} {{ pc }} {{- "\n" -}}
{% endfor -%}

View File

@ -60,4 +60,4 @@ autorestart=false
stdout_logfile=syslog stdout_logfile=syslog
stderr_logfile=syslog stderr_logfile=syslog
dependent_startup=true dependent_startup=true
dependent_startup_wait_for=start:exited dependent_startup_wait_for=dhcpservd:running

View File

@ -17,6 +17,9 @@ $(DOCKER_DHCP_RELAY)_LOAD_DOCKERS = $(DOCKER_CONFIG_ENGINE_BULLSEYE)
$(DOCKER_DHCP_RELAY)_INSTALL_PYTHON_WHEELS = $(SONIC_UTILITIES_PY3) $(DOCKER_DHCP_RELAY)_INSTALL_PYTHON_WHEELS = $(SONIC_UTILITIES_PY3)
$(DOCKER_DHCP_RELAY)_INSTALL_DEBS = $(PYTHON3_SWSSCOMMON) $(DOCKER_DHCP_RELAY)_INSTALL_DEBS = $(PYTHON3_SWSSCOMMON)
ifeq ($(INCLUDE_DHCP_SERVER), y)
$(DOCKER_DHCP_RELAY)_PYTHON_WHEELS += $(SONIC_DHCP_SERVER_PY3)
endif
$(DOCKER_DHCP_RELAY)_VERSION = 1.0.0 $(DOCKER_DHCP_RELAY)_VERSION = 1.0.0
$(DOCKER_DHCP_RELAY)_PACKAGE_NAME = dhcp-relay $(DOCKER_DHCP_RELAY)_PACKAGE_NAME = dhcp-relay

View File

@ -0,0 +1,7 @@
{
"FEATURE": {
"dhcp_server": {
"state": "enabled"
}
}
}

View File

@ -1,5 +1,10 @@
{ {
"VLAN_INTERFACE": { "VLAN_INTERFACE": {
"Vlan1000|fc02:2000::2/24": {} "Vlan1000|fc02:2000::2/24": {}
},
"FEATURE": {
"dhcp_server": {
"state": "disabled"
}
} }
} }

View File

@ -0,0 +1 @@
../py3/docker-dhcp-relay-enable-dhcp-server.supervisord.conf

View File

@ -42,7 +42,6 @@ dependent_startup_wait_for=rsyslogd:running
[group:dhcp-relay] [group:dhcp-relay]
programs=isc-dhcpv4-relay-Vlan1000,dhcp6relay programs=isc-dhcpv4-relay-Vlan1000,dhcp6relay
[program:isc-dhcpv4-relay-Vlan1000] [program:isc-dhcpv4-relay-Vlan1000]
command=/usr/sbin/dhcrelay -d -m discard -a %%h:%%p %%P --name-alias-map-file /tmp/port-name-alias-map.txt -id Vlan1000 -iu Vlan2000 -iu PortChannel02 -iu PortChannel03 -iu PortChannel04 -iu PortChannel01 192.0.0.1 192.0.0.2 command=/usr/sbin/dhcrelay -d -m discard -a %%h:%%p %%P --name-alias-map-file /tmp/port-name-alias-map.txt -id Vlan1000 -iu Vlan2000 -iu PortChannel02 -iu PortChannel03 -iu PortChannel04 -iu PortChannel01 192.0.0.1 192.0.0.2
priority=3 priority=3

View File

@ -42,7 +42,6 @@ dependent_startup_wait_for=rsyslogd:running
[group:dhcp-relay] [group:dhcp-relay]
programs=isc-dhcpv4-relay-Vlan1000,isc-dhcpv4-relay-Vlan2000,dhcp6relay programs=isc-dhcpv4-relay-Vlan1000,isc-dhcpv4-relay-Vlan2000,dhcp6relay
[program:isc-dhcpv4-relay-Vlan1000] [program:isc-dhcpv4-relay-Vlan1000]
command=/usr/sbin/dhcrelay -d -m discard -a %%h:%%p %%P --name-alias-map-file /tmp/port-name-alias-map.txt -id Vlan1000 -iu Vlan2000 -iu PortChannel02 -iu PortChannel03 -iu PortChannel04 -iu PortChannel01 192.0.0.1 192.0.0.2 command=/usr/sbin/dhcrelay -d -m discard -a %%h:%%p %%P --name-alias-map-file /tmp/port-name-alias-map.txt -id Vlan1000 -iu Vlan2000 -iu PortChannel02 -iu PortChannel03 -iu PortChannel04 -iu PortChannel01 192.0.0.1 192.0.0.2
priority=3 priority=3

View File

@ -0,0 +1,64 @@
[supervisord]
logfile_maxbytes=1MB
logfile_backups=2
nodaemon=true
[eventlistener:dependent-startup]
command=python3 -m supervisord_dependent_startup
autostart=true
autorestart=unexpected
startretries=0
exitcodes=0,3
events=PROCESS_STATE
buffer_size=1024
[eventlistener:supervisor-proc-exit-listener]
command=/usr/bin/supervisor-proc-exit-listener --container-name dhcp_relay
events=PROCESS_STATE_EXITED,PROCESS_STATE_RUNNING
autostart=true
autorestart=unexpected
buffer_size=1024
[program:rsyslogd]
command=/usr/sbin/rsyslogd -n -iNONE
priority=1
autostart=false
autorestart=false
stdout_logfile=syslog
stderr_logfile=syslog
dependent_startup=true
[program:start]
command=/usr/bin/start.sh
priority=2
autostart=false
autorestart=false
startsecs=0
stdout_logfile=syslog
stderr_logfile=syslog
dependent_startup=true
dependent_startup_wait_for=rsyslogd:running
[group:dhcp-relay]
programs=dhcprelayd,dhcp6relay
[program:dhcp6relay]
command=/usr/sbin/dhcp6relay
priority=3
autostart=false
autorestart=false
stdout_logfile=syslog
stderr_logfile=syslog
dependent_startup=true
dependent_startup_wait_for=start:exited
[program:dhcprelayd]
command=/usr/local/bin/dhcprelayd
priority=3
autostart=false
autorestart=false
stdout_logfile=syslog
stderr_logfile=syslog
dependent_startup=true
dependent_startup_wait_for=start:exited

View File

@ -42,7 +42,6 @@ dependent_startup_wait_for=rsyslogd:running
[group:dhcp-relay] [group:dhcp-relay]
programs=isc-dhcpv4-relay-Vlan1000,dhcp6relay programs=isc-dhcpv4-relay-Vlan1000,dhcp6relay
[program:isc-dhcpv4-relay-Vlan1000] [program:isc-dhcpv4-relay-Vlan1000]
command=/usr/sbin/dhcrelay -d -m discard -a %%h:%%p %%P --name-alias-map-file /tmp/port-name-alias-map.txt -id Vlan1000 -iu Vlan2000 -iu PortChannel01 -iu PortChannel02 -iu PortChannel03 -iu PortChannel04 192.0.0.1 192.0.0.2 command=/usr/sbin/dhcrelay -d -m discard -a %%h:%%p %%P --name-alias-map-file /tmp/port-name-alias-map.txt -id Vlan1000 -iu Vlan2000 -iu PortChannel01 -iu PortChannel02 -iu PortChannel03 -iu PortChannel04 192.0.0.1 192.0.0.2
priority=3 priority=3

View File

@ -42,7 +42,6 @@ dependent_startup_wait_for=rsyslogd:running
[group:dhcp-relay] [group:dhcp-relay]
programs=isc-dhcpv4-relay-Vlan1000,isc-dhcpv4-relay-Vlan2000,dhcp6relay programs=isc-dhcpv4-relay-Vlan1000,isc-dhcpv4-relay-Vlan2000,dhcp6relay
[program:isc-dhcpv4-relay-Vlan1000] [program:isc-dhcpv4-relay-Vlan1000]
command=/usr/sbin/dhcrelay -d -m discard -a %%h:%%p %%P --name-alias-map-file /tmp/port-name-alias-map.txt -id Vlan1000 -iu Vlan2000 -iu PortChannel01 -iu PortChannel02 -iu PortChannel03 -iu PortChannel04 192.0.0.1 192.0.0.2 command=/usr/sbin/dhcrelay -d -m discard -a %%h:%%p %%P --name-alias-map-file /tmp/port-name-alias-map.txt -id Vlan1000 -iu Vlan2000 -iu PortChannel01 -iu PortChannel02 -iu PortChannel03 -iu PortChannel04 192.0.0.1 192.0.0.2
priority=3 priority=3

View File

@ -154,22 +154,43 @@ class TestJ2Files(TestCase):
def test_dhcp_relay(self): def test_dhcp_relay(self):
# Test generation of wait_for_intf.sh # Test generation of wait_for_intf.sh
dhc_sample_data = os.path.join(self.test_dir, "dhcp-relay-sample.json") dhc_sample_data = os.path.join(self.test_dir, "dhcp-relay-sample.json")
enable_dhcp_server_sample_data = os.path.join(self.test_dir, "dhcp-relay-enable-dhcp-server-sample.json")
template_path = os.path.join(self.test_dir, '..', '..', '..', 'dockers', 'docker-dhcp-relay', 'wait_for_intf.sh.j2') template_path = os.path.join(self.test_dir, '..', '..', '..', 'dockers', 'docker-dhcp-relay', 'wait_for_intf.sh.j2')
argument = ['-m', self.t0_minigraph, '-j', dhc_sample_data, '-p', self.t0_port_config, '-t', template_path] argument = ['-m', self.t0_minigraph, '-j', dhc_sample_data, '-p', self.t0_port_config, '-t', template_path]
self.run_script(argument, output_file=self.output_file) self.run_script(argument, output_file=self.output_file)
self.assertTrue(utils.cmp(os.path.join(self.test_dir, 'sample_output', utils.PYvX_DIR, 'wait_for_intf.sh'), self.output_file)) self.assertTrue(utils.cmp(os.path.join(self.test_dir, 'sample_output', utils.PYvX_DIR, 'wait_for_intf.sh'), self.output_file))
# Test generation of docker-dhcp-relay.supervisord.conf # Test generation of docker-dhcp-relay.supervisord.conf witout dhcp_server feature entry
template_path = os.path.join(self.test_dir, '..', '..', '..', 'dockers', 'docker-dhcp-relay', 'docker-dhcp-relay.supervisord.conf.j2') template_path = os.path.join(self.test_dir, '..', '..', '..', 'dockers', 'docker-dhcp-relay', 'docker-dhcp-relay.supervisord.conf.j2')
argument = ['-m', self.t0_minigraph, '-p', self.t0_port_config, '-t', template_path] argument = ['-m', self.t0_minigraph, '-p', self.t0_port_config, '-t', template_path]
self.run_script(argument, output_file=self.output_file) self.run_script(argument, output_file=self.output_file)
self.assertTrue(utils.cmp(os.path.join(self.test_dir, 'sample_output', utils.PYvX_DIR, 'docker-dhcp-relay.supervisord.conf'), self.output_file)) self.assertTrue(utils.cmp(os.path.join(self.test_dir, 'sample_output', utils.PYvX_DIR, 'docker-dhcp-relay.supervisord.conf'), self.output_file))
# Test generation of docker-dhcp-relay.supervisord.conf with disabled dhcp_server feature
template_path = os.path.join(self.test_dir, '..', '..', '..', 'dockers', 'docker-dhcp-relay',
'docker-dhcp-relay.supervisord.conf.j2')
argument = ['-m', self.t0_minigraph, '-j', dhc_sample_data, '-p', self.t0_port_config, '-t', template_path]
self.run_script(argument, output_file=self.output_file)
self.assertTrue(utils.cmp(os.path.join(self.test_dir, 'sample_output', utils.PYvX_DIR,
'docker-dhcp-relay.supervisord.conf'), self.output_file))
# Test generation of docker-dhcp-relay.supervisord.conf with enabled dhcp_server feature
template_path = os.path.join(self.test_dir, '..', '..', '..', 'dockers', 'docker-dhcp-relay',
'docker-dhcp-relay.supervisord.conf.j2')
argument = ['-m', self.t0_minigraph, '-j', enable_dhcp_server_sample_data, '-p', self.t0_port_config, '-t',
template_path]
self.run_script(argument, output_file=self.output_file)
self.assertTrue(utils.cmp(os.path.join(self.test_dir, 'sample_output', utils.PYvX_DIR,
'docker-dhcp-relay-enable-dhcp-server.supervisord.conf'),
self.output_file))
# Test generation of docker-dhcp-relay.supervisord.conf when a vlan is missing ip/ipv6 helpers # Test generation of docker-dhcp-relay.supervisord.conf when a vlan is missing ip/ipv6 helpers
template_path = os.path.join(self.test_dir, '..', '..', '..', 'dockers', 'docker-dhcp-relay', 'docker-dhcp-relay.supervisord.conf.j2') template_path = os.path.join(self.test_dir, '..', '..', '..', 'dockers', 'docker-dhcp-relay',
'docker-dhcp-relay.supervisord.conf.j2')
argument = ['-m', self.no_ip_helper_minigraph, '-p', self.t0_port_config, '-t', template_path] argument = ['-m', self.no_ip_helper_minigraph, '-p', self.t0_port_config, '-t', template_path]
self.run_script(argument, output_file=self.output_file) self.run_script(argument, output_file=self.output_file)
self.assertTrue(utils.cmp(os.path.join(self.test_dir, 'sample_output', utils.PYvX_DIR, 'docker-dhcp-relay-no-ip-helper.supervisord.conf'), self.output_file)) self.assertTrue(utils.cmp(os.path.join(self.test_dir, 'sample_output', utils.PYvX_DIR,
'docker-dhcp-relay-no-ip-helper.supervisord.conf'), self.output_file))
def test_radv(self): def test_radv(self):
# Test generation of radvd.conf with multiple ipv6 prefixes # Test generation of radvd.conf with multiple ipv6 prefixes

View File

@ -0,0 +1,131 @@
import ipaddress
import syslog
from abc import abstractmethod
from swsscommon import swsscommon
DHCP_SERVER_IPV4 = "DHCP_SERVER_IPV4"
DHCP_SERVER_IPV4_PORT = "DHCP_SERVER_IPV4_PORT"
DHCP_SERVER_IPV4_RANGE = "DHCP_SERVER_IPV4_RANGE"
VLAN = "VLAN"
VLAN_MEMBER = "VLAN_MEMBER"
VLAN_INTERFACE = "VLAN_INTERFACE"
DEFAULT_SELECT_TIMEOUT = 5000 # millisecond
class DhcpDbMonitor(object):
def __init__(self, db_connector, select_timeout=DEFAULT_SELECT_TIMEOUT):
self.db_connector = db_connector
self.sel = swsscommon.Select()
self.select_timeout = select_timeout
@abstractmethod
def subscribe_table(self):
"""
Subcribe db table to monitor
"""
raise NotImplementedError
@abstractmethod
def _do_check(self):
"""
Check whether interested table content changed
"""
raise NotImplementedError
def check_db_update(self, check_param):
"""
Fetch db and check update
"""
state, _ = self.sel.select(self.select_timeout)
if state == swsscommon.Select.TIMEOUT or state != swsscommon.Select.OBJECT:
return False
return self._do_check(check_param)
class DhcpRelaydDbMonitor(DhcpDbMonitor):
subscribe_dhcp_server_table = None
subscribe_vlan_table = None
subscribe_vlan_intf_table = None
def subscribe_table(self):
self.subscribe_dhcp_server_table = swsscommon.SubscriberStateTable(self.db_connector.config_db,
DHCP_SERVER_IPV4)
self.subscribe_vlan_table = swsscommon.SubscriberStateTable(self.db_connector.config_db, VLAN)
self.subscribe_vlan_intf_table = swsscommon.SubscriberStateTable(self.db_connector.config_db, VLAN_INTERFACE)
# Subscribe dhcp_server_ipv4 and vlan/vlan_interface table. No need to subscribe vlan_member table
self.sel.addSelectable(self.subscribe_dhcp_server_table)
self.sel.addSelectable(self.subscribe_vlan_table)
self.sel.addSelectable(self.subscribe_vlan_intf_table)
def _do_check(self, check_param):
if "enabled_dhcp_interfaces" not in check_param:
syslog.syslog(syslog.LOG_ERR, "Cannot get enabled_dhcp_interfaces")
return (True, True, True)
enabled_dhcp_interfaces = check_param["enabled_dhcp_interfaces"]
return (self._check_dhcp_server_update(enabled_dhcp_interfaces),
self._check_vlan_update(enabled_dhcp_interfaces),
self._check_vlan_intf_update(enabled_dhcp_interfaces))
def _check_dhcp_server_update(self, enabled_dhcp_interfaces):
"""
Check dhcp_server_ipv4 table
Args:
enabled_dhcp_interfaces: DHCP interface that enabled dhcp_server
Returns:
Whether need to refresh
"""
need_refresh = False
while self.subscribe_dhcp_server_table.hasData():
key, op, entry = self.subscribe_dhcp_server_table.pop()
if op == "SET":
for field, value in entry:
if field != "state":
continue
# Only if new state is not consistent with old state, we need to refresh
if key in enabled_dhcp_interfaces and value == "disabled":
need_refresh = True
elif key not in enabled_dhcp_interfaces and value == "enabled":
need_refresh = True
# For del operation, we can skip disabled change
if op == "DEL":
if key in enabled_dhcp_interfaces:
need_refresh = True
return need_refresh
def _check_vlan_update(self, enabled_dhcp_interfaces):
"""
Check vlan table
Args:
enabled_dhcp_interfaces: DHCP interface that enabled dhcp_server
Returns:
Whether need to refresh
"""
need_refresh = False
while self.subscribe_vlan_table.hasData():
key, op, _ = self.subscribe_vlan_table.pop()
# For vlan doesn't have related dhcp entry, not need to refresh dhcrelay process
if key not in enabled_dhcp_interfaces:
continue
need_refresh = True
return need_refresh
def _check_vlan_intf_update(self, enabled_dhcp_interfaces):
"""
Check vlan_interface table
Args:
enabled_dhcp_interfaces: DHCP interface that enabled dhcp_server
Returns:
Whether need to refresh
"""
need_refresh = False
while self.subscribe_vlan_intf_table.hasData():
key, _, _ = self.subscribe_vlan_intf_table.pop()
splits = key.split("|")
vlan_name = splits[0]
ip_address = splits[1].split("/")[0] if len(splits) > 1 else None
if vlan_name not in enabled_dhcp_interfaces:
continue
if ip_address is None or ipaddress.ip_address(ip_address).version != 4:
continue
need_refresh = True
return need_refresh

View File

@ -56,6 +56,16 @@ def get_entry(table, entry_name):
return dict(entry) return dict(entry)
def terminate_proc(proc):
"""
Terminate process, to make sure it exit successfully
Args:
proc: Process object in psutil
"""
proc.terminate()
proc.wait()
def merge_intervals(intervals): def merge_intervals(intervals):
""" """
Merge ip range intervals. Merge ip range intervals.

View File

@ -0,0 +1,191 @@
# TODO Add support for running different dhcrelay processes for each dhcp interface
# Currently if we run multiple dhcrelay processes, except for the last running process,
# others will not relay dhcp_release packet.
import psutil
import subprocess
import sys
import syslog
import time
from swsscommon import swsscommon
from dhcp_server.common.utils import DhcpDbConnector, terminate_proc
from dhcp_server.common.dhcp_db_monitor import DhcpRelaydDbMonitor
REDIS_SOCK_PATH = "/var/run/redis/redis.sock"
DHCP_SERVER_IPV4_SERVER_IP = "DHCP_SERVER_IPV4_SERVER_IP"
DHCP_SERVER_IPV4 = "DHCP_SERVER_IPV4"
VLAN = "VLAN"
VLAN_INTERFACE = "VLAN_INTERFACE"
DEFAULT_SELECT_TIMEOUT = 5000 # millisecond
DHCP_SERVER_INTERFACE = "eth0"
DEFAULT_REFRESH_INTERVAL = 2
KILLED_OLD = 1
NOT_KILLED = 2
NOT_FOUND_PROC = 3
class DhcpRelayd(object):
sel = None
enabled_dhcp_interfaces = set()
def __init__(self, db_connector, select_timeout=DEFAULT_SELECT_TIMEOUT):
"""
Args:
db_connector: db connector obj
select_timeout: timeout setting for subscribe db change
"""
self.db_connector = db_connector
self.last_refresh_time = None
self.dhcp_relayd_monitor = DhcpRelaydDbMonitor(db_connector, select_timeout)
def start(self):
"""
Start function
"""
self.refresh_dhcrelay()
self.dhcp_relayd_monitor.subscribe_table()
def refresh_dhcrelay(self, force_kill=False):
"""
To refresh dhcrelay/dhcpmon process (start or restart)
"""
syslog.syslog(syslog.LOG_INFO, "Start to refresh dhcrelay related processes")
dhcp_server_ip = self._get_dhcp_server_ip()
dhcp_server_ipv4_table = self.db_connector.get_config_db_table(DHCP_SERVER_IPV4)
vlan_table = self.db_connector.get_config_db_table(VLAN)
dhcp_interfaces = set()
self.enabled_dhcp_interfaces = set()
for dhcp_interface, config in dhcp_server_ipv4_table.items():
# Reason for add to enabled_dhcp_interfaces firstly is for below scenario:
# Firstly vlan 1000 is not in vlan table but enabled in dhcp_server table, then add vlan1000 to vlan table
# we need to refresh
if config["state"] == "enabled":
dhcp_interfaces.add(dhcp_interface)
self.enabled_dhcp_interfaces.add(dhcp_interface)
if dhcp_interface not in vlan_table:
dhcp_interfaces.discard(dhcp_interface)
continue
self._start_dhcrelay_process(dhcp_interfaces, dhcp_server_ip, force_kill)
self._start_dhcpmon_process(dhcp_interfaces, force_kill)
def wait(self):
"""
Wait function, check db change here
"""
while True:
res = (self.dhcp_relayd_monitor.check_db_update({"enabled_dhcp_interfaces": self.enabled_dhcp_interfaces}))
# Select timeout or no successful
if isinstance(res, bool):
continue
(dhcp_server_res, vlan_res, vlan_intf_res) = res
# vlan ip change require kill old dhcp_relay related processes
if vlan_intf_res:
self.refresh_dhcrelay(True)
elif dhcp_server_res or vlan_res:
self.refresh_dhcrelay(False)
def _start_dhcrelay_process(self, new_dhcp_interfaces, dhcp_server_ip, force_kill):
# To check whether need to kill dhcrelay process
kill_res = self._kill_exist_relay_releated_process(new_dhcp_interfaces, "dhcrelay", force_kill)
if kill_res == NOT_KILLED:
# Means old running status consistent with the new situation, no need to run new
return
# No need to start new dhcrelay process
if len(new_dhcp_interfaces) == 0:
return
cmds = ["/usr/sbin/dhcrelay", "-d", "-m", "discard", "-a", "%h:%p", "%P", "--name-alias-map-file",
"/tmp/port-name-alias-map.txt"]
for dhcp_interface in new_dhcp_interfaces:
cmds += ["-id", dhcp_interface]
cmds += ["-iu", "docker0", dhcp_server_ip]
popen_res = subprocess.Popen(cmds)
# To make sure process start successfully not in zombie status
proc = psutil.Process(popen_res.pid)
time.sleep(1)
if proc.status() == psutil.STATUS_ZOMBIE:
syslog.syslog(syslog.LOG_ERR, "Failed to start dhcrelay process with: {}".format(cmds))
terminate_proc(proc)
sys.exit(1)
syslog.syslog(syslog.LOG_INFO, "dhcrelay process started successfully, cmds: {}".format(cmds))
def _start_dhcpmon_process(self, new_dhcp_interfaces, force_kill):
# To check whether need to kill dhcrelay process
kill_res = self._kill_exist_relay_releated_process(new_dhcp_interfaces, "dhcpmon", force_kill)
if kill_res == NOT_KILLED:
# Means old running status consistent with the new situation, no need to run new
return
# No need to start new dhcrelay process
if len(new_dhcp_interfaces) == 0:
return
pids_cmds = {}
for dhcp_interface in new_dhcp_interfaces:
cmds = ["/usr/sbin/dhcpmon", "-id", dhcp_interface, "-iu", "docker0", "-im", "eth0"]
popen_res = subprocess.Popen(cmds)
pids_cmds[popen_res.pid] = cmds
time.sleep(1)
# To make sure process start successfully not in zombie status
for pid, cmds in pids_cmds.items():
proc = psutil.Process(pid)
if proc.status() == psutil.STATUS_ZOMBIE:
syslog.syslog(syslog.LOG_ERR, "Faild to start dhcpmon process: {}".format(cmds))
terminate_proc(proc)
else:
syslog.syslog(syslog.LOG_INFO, "dhcpmon process started successfully, cmds: {}".format(cmds))
def _kill_exist_relay_releated_process(self, new_dhcp_interfaces, process_name, force_kill):
old_dhcp_interfaces = set()
# Because in system there maybe more than 1 dhcpmon processes are running, so we need list to store
target_procs = []
# Get old dhcrelay process and get old dhcp interfaces
for proc in psutil.process_iter():
if proc.name() == process_name:
cmds = proc.cmdline()
index = 0
target_procs.append(proc)
while index < len(cmds):
if cmds[index] == "-id":
old_dhcp_interfaces.add(cmds[index + 1])
index += 2
else:
index += 1
if len(target_procs) == 0:
return NOT_FOUND_PROC
# No need to kill
if not force_kill and (process_name == "dhcrelay" and old_dhcp_interfaces == new_dhcp_interfaces or
process_name == "dhcpmon" and old_dhcp_interfaces == (new_dhcp_interfaces)):
return NOT_KILLED
for proc in target_procs:
terminate_proc(proc)
syslog.syslog(syslog.LOG_INFO, "Kill process: {}".format(process_name))
return KILLED_OLD
def _get_dhcp_server_ip(self):
dhcp_server_ip_table = swsscommon.Table(self.db_connector.state_db, DHCP_SERVER_IPV4_SERVER_IP)
for _ in range(10):
state, ip = dhcp_server_ip_table.hget(DHCP_SERVER_INTERFACE, "ip")
if state:
return ip
else:
syslog.syslog(syslog.LOG_INFO, "Cannot get dhcp server ip")
time.sleep(10)
syslog.syslog(syslog.LOG_ERR, "Cannot get dhcp_server ip from state_db")
sys.exit(1)
def main():
dhcp_db_connector = DhcpDbConnector(redis_sock=REDIS_SOCK_PATH)
dhcprelayd = DhcpRelayd(dhcp_db_connector)
dhcprelayd.start()
dhcprelayd.wait()
if __name__ == "__main__":
main()

View File

@ -1,12 +1,11 @@
#!/usr/bin/env python #!/usr/bin/env python
import ipaddress import ipaddress
import json
import os import os
import syslog import syslog
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from .dhcp_server_utils import merge_intervals from dhcp_server.common.utils import merge_intervals
PORT_MAP_PATH = "/tmp/port-name-alias-map.txt" PORT_MAP_PATH = "/tmp/port-name-alias-map.txt"
UNICODE_TYPE = str UNICODE_TYPE = str

View File

@ -1,5 +1,3 @@
import ipaddress
import json
import signal import signal
import syslog import syslog
import threading import threading
@ -66,12 +64,13 @@ class LeaseHanlder(object):
old_lease_table = self.db_connector.get_state_db_table(DHCP_SERVER_IPV4_LEASE) old_lease_table = self.db_connector.get_state_db_table(DHCP_SERVER_IPV4_LEASE)
old_lease_key = set(old_lease_table.keys()) old_lease_key = set(old_lease_table.keys())
# 1.1 If start time equal to end time, means lease has been released # 1.1 If start time equal to end time or lease expired, means lease has been released
# 1.1.1 If current lease table has this old lease, delete it # 1.1.1 If current lease table has this old lease, delete it
# 1.1.2 Else skip # 1.1.2 Else skip
# 1.2 Else, means lease valid, save it. # 1.2 Else, means lease valid, save it.
for key, value in new_lease.items(): for key, value in new_lease.items():
if value["lease_start"] == value["lease_end"]: unix_time = datetime.now().timestamp()
if value["lease_start"] == value["lease_end"] or unix_time >= int(value["lease_end"]):
if key in old_lease_key: if key in old_lease_key:
self.db_connector.state_db.delete("{}|{}".format(DHCP_SERVER_IPV4_LEASE, key)) self.db_connector.state_db.delete("{}|{}".format(DHCP_SERVER_IPV4_LEASE, key))
continue continue

View File

@ -2,14 +2,19 @@
import psutil import psutil
import signal import signal
import time import time
import sys
import syslog
from .dhcp_cfggen import DhcpServCfgGenerator from .dhcp_cfggen import DhcpServCfgGenerator
from .dhcp_lease import LeaseManager from .dhcp_lease import LeaseManager
from .dhcp_server_utils import DhcpDbConnector from dhcp_server.common.utils import DhcpDbConnector
KEA_DHCP4_CONFIG = "/etc/kea/kea-dhcp4.conf" KEA_DHCP4_CONFIG = "/etc/kea/kea-dhcp4.conf"
KEA_DHCP4_PROC_NAME = "kea-dhcp4" KEA_DHCP4_PROC_NAME = "kea-dhcp4"
KEA_LEASE_FILE_PATH = "/tmp/kea-lease.csv" KEA_LEASE_FILE_PATH = "/tmp/kea-lease.csv"
REDIS_SOCK_PATH = "/var/run/redis/redis.sock" REDIS_SOCK_PATH = "/var/run/redis/redis.sock"
DHCP_SERVER_IPV4_SERVER_IP = "DHCP_SERVER_IPV4_SERVER_IP"
DHCP_SERVER_INTERFACE = "eth0"
AF_INET = 2
class DhcpServd(object): class DhcpServd(object):
@ -37,8 +42,27 @@ class DhcpServd(object):
# After refresh kea-config, we need to SIGHUP kea-dhcp4 process to read new config # After refresh kea-config, we need to SIGHUP kea-dhcp4 process to read new config
self._notify_kea_dhcp4_proc() self._notify_kea_dhcp4_proc()
def _update_dhcp_server_ip(self):
"""
Add ip address of "eth0" inside dhcp_server container as dhcp_server_ip into state_db
"""
dhcp_server_ip = None
for _ in range(10):
dhcp_interface = psutil.net_if_addrs().get(DHCP_SERVER_INTERFACE, [])
for address in dhcp_interface:
if address.family == AF_INET:
dhcp_server_ip = address.address
self.db_connector.state_db.hset("{}|{}".format(DHCP_SERVER_IPV4_SERVER_IP, DHCP_SERVER_INTERFACE),
"ip", dhcp_server_ip)
return
else:
time.sleep(5)
syslog.syslog(syslog.LOG_INFO, "Cannot get ip address of {}".format(DHCP_SERVER_INTERFACE))
sys.exit(1)
def start(self): def start(self):
self.dump_dhcp4_config() self.dump_dhcp4_config()
self._update_dhcp_server_ip()
lease_manager = LeaseManager(self.db_connector, KEA_LEASE_FILE_PATH) lease_manager = LeaseManager(self.db_connector, KEA_LEASE_FILE_PATH)
lease_manager.start() lease_manager.start()

View File

@ -1,18 +1,12 @@
from setuptools import setup from setuptools import setup, find_packages
dependencies = [ dependencies = [
"psutil", "psutil"
"coverage"
] ]
test_deps = [ test_deps = [
"pytest" "pytest",
] "freezegun"
py_modules = [
"dhcp_server_utils",
"dhcp_cfggen",
"dhcp_lease"
] ]
setup( setup(
@ -29,14 +23,16 @@ setup(
"wheel", "wheel",
], ],
packages=[ packages=[
"dhcp_server" "dhcp_server.common",
"dhcp_server.dhcpservd",
"dhcp_server.dhcprelayd"
], ],
entry_points={ entry_points={
"console_scripts": [ "console_scripts": [
"dhcpservd = dhcp_server.dhcpservd:main" "dhcprelayd = dhcp_server.dhcprelayd.dhcprelayd:main",
"dhcpservd = dhcp_server.dhcpservd.dhcpservd:main"
] ]
}, },
py_modules=py_modules,
classifiers=[ classifiers=[
"Intended Audience :: Developers", "Intended Audience :: Developers",
"Operating System :: Linux", "Operating System :: Linux",

View File

@ -1,6 +1,9 @@
import heapq
import json import json
import psutil
MOCK_CONFIG_DB_PATH = "tests/test_data/mock_config_db.json" MOCK_CONFIG_DB_PATH = "tests/test_data/mock_config_db.json"
MOCK_STATE_DB_PATH = "tests/test_data/mock_state_db.json"
class MockConfigDb(object): class MockConfigDb(object):
@ -10,3 +13,75 @@ class MockConfigDb(object):
def get_config_db_table(self, table_name): def get_config_db_table(self, table_name):
return self.config_db.get(table_name, {}) return self.config_db.get(table_name, {})
class MockSelect(object):
def __init__(self):
pass
def select(self, timeout):
return None, None
class MockSubscribeTable(object):
def __init__(self, tables):
self.stack = []
for item in tables:
heapq.heappush(self.stack, item)
# if table_name == "DHCP_SERVER_IPV4":
# heapq.heappush(self.stack, ("Vlan1000", "SET", (("state", "enabled"),)))
# heapq.heappush(self.stack, ("Vlan1000", "SET", (("customized_options", "option1"), ("state", "enabled"),)))
# heapq.heappush(self.stack, ("Vlan2000", "SET", (("state", "enabled"),)))
# heapq.heappush(self.stack, ("Vlan1000", "DEL", ()))
# heapq.heappush(self.stack, ("Vlan2000", "DEL", ()))
# if table_name == "VLAN":
# heapq.heappush(self.stack, ("Vlan1000", "SET", (("vlanid", "1000"),)))
# heapq.heappush(self.stack, ("Vlan1001", "SET", (("vlanid", "1001"),)))
# heapq.heappush(self.stack, ("Vlan1001", "DEL", (("vlanid", "1001"),)))
# heapq.heappush(self.stack, ("Vlan1002", "SET", (("vlanid", "1002"),)))
# heapq.heappush(self.stack, ("Vlan2000", "SET", (("vlanid", "2000"),)))
def pop(self):
res = heapq.heappop(self.stack)
return res
def hasData(self):
return len(self.stack) != 0
def mock_get_config_db_table(table_name):
mock_config_db = MockConfigDb()
return mock_config_db.get_config_db_table(table_name)
class MockProc(object):
def __init__(self, name, pid=None, status=psutil.STATUS_RUNNING):
self.proc_name = name
self.pid = pid
def name(self):
return self.proc_name
def send_signal(self, sig_num):
pass
def cmdline(self):
if self.proc_name == "dhcrelay":
return ["/usr/sbin/dhcrelay", "-d", "-m", "discard", "-a", "%h:%p", "%P", "--name-alias-map-file",
"/tmp/port-name-alias-map.txt", "-id", "Vlan1000", "-iu", "docker0", "240.127.1.2"]
if self.proc_name == "dhcpmon":
return ["/usr/sbin/dhcpmon", "-id", "Vlan1000", "-iu", "docker0", "-im", "eth0"]
def terminate(self):
pass
def wait(self):
pass
def status(self):
return self.status
class MockPopen(object):
def __init__(self, pid):
self.pid = pid

View File

@ -1,30 +1,38 @@
import pytest import pytest
import dhcp_server.dhcp_server_utils as dhcp_server_utils import dhcp_server.common.utils as utils
import os
import sys
from unittest.mock import patch, PropertyMock from unittest.mock import patch, PropertyMock
from dhcp_server.dhcp_cfggen import DhcpServCfgGenerator from dhcp_server.dhcpservd.dhcp_cfggen import DhcpServCfgGenerator
test_path = os.path.dirname(os.path.abspath(__file__))
modules_path = os.path.dirname(test_path)
sys.path.insert(0, test_path)
sys.path.insert(0, modules_path)
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def mock_swsscommon_dbconnector_init(): def mock_swsscommon_dbconnector_init():
with patch.object(dhcp_server_utils.swsscommon.DBConnector, "__init__", return_value=None) as mock_dbconnector_init: with patch.object(utils.swsscommon.DBConnector, "__init__", return_value=None) as mock_dbconnector_init:
yield mock_dbconnector_init yield mock_dbconnector_init
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def mock_swsscommon_table_init(): def mock_swsscommon_table_init():
with patch.object(dhcp_server_utils.swsscommon.Table, "__init__", return_value=None) as mock_table_init: with patch.object(utils.swsscommon.Table, "__init__", return_value=None) as mock_table_init:
yield mock_table_init yield mock_table_init
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def mock_get_render_template(): def mock_get_render_template():
with patch("dhcp_server.dhcp_cfggen.DhcpServCfgGenerator._get_render_template", return_value=None) as mock_template: with patch("dhcp_server.dhcpservd.dhcp_cfggen.DhcpServCfgGenerator._get_render_template",
return_value=None) as mock_template:
yield mock_template yield mock_template
@pytest.fixture @pytest.fixture
def mock_parse_port_map_alias(scope="function"): def mock_parse_port_map_alias(scope="function"):
with patch("dhcp_server.dhcp_cfggen.DhcpServCfgGenerator._parse_port_map_alias", return_value=None) as mock_map, \ with patch("dhcp_server.dhcpservd.dhcp_cfggen.DhcpServCfgGenerator._parse_port_map_alias",
return_value=None) as mock_map, \
patch.object(DhcpServCfgGenerator, "port_alias_map", return_value={"Ethernet24": "etp7", "Ethernet28": "etp8"}, patch.object(DhcpServCfgGenerator, "port_alias_map", return_value={"Ethernet24": "etp7", "Ethernet28": "etp8"},
new_callable=PropertyMock), \ new_callable=PropertyMock), \
patch.object(DhcpServCfgGenerator, "lease_update_script_path", return_value="/etc/kea/lease_update.sh", patch.object(DhcpServCfgGenerator, "lease_update_script_path", return_value="/etc/kea/lease_update.sh",
@ -32,4 +40,3 @@ def mock_parse_port_map_alias(scope="function"):
patch.object(DhcpServCfgGenerator, "lease_path", return_value="/tmp/kea-lease.csv", new_callable=PropertyMock): patch.object(DhcpServCfgGenerator, "lease_path", return_value="/tmp/kea-lease.csv", new_callable=PropertyMock):
yield mock_map yield mock_map

View File

@ -7,3 +7,4 @@ address,hwaddr,client_id,valid_lifetime,expire,subnet_id,fqdn_fwd,fqdn_rev,hostn
192.168.0.2,10:70:fd:b6:13:00,,0,1693997305,1,0,0,7626dced293e,0,,0 192.168.0.2,10:70:fd:b6:13:00,,0,1693997305,1,0,0,7626dced293e,0,,0
193.168.2.2,10:70:fd:b6:13:15,,3600,1693999305,1,0,0,7626dced293e,0,,0 193.168.2.2,10:70:fd:b6:13:15,,3600,1693999305,1,0,0,7626dced293e,0,,0
193.168.2.3,10:70:fd:b6:13:20,,3600,1693999305,1,0,0,7626dced293e,0,,0 193.168.2.3,10:70:fd:b6:13:20,,3600,1693999305,1,0,0,7626dced293e,0,,0
193.168.0.132,10:70:fd:b6:13:18,,3600,1697610805,1,0,0,7626dced293e,0,,0
1 address hwaddr client_id valid_lifetime expire subnet_id fqdn_fwd fqdn_rev hostname state user_context pool_id
7 192.168.0.2 10:70:fd:b6:13:00 0 1693997305 1 0 0 7626dced293e 0 0
8 193.168.2.2 10:70:fd:b6:13:15 3600 1693999305 1 0 0 7626dced293e 0 0
9 193.168.2.3 10:70:fd:b6:13:20 3600 1693999305 1 0 0 7626dced293e 0 0
10 193.168.0.132 10:70:fd:b6:13:18 3600 1697610805 1 0 0 7626dced293e 0 0

View File

@ -4,6 +4,10 @@
"hostname": "sonic-host" "hostname": "sonic-host"
} }
}, },
"VLAN": {
"Vlan1000": {},
"Vlan2000": {}
},
"VLAN_INTERFACE": { "VLAN_INTERFACE": {
"Vlan1000|192.168.0.1/21": { "Vlan1000|192.168.0.1/21": {
"NULL": "NULL" "NULL": "NULL"
@ -77,6 +81,17 @@
"mode": "PORT", "mode": "PORT",
"netmask": "255.255.255.0", "netmask": "255.255.255.0",
"state": "enabled" "state": "enabled"
},
"Vlan5000": {
"customized_options": [
"option60",
"option223"
],
"gateway": "192.168.4.1",
"lease_time": "900",
"mode": "PORT",
"netmask": "255.255.255.0",
"state": "disabled"
} }
}, },
"DHCP_SERVER_IPV4_CUSTOMIZED_OPTIONS": { "DHCP_SERVER_IPV4_CUSTOMIZED_OPTIONS": {

View File

@ -3,9 +3,8 @@ import ipaddress
import json import json
import pytest import pytest
from common_utils import MockConfigDb from common_utils import MockConfigDb
from dhcp_server.dhcp_server_utils import DhcpDbConnector from dhcp_server.common.utils import DhcpDbConnector
from dhcp_server.dhcp_cfggen import DhcpServCfgGenerator from dhcp_server.dhcpservd.dhcp_cfggen import DhcpServCfgGenerator
from unittest.mock import patch, MagicMock
expected_dhcp_config = { expected_dhcp_config = {
"Dhcp4": { "Dhcp4": {

View File

@ -0,0 +1,219 @@
import pytest
from common_utils import MockSubscribeTable
from dhcp_server.common.dhcp_db_monitor import DhcpDbMonitor, DhcpRelaydDbMonitor
from dhcp_server.common.utils import DhcpDbConnector
from swsscommon import swsscommon
from unittest.mock import patch, call, ANY, PropertyMock
tested_subscribe_dhcp_server_table = [
{
"table": [
("Vlan1000", "SET", (("customized_options", "option1"), ("state", "enabled"),))
],
"exp_res": False
},
{
"table": [
("Vlan2000", "SET", (("state", "enabled"),))
],
"exp_res": True
},
{
"table": [
("Vlan1000", "DEL", ())
],
"exp_res": True
},
{
"table": [
("Vlan2000", "DEL", ())
],
"exp_res": False
},
{
"table": [
("Vlan2000", "DEL", ()),
("Vlan1000", "DEL", ())
],
"exp_res": True
},
{
"table": [
("Vlan3000", "SET", (("state", "enabled"),))
],
"exp_res": True
}
]
tested_subscribe_vlan_table = [
{
"table": [
("Vlan1000", "SET", (("vlanid", "1000"),))
],
"exp_res": True
},
{
"table": [
("Vlan1001", "SET", (("vlanid", "1001"),))
],
"exp_res": False
},
{
"table": [
("Vlan1000", "SET", (("vlanid", "1000"),)),
("Vlan1002", "SET", (("vlanid", "1002"),))
],
"exp_res": True
},
{
"table": [
("Vlan1001", "DEL", ())
],
"exp_res": False
},
{
"table": [
("Vlan1000", "DEL", ())
],
"exp_res": True
},
{
"table": [
("Vlan1000", "SET", (("vlanid", "1000"),)),
("Vlan1001", "DEL", ())
],
"exp_res": True
},
{
"table": [
("Vlan1003", "SET", (("vlanid", "1003"),))
],
"exp_res": False
}
]
tested_subscribe_vlan_intf_table = [
{
"table": [
("Vlan1000", "SET", ())
],
"exp_res": False
},
{
"table": [
("Vlan1000|192.168.0.1/24", "SET", ())
],
"exp_res": True
},
{
"table": [
("Vlan1000|fc02::8/64", "SET", ())
],
"exp_res": False
},
{
"table": [
("Vlan2000|192.168.0.1/24", "SET", ())
],
"exp_res": False
},
{
"table": [
("Vlan1001|192.168.0.1/24", "SET", ())
],
"exp_res": False
}
]
@pytest.mark.parametrize("select_result", [swsscommon.Select.TIMEOUT, swsscommon.Select.OBJECT])
def test_dhcp_db_monitor(mock_swsscommon_dbconnector_init, select_result):
db_connector = DhcpDbConnector()
dhcp_db_monitor = DhcpDbMonitor(db_connector)
try:
dhcp_db_monitor.subscribe_table()
except NotImplementedError:
pass
try:
dhcp_db_monitor._do_check()
except NotImplementedError:
pass
with patch.object(DhcpDbMonitor, "_do_check", return_value=None) as mock_do_check, \
patch.object(swsscommon.Select, "select", return_value=(select_result, None)):
dhcp_db_monitor.check_db_update("mock_param")
if select_result == swsscommon.Select.TIMEOUT:
mock_do_check.assert_not_called()
elif select_result == swsscommon.Select.OBJECT:
mock_do_check.assert_called_once_with("mock_param")
def test_dhcp_relayd_monitor_subscribe_table(mock_swsscommon_dbconnector_init):
with patch.object(swsscommon, "SubscriberStateTable", side_effect=mock_subscriber_state_table) as mock_subscribe, \
patch.object(swsscommon.Select, "addSelectable", return_value=None) as mock_add_select:
db_connector = DhcpDbConnector()
dhcp_relayd_db_monitor = DhcpRelaydDbMonitor(db_connector)
dhcp_relayd_db_monitor.subscribe_table()
mock_subscribe.assert_has_calls([
call(ANY, "DHCP_SERVER_IPV4"),
call(ANY, "VLAN"),
call(ANY, "VLAN_INTERFACE")
])
mock_add_select.assert_has_calls([
call("DHCP_SERVER_IPV4"),
call("VLAN"),
call("VLAN_INTERFACE")
])
@pytest.mark.parametrize("check_param", [{}, {"enabled_dhcp_interfaces": "dummy"}])
def test_dhcp_relayd_monitor_do_check(mock_swsscommon_dbconnector_init, check_param):
with patch.object(DhcpRelaydDbMonitor, "_check_dhcp_server_update") as mock_check_dhcp_server_update, \
patch.object(DhcpRelaydDbMonitor, "_check_vlan_update") as mock_check_vlan_update, \
patch.object(DhcpRelaydDbMonitor, "_check_vlan_intf_update") as mock_check_vlan_intf_update:
db_connector = DhcpDbConnector()
dhcp_relayd_db_monitor = DhcpRelaydDbMonitor(db_connector)
dhcp_relayd_db_monitor._do_check(check_param)
if "enabled_dhcp_interfaces" in check_param:
mock_check_dhcp_server_update.assert_called_once_with("dummy")
mock_check_vlan_update.assert_called_once_with("dummy")
mock_check_vlan_intf_update.assert_called_once_with("dummy")
else:
mock_check_dhcp_server_update.assert_not_called()
mock_check_vlan_update.assert_not_called()
mock_check_vlan_intf_update.assert_not_called()
@pytest.mark.parametrize("dhcp_server_table_update", tested_subscribe_dhcp_server_table)
def test_dhcp_relayd_monitor_check_dhcp_server_update(mock_swsscommon_dbconnector_init, dhcp_server_table_update):
tested_table = dhcp_server_table_update["table"]
with patch.object(DhcpRelaydDbMonitor, "subscribe_dhcp_server_table",
return_value=MockSubscribeTable(tested_table),
new_callable=PropertyMock):
db_connector = DhcpDbConnector()
dhcp_relayd_db_monitor = DhcpRelaydDbMonitor(db_connector)
check_res = dhcp_relayd_db_monitor._check_dhcp_server_update(set(["Vlan1000"]))
assert check_res == dhcp_server_table_update["exp_res"]
@pytest.mark.parametrize("vlan_table_update", tested_subscribe_vlan_table)
def test_dhcp_relayd_monitor_check_vlan_update(mock_swsscommon_dbconnector_init, vlan_table_update):
tested_table = vlan_table_update["table"]
with patch.object(DhcpRelaydDbMonitor, "subscribe_vlan_table", return_value=MockSubscribeTable(tested_table),
new_callable=PropertyMock):
db_connector = DhcpDbConnector()
dhcp_relayd_db_monitor = DhcpRelaydDbMonitor(db_connector)
check_res = dhcp_relayd_db_monitor._check_vlan_update(set(["Vlan1000"]))
assert check_res == vlan_table_update["exp_res"]
@pytest.mark.parametrize("vlan_intf_table_update", tested_subscribe_vlan_intf_table)
def test_dhcp_relayd_monitor_check_vlan_intf_update(mock_swsscommon_dbconnector_init, vlan_intf_table_update):
tested_table = vlan_intf_table_update["table"]
with patch.object(DhcpRelaydDbMonitor, "subscribe_vlan_intf_table", return_value=MockSubscribeTable(tested_table),
new_callable=PropertyMock):
db_connector = DhcpDbConnector()
dhcp_relayd_db_monitor = DhcpRelaydDbMonitor(db_connector)
check_res = dhcp_relayd_db_monitor._check_vlan_intf_update(set(["Vlan1000"]))
assert check_res == vlan_intf_table_update["exp_res"]
def mock_subscriber_state_table(db, table_name):
return table_name

View File

@ -1,5 +1,6 @@
from dhcp_server.dhcp_server_utils import DhcpDbConnector from dhcp_server.common.utils import DhcpDbConnector
from dhcp_server.dhcp_lease import KeaDhcp4LeaseHandler, LeaseHanlder from dhcp_server.dhcpservd.dhcp_lease import KeaDhcp4LeaseHandler, LeaseHanlder
from freezegun import freeze_time
from swsscommon import swsscommon from swsscommon import swsscommon
from unittest.mock import patch, call, MagicMock from unittest.mock import patch, call, MagicMock
@ -14,6 +15,11 @@ expected_lease = {
"lease_end": "1694000915", "lease_end": "1694000915",
"ip": "192.168.0.131" "ip": "192.168.0.131"
}, },
"Vlan1000|10:70:fd:b6:13:18": {
"lease_start": "1697607205",
"lease_end": "1697610805",
"ip": "193.168.0.132"
},
"Vlan2000|10:70:fd:b6:13:15": { "Vlan2000|10:70:fd:b6:13:15": {
"lease_start": "1693995705", "lease_start": "1693995705",
"lease_end": "1693999305", "lease_end": "1693999305",
@ -24,6 +30,7 @@ expected_fdb_info = {
"10:70:fd:b6:13:00": "Vlan1000", "10:70:fd:b6:13:00": "Vlan1000",
"10:70:fd:b6:13:15": "Vlan2000", "10:70:fd:b6:13:15": "Vlan2000",
"10:70:fd:b6:13:17": "Vlan1000", "10:70:fd:b6:13:17": "Vlan1000",
"10:70:fd:b6:13:18": "Vlan1000"
} }
@ -43,8 +50,6 @@ def test_read_kea_lease(mock_swsscommon_dbconnector_init):
kea_lease_handler = KeaDhcp4LeaseHandler(db_connector, lease_file="tests/test_data/kea-lease.csv") kea_lease_handler = KeaDhcp4LeaseHandler(db_connector, lease_file="tests/test_data/kea-lease.csv")
# Verify whether lease information read is as expected # Verify whether lease information read is as expected
lease = kea_lease_handler._read() lease = kea_lease_handler._read()
print(lease)
print(expected_lease)
assert lease == expected_lease assert lease == expected_lease
@ -52,9 +57,10 @@ def test_get_fdb_info(mock_swsscommon_dbconnector_init):
mock_fdb_table = { mock_fdb_table = {
"Vlan2000:10:70:fd:b6:13:15": {"port": "Ethernet31", "type": "dynamic"}, "Vlan2000:10:70:fd:b6:13:15": {"port": "Ethernet31", "type": "dynamic"},
"Vlan1000:10:70:fd:b6:13:00": {"port": "Ethernet32", "type": "dynamic"}, "Vlan1000:10:70:fd:b6:13:00": {"port": "Ethernet32", "type": "dynamic"},
"Vlan1000:10:70:fd:b6:13:17": {"port": "Ethernet32", "type": "dynamic"} "Vlan1000:10:70:fd:b6:13:17": {"port": "Ethernet33", "type": "dynamic"},
"Vlan1000:10:70:fd:b6:13:18": {"port": "Ethernet34", "type": "dynamic"}
} }
with patch("dhcp_server.dhcp_server_utils.DhcpDbConnector.get_state_db_table", return_value=mock_fdb_table): with patch("dhcp_server.common.utils.DhcpDbConnector.get_state_db_table", return_value=mock_fdb_table):
db_connector = DhcpDbConnector() db_connector = DhcpDbConnector()
kea_lease_handler = KeaDhcp4LeaseHandler(db_connector, lease_file="tests/test_data/kea-lease.csv") kea_lease_handler = KeaDhcp4LeaseHandler(db_connector, lease_file="tests/test_data/kea-lease.csv")
# Verify whether lease information read is as expected # Verify whether lease information read is as expected
@ -62,13 +68,21 @@ def test_get_fdb_info(mock_swsscommon_dbconnector_init):
assert fdb_info == expected_fdb_info assert fdb_info == expected_fdb_info
# Cannot mock built-in/extension type function(datetime.datetime.timestamp), need to free time
@freeze_time("2023-09-08")
def test_update_kea_lease(mock_swsscommon_dbconnector_init, mock_swsscommon_table_init): def test_update_kea_lease(mock_swsscommon_dbconnector_init, mock_swsscommon_table_init):
tested_lease = expected_lease tested_lease = expected_lease
mock_lease_table = {
"Vlan1000|aa:bb:cc:dd:ee:ff": {},
"Vlan1000|10:70:fd:b6:13:00": {},
"Vlan1000|10:70:fd:b6:13:17": {},
"Vlan1000|10:70:fd:b6:13:18": {}
}
with patch.object(swsscommon.Table, "getKeys"), \ with patch.object(swsscommon.Table, "getKeys"), \
patch.object(swsscommon.DBConnector, "hset") as mock_hset, \ patch.object(swsscommon.DBConnector, "hset") as mock_hset, \
patch.object(KeaDhcp4LeaseHandler, "_read", MagicMock(return_value=tested_lease)), \ patch.object(KeaDhcp4LeaseHandler, "_read", MagicMock(return_value=tested_lease)), \
patch.object(DhcpDbConnector, "get_state_db_table", patch.object(DhcpDbConnector, "get_state_db_table",
return_value={"Vlan1000|aa:bb:cc:dd:ee:ff": {}, "Vlan1000|10:70:fd:b6:13:00": {}}), \ return_value=mock_lease_table), \
patch.object(swsscommon.DBConnector, "delete") as mock_delete, \ patch.object(swsscommon.DBConnector, "delete") as mock_delete, \
patch("time.sleep", return_value=None) as mock_sleep: patch("time.sleep", return_value=None) as mock_sleep:
db_connector = DhcpDbConnector() db_connector = DhcpDbConnector()
@ -77,14 +91,15 @@ def test_update_kea_lease(mock_swsscommon_dbconnector_init, mock_swsscommon_tabl
# Verify that old key was deleted # Verify that old key was deleted
mock_delete.assert_has_calls([ mock_delete.assert_has_calls([
call("DHCP_SERVER_IPV4_LEASE|Vlan1000|10:70:fd:b6:13:00"), call("DHCP_SERVER_IPV4_LEASE|Vlan1000|10:70:fd:b6:13:00"),
call("DHCP_SERVER_IPV4_LEASE|Vlan1000|10:70:fd:b6:13:17"),
call("DHCP_SERVER_IPV4_LEASE|Vlan1000|aa:bb:cc:dd:ee:ff") call("DHCP_SERVER_IPV4_LEASE|Vlan1000|aa:bb:cc:dd:ee:ff")
]) ])
# Verify that lease has been updated, to be noted that lease for "192.168.0.2" didn't been updated because # Verify that lease has been updated, to be noted that lease for "192.168.0.2" didn't been updated because
# lease_start equals to lease_end # lease_start equals to lease_end
mock_hset.assert_has_calls([ mock_hset.assert_has_calls([
call('DHCP_SERVER_IPV4_LEASE|Vlan1000|10:70:fd:b6:13:17', 'lease_start', '1693997315'), call("DHCP_SERVER_IPV4_LEASE|Vlan1000|10:70:fd:b6:13:18", "lease_start", "1697607205"),
call('DHCP_SERVER_IPV4_LEASE|Vlan1000|10:70:fd:b6:13:17', 'lease_end', '1694000915'), call("DHCP_SERVER_IPV4_LEASE|Vlan1000|10:70:fd:b6:13:18", "lease_end", "1697610805"),
call('DHCP_SERVER_IPV4_LEASE|Vlan1000|10:70:fd:b6:13:17', 'ip', '192.168.0.131') call("DHCP_SERVER_IPV4_LEASE|Vlan1000|10:70:fd:b6:13:18", "ip", "193.168.0.132")
]) ])
kea_lease_handler.update_lease() kea_lease_handler.update_lease()
mock_sleep.assert_called_once_with(2) mock_sleep.assert_called_once_with(2)

View File

@ -0,0 +1,129 @@
import psutil
import pytest
import subprocess
import sys
import time
from common_utils import mock_get_config_db_table, MockProc, MockPopen
from dhcp_server.common.utils import DhcpDbConnector
from dhcp_server.dhcprelayd.dhcprelayd import DhcpRelayd, KILLED_OLD, NOT_KILLED, NOT_FOUND_PROC
from dhcp_server.common.dhcp_db_monitor import DhcpRelaydDbMonitor
from swsscommon import swsscommon
from unittest.mock import patch, call
def test_start(mock_swsscommon_dbconnector_init):
with patch.object(DhcpRelayd, "refresh_dhcrelay", return_value=None) as mock_refresh, \
patch.object(DhcpRelaydDbMonitor, "subscribe_table", return_value=None) as mock_subscribe:
dhcp_db_connector = DhcpDbConnector()
dhcprelayd = DhcpRelayd(dhcp_db_connector)
dhcprelayd.start()
mock_refresh.assert_called_once_with()
mock_subscribe.assert_called_once_with()
def test_refresh_dhcrelay(mock_swsscommon_dbconnector_init):
with patch.object(DhcpRelayd, "_get_dhcp_server_ip", return_value="240.127.1.2"), \
patch.object(DhcpDbConnector, "get_config_db_table", side_effect=mock_get_config_db_table), \
patch.object(DhcpRelayd, "_start_dhcrelay_process", return_value=None), \
patch.object(DhcpRelayd, "_start_dhcpmon_process", return_value=None):
dhcp_db_connector = DhcpDbConnector()
dhcprelayd = DhcpRelayd(dhcp_db_connector)
dhcprelayd.refresh_dhcrelay()
@pytest.mark.parametrize("new_dhcp_interfaces", [[], ["Vlan1000"], ["Vlan1000", "Vlan2000"]])
@pytest.mark.parametrize("kill_res", [KILLED_OLD, NOT_KILLED, NOT_FOUND_PROC])
@pytest.mark.parametrize("proc_status", [psutil.STATUS_ZOMBIE, psutil.STATUS_RUNNING])
def test_start_dhcrelay_process(mock_swsscommon_dbconnector_init, new_dhcp_interfaces, kill_res, proc_status):
with patch.object(DhcpRelayd, "_kill_exist_relay_releated_process", return_value=kill_res), \
patch.object(subprocess, "Popen", return_value=MockPopen(999)) as mock_popen, \
patch.object(time, "sleep"), \
patch("dhcp_server.dhcprelayd.dhcprelayd.terminate_proc", return_value=None) as mock_terminate, \
patch.object(psutil.Process, "__init__", return_value=None), \
patch.object(psutil.Process, "status", return_value=proc_status), \
patch.object(sys, "exit") as mock_exit:
dhcp_db_connector = DhcpDbConnector()
dhcprelayd = DhcpRelayd(dhcp_db_connector)
dhcprelayd._start_dhcrelay_process(new_dhcp_interfaces, "240.127.1.2", False)
if len(new_dhcp_interfaces) == 0 or kill_res == NOT_KILLED:
mock_popen.assert_not_called()
else:
call_param = ["/usr/sbin/dhcrelay", "-d", "-m", "discard", "-a", "%h:%p", "%P", "--name-alias-map-file",
"/tmp/port-name-alias-map.txt"]
for interface in new_dhcp_interfaces:
call_param += ["-id", interface]
call_param += ["-iu", "docker0", "240.127.1.2"]
mock_popen.assert_called_once_with(call_param)
if len(new_dhcp_interfaces) != 0 and kill_res != NOT_KILLED and proc_status == psutil.STATUS_ZOMBIE:
mock_terminate.assert_called_once()
mock_exit.assert_called_once_with(1)
else:
mock_terminate.assert_not_called()
mock_exit.assert_not_called()
@pytest.mark.parametrize("new_dhcp_interfaces_list", [[], ["Vlan1000"], ["Vlan1000", "Vlan2000"]])
@pytest.mark.parametrize("kill_res", [KILLED_OLD, NOT_KILLED, NOT_FOUND_PROC])
@pytest.mark.parametrize("proc_status", [psutil.STATUS_ZOMBIE, psutil.STATUS_RUNNING])
def test_start_dhcpmon_process(mock_swsscommon_dbconnector_init, new_dhcp_interfaces_list, kill_res, proc_status):
new_dhcp_interfaces = set(new_dhcp_interfaces_list)
with patch.object(DhcpRelayd, "_kill_exist_relay_releated_process", return_value=kill_res), \
patch.object(subprocess, "Popen", return_value=MockPopen(999)) as mock_popen, \
patch.object(time, "sleep"), \
patch("dhcp_server.dhcprelayd.dhcprelayd.terminate_proc", return_value=None) as mock_terminate, \
patch.object(psutil.Process, "__init__", return_value=None), \
patch.object(psutil.Process, "status", return_value=proc_status):
dhcp_db_connector = DhcpDbConnector()
dhcprelayd = DhcpRelayd(dhcp_db_connector)
dhcprelayd._start_dhcpmon_process(new_dhcp_interfaces, False)
if len(new_dhcp_interfaces) == 0 or kill_res == NOT_KILLED:
mock_popen.assert_not_called()
else:
calls = []
for interface in new_dhcp_interfaces:
call_param = ["/usr/sbin/dhcpmon", "-id", interface, "-iu", "docker0", "-im", "eth0"]
calls.append(call(call_param))
mock_popen.assert_has_calls(calls)
if len(new_dhcp_interfaces) != 0 and kill_res != NOT_KILLED and proc_status == psutil.STATUS_ZOMBIE:
mock_terminate.assert_called_once()
else:
mock_terminate.assert_not_called()
@pytest.mark.parametrize("new_dhcp_interfaces_list", [[], ["Vlan1000"], ["Vlan1000", "Vlan2000"]])
@pytest.mark.parametrize("process_name", ["dhcrelay", "dhcpmon"])
@pytest.mark.parametrize("running_procs", [[], ["dhcrelay"], ["dhcpmon"], ["dhcrelay", "dhcpmon"]])
@pytest.mark.parametrize("force_kill", [True, False])
def test_kill_exist_relay_releated_process(mock_swsscommon_dbconnector_init, new_dhcp_interfaces_list, process_name,
running_procs, force_kill):
new_dhcp_interfaces = set(new_dhcp_interfaces_list)
process_iter_ret = []
for running_proc in running_procs:
process_iter_ret.append(MockProc(running_proc))
with patch.object(psutil, "process_iter", return_value=process_iter_ret):
dhcp_db_connector = DhcpDbConnector()
dhcprelayd = DhcpRelayd(dhcp_db_connector)
res = dhcprelayd._kill_exist_relay_releated_process(new_dhcp_interfaces, process_name, force_kill)
if force_kill and process_name in running_procs:
assert res == KILLED_OLD
elif new_dhcp_interfaces_list == ["Vlan1000"] and process_name in running_procs:
assert res == NOT_KILLED
elif process_name not in running_procs:
assert res == NOT_FOUND_PROC
elif new_dhcp_interfaces_list != ["Vlan1000"]:
assert res == KILLED_OLD
@pytest.mark.parametrize("get_res", [(1, "240.127.1.2"), (0, None)])
def test_get_dhcp_server_ip(mock_swsscommon_dbconnector_init, mock_swsscommon_table_init, get_res):
with patch.object(swsscommon.Table, "hget", return_value=get_res), \
patch.object(time, "sleep") as mock_sleep, \
patch.object(sys, "exit") as mock_exit:
dhcp_db_connector = DhcpDbConnector()
dhcprelayd = DhcpRelayd(dhcp_db_connector)
ret = dhcprelayd._get_dhcp_server_ip()
if get_res[0] == 1:
assert ret == get_res[1]
else:
mock_exit.assert_called_once_with(1)
mock_sleep.assert_has_calls([call(10) for _ in range(10)])

View File

@ -1,15 +1,22 @@
import pytest import pytest
import psutil import psutil
import signal import signal
from dhcp_server.dhcp_server_utils import DhcpDbConnector import sys
from dhcp_server.dhcp_cfggen import DhcpServCfgGenerator import time
from dhcp_server.dhcpservd import DhcpServd from common_utils import MockProc
from dhcp_server.common.utils import DhcpDbConnector
from dhcp_server.dhcpservd.dhcp_cfggen import DhcpServCfgGenerator
from dhcp_server.dhcpservd.dhcpservd import DhcpServd
from swsscommon import swsscommon
from unittest.mock import patch, call, MagicMock from unittest.mock import patch, call, MagicMock
AF_INET = 2
AF_INET6 = 10
def test_dump_dhcp4_config(mock_swsscommon_dbconnector_init): def test_dump_dhcp4_config(mock_swsscommon_dbconnector_init):
with patch("dhcp_server.dhcp_cfggen.DhcpServCfgGenerator.generate", return_value="dummy_config") as mock_generate, \ with patch("dhcp_server.dhcpservd.dhcp_cfggen.DhcpServCfgGenerator.generate", return_value="dummy_config") as mock_generate, \
patch("dhcp_server.dhcpservd.DhcpServd._notify_kea_dhcp4_proc", MagicMock()) as mock_notify_kea_dhcp4_proc: patch("dhcp_server.dhcpservd.dhcpservd.DhcpServd._notify_kea_dhcp4_proc", MagicMock()) as mock_notify_kea_dhcp4_proc:
dhcp_db_connector = DhcpDbConnector() dhcp_db_connector = DhcpDbConnector()
dhcp_cfg_generator = DhcpServCfgGenerator(dhcp_db_connector, dhcp_cfg_generator = DhcpServCfgGenerator(dhcp_db_connector,
port_map_path="tests/test_data/port-name-alias-map.txt", port_map_path="tests/test_data/port-name-alias-map.txt",
@ -40,21 +47,45 @@ def test_notify_kea_dhcp4_proc(process_list, mock_swsscommon_dbconnector_init, m
mock_send_signal.assert_not_called() mock_send_signal.assert_not_called()
@pytest.mark.parametrize("mock_intf", [True, False])
def test_update_dhcp_server_ip(mock_swsscommon_dbconnector_init, mock_parse_port_map_alias, mock_get_render_template,
mock_intf):
mock_interface = {} if not mock_intf else {
"eth0": [
MockIntf(AF_INET6, "fd00::2"),
MockIntf(AF_INET, "240.127.1.2")
]
}
with patch.object(psutil, "net_if_addrs", return_value=mock_interface), \
patch.object(swsscommon.DBConnector, "hset") as mock_hset, \
patch.object(time, "sleep") as mock_sleep, \
patch.object(sys, "exit") as mock_exit:
dhcp_db_connector = DhcpDbConnector()
dhcp_cfg_generator = DhcpServCfgGenerator(dhcp_db_connector)
dhcpservd = DhcpServd(dhcp_cfg_generator, dhcp_db_connector)
dhcpservd._update_dhcp_server_ip()
if mock_intf:
mock_hset.assert_has_calls([
call("DHCP_SERVER_IPV4_SERVER_IP|eth0", "ip", "240.127.1.2")
])
else:
mock_hset.assert_not_called()
mock_exit.assert_called_once_with(1)
mock_sleep.assert_has_calls([call(5) for _ in range(10)])
def test_start(mock_swsscommon_dbconnector_init, mock_parse_port_map_alias, mock_get_render_template): def test_start(mock_swsscommon_dbconnector_init, mock_parse_port_map_alias, mock_get_render_template):
with patch.object(DhcpServd, "dump_dhcp4_config") as mock_dump: with patch.object(DhcpServd, "dump_dhcp4_config") as mock_dump, \
patch.object(DhcpServd, "_update_dhcp_server_ip") as mock_update_dhcp_server_ip:
dhcp_db_connector = DhcpDbConnector() dhcp_db_connector = DhcpDbConnector()
dhcp_cfg_generator = DhcpServCfgGenerator(dhcp_db_connector) dhcp_cfg_generator = DhcpServCfgGenerator(dhcp_db_connector)
dhcpservd = DhcpServd(dhcp_cfg_generator, dhcp_db_connector) dhcpservd = DhcpServd(dhcp_cfg_generator, dhcp_db_connector)
dhcpservd.start() dhcpservd.start()
mock_dump.assert_called_once_with() mock_dump.assert_called_once_with()
mock_update_dhcp_server_ip.assert_called_once_with()
class MockProc(object): class MockIntf(object):
def __init__(self, name): def __init__(self, family, address):
self.proc_name = name self.family = family
self.address = address
def name(self):
return self.proc_name
def send_signal(self, sig_num):
pass

View File

@ -1,4 +1,4 @@
import dhcp_server.dhcp_server_utils as dhcp_server_utils import dhcp_server.common.utils as utils
import ipaddress import ipaddress
import pytest import pytest
from swsscommon import swsscommon from swsscommon import swsscommon
@ -29,7 +29,7 @@ interval_test_data = {
def test_construct_without_sock(mock_swsscommon_dbconnector_init): def test_construct_without_sock(mock_swsscommon_dbconnector_init):
dhcp_server_utils.DhcpDbConnector() utils.DhcpDbConnector()
mock_swsscommon_dbconnector_init.assert_has_calls([ mock_swsscommon_dbconnector_init.assert_has_calls([
call(swsscommon.CONFIG_DB, "127.0.0.1", 6379, 0), call(swsscommon.CONFIG_DB, "127.0.0.1", 6379, 0),
call(swsscommon.STATE_DB, "127.0.0.1", 6379, 0) call(swsscommon.STATE_DB, "127.0.0.1", 6379, 0)
@ -38,7 +38,7 @@ def test_construct_without_sock(mock_swsscommon_dbconnector_init):
def test_construct_sock(mock_swsscommon_dbconnector_init): def test_construct_sock(mock_swsscommon_dbconnector_init):
redis_sock = "/var/run/redis/redis.sock" redis_sock = "/var/run/redis/redis.sock"
dhcp_db_connector = dhcp_server_utils.DhcpDbConnector(redis_sock=redis_sock) dhcp_db_connector = utils.DhcpDbConnector(redis_sock=redis_sock)
assert dhcp_db_connector.redis_sock == redis_sock assert dhcp_db_connector.redis_sock == redis_sock
mock_swsscommon_dbconnector_init.assert_has_calls([ mock_swsscommon_dbconnector_init.assert_has_calls([
@ -48,15 +48,13 @@ def test_construct_sock(mock_swsscommon_dbconnector_init):
def test_get_config_db_table(mock_swsscommon_dbconnector_init, mock_swsscommon_table_init): def test_get_config_db_table(mock_swsscommon_dbconnector_init, mock_swsscommon_table_init):
dhcp_db_connector = dhcp_server_utils.DhcpDbConnector() dhcp_db_connector = utils.DhcpDbConnector()
with patch.object(swsscommon.Table, "getKeys", return_value=["key1", "key2"]) as mock_get_keys, \ with patch.object(swsscommon.Table, "getKeys", return_value=["key1", "key2"]) as mock_get_keys, \
patch.object(dhcp_server_utils, "get_entry", return_value={"list": "1,2", "value": "3,4"}), \ patch.object(utils, "get_entry", return_value={"list": "1,2", "value": "3,4"}), \
patch.object(swsscommon.Table, "hget", side_effect=mock_hget): patch.object(swsscommon.Table, "hget", side_effect=mock_hget):
ret = dhcp_db_connector.get_config_db_table("VLAN") ret = dhcp_db_connector.get_config_db_table("VLAN")
mock_swsscommon_table_init.assert_called_once_with(dhcp_db_connector.config_db, "VLAN") mock_swsscommon_table_init.assert_called_once_with(dhcp_db_connector.config_db, "VLAN")
print(ret)
mock_get_keys.assert_called_once_with() mock_get_keys.assert_called_once_with()
print(ret)
assert ret == { assert ret == {
"key1": {"list": ["1", "2"], "value": "3,4"}, "key1": {"list": ["1", "2"], "value": "3,4"},
"key2": {"list": ["1", "2"], "value": "3,4"} "key2": {"list": ["1", "2"], "value": "3,4"}
@ -67,7 +65,7 @@ def test_get_config_db_table(mock_swsscommon_dbconnector_init, mock_swsscommon_t
def test_merge_intervals(test_type): def test_merge_intervals(test_type):
intervals = convert_ip_address_intervals(interval_test_data[test_type]["intervals"]) intervals = convert_ip_address_intervals(interval_test_data[test_type]["intervals"])
expected_res = convert_ip_address_intervals(interval_test_data[test_type]["expected_res"]) expected_res = convert_ip_address_intervals(interval_test_data[test_type]["expected_res"])
assert dhcp_server_utils.merge_intervals(intervals) == expected_res assert utils.merge_intervals(intervals) == expected_res
def mock_hget(_, field): def mock_hget(_, field):