[dhcp_relay] Use dhcprelayd to manage critical processes (#17236)

Modify j2 template files in docker-dhcp-relay. Add dhcprelayd to group dhcp-relay instead of isc-dhcp-relay-VlanXXX, which would make dhcprelayd to become critical process.
In dhcprelayd, subscribe FEATURE table to check whether dhcp_server feature is enabled.
2.1 If dhcp_server feature is disabled, means we need original dhcp_relay functionality, dhcprelayd would do nothing. Because dhcrelay/dhcpmon configuration is generated in supervisord configuration, they will automatically run.
2.2 If dhcp_server feature is enabled, dhcprelayd will stop dhcpmon/dhcrelay processes started by supervisord and subscribe dhcp_server related tables in config_db to start dhcpmon/dhcrelay processes.
2.3 While dhcprelayd running, it will regularly check feature status (by default per 5s) and would encounter below 4 state change about dhcp_server feature:
A) disabled -> enabled
In this scenario, dhcprelayd will subscribe dhcp_server related tables and stop dhcpmon/dhcrelay processes started by supervisord and start new pair of dhcpmon/dhcrelay processes. After this, dhcpmon/dhcrelay processes are totally managed by dhcprelayd.
B) enabled -> enabled
In this scenaro, dhcprelayd will monitor db changes in dhcp_server related tables to determine whether to restart dhcpmon/dhrelay processes.
C) enabled -> disabled
In this scenario, dhcprelayd would unsubscribe dhcp_server related tables and kill dhcpmon/dhcrelay processes started by itself. And then dhcprelayd will start dhcpmon/dhcrelay processes via supervisorctl.
D) disabled -> disabled
dhcprelayd will check whether dhcrelay processes running status consistent with supervisord configuration file. If they are not consistent, dhcprelayd will kill itself, then dhcp_relay container will stop because dhcprelayd is critical process.
This commit is contained in:
Yaqiang Zhu 2023-11-27 12:30:01 -05:00 committed by GitHub
parent 49dd425603
commit da80593ecb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 506 additions and 119 deletions

View File

@ -13,13 +13,11 @@ 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" %}\
RUN apt-get install -y libjsoncpp-dev \
python3-dev \
build-essential{%- endif %}
build-essential
{% if INCLUDE_DHCP_SERVER == "y" -%}
RUN pip3 install psutil
{%- endif %}
RUN apt-get install -y libjsoncpp-dev
@ -40,10 +38,8 @@ RUN apt-get install -y libjsoncpp-dev
{% 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 && \

View File

@ -1,18 +1,10 @@
[group:dhcp-relay]
programs=
programs=dhcprelayd,
{%- 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 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 }}
{%- endif %}
{% if DHCP_RELAY and vlan_name in DHCP_RELAY and DHCP_RELAY[vlan_name]['dhcpv6_servers']|length > 0 %}
{% set _dummy = relay_for_ipv6.update({'flag': True}) %}
{%- endif %}

View File

@ -39,10 +39,6 @@ stderr_logfile=syslog
dependent_startup=true
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 VLAN_INTERFACE %}
{# Count how many VLANs require a DHCP relay agent... #}
@ -63,16 +59,14 @@ dependent_startup_wait_for=rsyslogd:running
{# Create a program entry for each DHCP relay agent instance #}
{% set relay_for_ipv4 = { 'flag': False } %}
{% set relay_for_ipv6 = { 'flag': False } %}
{% if not dhcp_server_ipv4_enabled %}
{% for vlan_name in VLAN_INTERFACE %}
{% include 'dhcpv4-relay.agents.j2' %}
{% endfor %}
{% endif %}
{% include 'dhcpv6-relay.agents.j2' %}
{% if not dhcp_server_ipv4_enabled %}
{% include 'dhcp-relay.monitors.j2' %}
{% else %}
{% endif %}
{% endif %}
[program:dhcprelayd]
command=/usr/local/bin/dhcprelayd
priority=3
@ -82,6 +76,3 @@ stdout_logfile=syslog
stderr_logfile=syslog
dependent_startup=true
dependent_startup_wait_for=start:exited
{% endif %}
{% endif %}
{% endif %}

View File

@ -17,9 +17,7 @@ $(DOCKER_DHCP_RELAY)_LOAD_DOCKERS = $(DOCKER_CONFIG_ENGINE_BULLSEYE)
$(DOCKER_DHCP_RELAY)_INSTALL_PYTHON_WHEELS = $(SONIC_UTILITIES_PY3)
$(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)_PACKAGE_NAME = dhcp-relay

View File

@ -5,6 +5,4 @@ $(SONIC_DHCP_SERVER_PY3)_SRC_PATH = $(SRC_PATH)/sonic-dhcp-server
$(SONIC_DHCP_SERVER_PY3)_DEPENDS += $(SONIC_PY_COMMON_PY3)
$(SONIC_DHCP_SERVER_PY3)_DEBS_DEPENDS = $(LIBSWSSCOMMON) $(PYTHON3_SWSSCOMMON)
$(SONIC_DHCP_SERVER_PY3)_PYTHON_VERSION = 3
ifeq ($(INCLUDE_DHCP_SERVER), y)
SONIC_PYTHON_WHEELS += $(SONIC_DHCP_SERVER_PY3)
endif

View File

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

View File

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

View File

@ -40,7 +40,7 @@ dependent_startup=true
dependent_startup_wait_for=rsyslogd:running
[group:dhcp-relay]
programs=isc-dhcpv4-relay-Vlan1000,dhcp6relay
programs=dhcprelayd,dhcp6relay
[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
@ -76,4 +76,12 @@ stderr_logfile=syslog
dependent_startup=true
dependent_startup_wait_for=isc-dhcpv4-relay-Vlan1000:running
[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

@ -40,7 +40,7 @@ dependent_startup=true
dependent_startup_wait_for=rsyslogd:running
[group:dhcp-relay]
programs=isc-dhcpv4-relay-Vlan1000,isc-dhcpv4-relay-Vlan2000,dhcp6relay
programs=dhcprelayd,dhcp6relay
[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
@ -96,4 +96,12 @@ stderr_logfile=syslog
dependent_startup=true
dependent_startup_wait_for=isc-dhcpv4-relay-Vlan2000:running
[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

@ -40,7 +40,7 @@ dependent_startup=true
dependent_startup_wait_for=rsyslogd:running
[group:dhcp-relay]
programs=isc-dhcpv4-relay-Vlan1000,dhcp6relay
programs=dhcprelayd,dhcp6relay
[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
@ -76,4 +76,12 @@ stderr_logfile=syslog
dependent_startup=true
dependent_startup_wait_for=isc-dhcpv4-relay-Vlan1000:running
[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

@ -40,7 +40,7 @@ dependent_startup=true
dependent_startup_wait_for=rsyslogd:running
[group:dhcp-relay]
programs=isc-dhcpv4-relay-Vlan1000,isc-dhcpv4-relay-Vlan2000,dhcp6relay
programs=dhcprelayd,dhcp6relay
[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
@ -96,4 +96,12 @@ stderr_logfile=syslog
dependent_startup=true
dependent_startup_wait_for=isc-dhcpv4-relay-Vlan2000:running
[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

@ -154,36 +154,16 @@ class TestJ2Files(TestCase):
def test_dhcp_relay(self):
# Test generation of wait_for_intf.sh
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')
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, 'wait_for_intf.sh'), self.output_file))
# 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')
argument = ['-m', self.t0_minigraph, '-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 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
template_path = os.path.join(self.test_dir, '..', '..', '..', 'dockers', 'docker-dhcp-relay',
'docker-dhcp-relay.supervisord.conf.j2')

View File

@ -12,6 +12,7 @@ DHCP_SERVER_IPV4_CUSTOMIZED_OPTIONS = "DHCP_SERVER_IPV4_CUSTOMIZED_OPTIONS"
VLAN = "VLAN"
VLAN_MEMBER = "VLAN_MEMBER"
VLAN_INTERFACE = "VLAN_INTERFACE"
FEATURE = "FEATURE"
class ConfigDbEventChecker(object):
@ -343,6 +344,60 @@ class VlanMemberTableEventChecker(ConfigDbEventChecker):
return False
class DhcpServerFeatureStateChecker(ConfigDbEventChecker):
"""
This event checker interested in dhcp_server feature state change in FEATURE table
"""
table_name = FEATURE
def __init__(self, sel, db):
self.table_name = FEATURE
ConfigDbEventChecker.__init__(self, sel, db)
def _get_parameter(self, db_snapshot):
return ConfigDbEventChecker.get_parameter_by_name(db_snapshot, "dhcp_server_feature_enabled")
def _process_check(self, key, op, entry, dhcp_server_feature_enabled):
if key != "dhcp_server":
return False
if op == "DEL":
return dhcp_server_feature_enabled
for field, value in entry:
if field != "state":
continue
return value == "enabled" and not dhcp_server_feature_enabled or \
value == "disabled" and dhcp_server_feature_enabled
return False
def _enable_monitor_checkers(checker_names, checker_dict):
"""
Enable checkers
Args:
checker_names: set of tables checker to be enable
checker_dict: check_dict in monitor
"""
for checker in checker_names:
if checker not in checker_dict:
syslog.syslog(syslog.LOG_ERR, "Cannot find checker for {} in checker_dict".format(checker))
continue
checker_dict[checker].enable()
def _disable_monitor_checkers(checker_names, checker_dict):
"""
Disable checkers
Args:
checker_names: set contains name of tables need to be disable
checker_dict: check_dict in monitor
"""
for checker in checker_names:
if checker not in checker_dict:
syslog.syslog(syslog.LOG_ERR, "Cannot find checker for {} in checker_dict".format(checker))
continue
checker_dict[checker].disable()
class DhcpRelaydDbMonitor(object):
checker_dict = {}
@ -354,17 +409,21 @@ class DhcpRelaydDbMonitor(object):
for checker in checkers:
self.checker_dict[checker.get_class_name()] = checker
def enable_checker(self, checker_names):
def enable_checkers(self, checker_names):
"""
Enable checkers
Args:
checker_names: set of tables checker to be enable
"""
for table in checker_names:
if table not in self.checker_dict:
syslog.syslog(syslog.LOG_ERR, "Cannot find checker for {} in checker_dict".format(table))
continue
self.checker_dict[table].enable()
_enable_monitor_checkers(checker_names, self.checker_dict)
def disable_checkers(self, checker_names):
"""
Disable checkers
Args:
checker_names: set contains name of tables need to be disable
"""
_disable_monitor_checkers(checker_names, self.checker_dict)
def check_db_update(self, db_snapshot):
"""
@ -376,10 +435,13 @@ class DhcpRelaydDbMonitor(object):
"""
state, _ = self.sel.select(self.select_timeout)
if state == swsscommon.Select.TIMEOUT or state != swsscommon.Select.OBJECT:
return (False, False, False)
return (self.checker_dict["DhcpServerTableIntfEnablementEventChecker"].check_update_event(db_snapshot),
self.checker_dict["VlanTableEventChecker"].check_update_event(db_snapshot),
self.checker_dict["VlanIntfTableEventChecker"].check_update_event(db_snapshot))
return {}
check_res = {}
for name, checker in self.checker_dict.items():
if not checker.is_enabled():
continue
check_res[name] = checker.check_update_event(db_snapshot)
return check_res
class DhcpServdDbMonitor(object):
@ -399,11 +461,7 @@ class DhcpServdDbMonitor(object):
Args:
checker_names: set contains name of tables need to be disable
"""
for table in checker_names:
if table not in self.checker_dict:
syslog.syslog(syslog.LOG_ERR, "Cannot find checker for {} in checker_dict".format(table))
continue
self.checker_dict[table].disable()
_disable_monitor_checkers(checker_names, self.checker_dict)
def enable_checkers(self, checker_names):
"""
@ -411,11 +469,7 @@ class DhcpServdDbMonitor(object):
Args:
checker_names: set contains name of tables need to be enable
"""
for table in checker_names:
if table not in self.checker_dict:
syslog.syslog(syslog.LOG_ERR, "Cannot find checker for {} in checker_dict".format(table))
continue
self.checker_dict[table].enable()
_enable_monitor_checkers(checker_names, self.checker_dict)
def check_db_update(self, db_snapshot):
"""

View File

@ -1,4 +1,5 @@
import ipaddress
import psutil
import string
from swsscommon import swsscommon
@ -147,3 +148,18 @@ def _parse_table_to_dict(table):
new_entry[field] = value.split(",")
ret[key] = new_entry
return ret
def get_target_process_cmds(process_name):
"""
Get running process cmds
Args:
process_name: name of process
Returns:
List of cmds list
"""
res = []
for proc in psutil.process_iter():
if proc.name() == process_name:
res.append(proc.cmdline())
return res

View File

@ -2,22 +2,27 @@
# Currently if we run multiple dhcrelay processes, except for the last running process,
# others will not relay dhcp_release packet.
import psutil
import re
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.utils import DhcpDbConnector, terminate_proc, get_target_process_cmds
from dhcp_server.common.dhcp_db_monitor import DhcpRelaydDbMonitor, DhcpServerTableIntfEnablementEventChecker, \
VlanTableEventChecker, VlanIntfTableEventChecker
VlanTableEventChecker, VlanIntfTableEventChecker, DhcpServerFeatureStateChecker
REDIS_SOCK_PATH = "/var/run/redis/redis.sock"
SUPERVISORD_CONF_PATH = "/etc/supervisor/conf.d/docker-dhcp-relay.supervisord.conf"
DHCP_SERVER_IPV4_SERVER_IP = "DHCP_SERVER_IPV4_SERVER_IP"
DHCP_SERVER_IPV4 = "DHCP_SERVER_IPV4"
VLAN = "VLAN"
DEFAULT_SELECT_TIMEOUT = 5000 # millisecond
DHCP_SERVER_INTERFACE = "eth0"
DEFAULT_CHECKER = ["DhcpServerTableIntfEnablementEventChecker", "VlanTableEventChecker", "VlanIntfTableEventChecker"]
FEATURE_CHECKER = "DhcpServerFeatureStateChecker"
DHCP_SERVER_CHECKER = "DhcpServerTableIntfEnablementEventChecker"
VLAN_CHECKER = "VlanTableEventChecker"
VLAN_INTF_CHECKER = "VlanIntfTableEventChecker"
KILLED_OLD = 1
NOT_KILLED = 2
NOT_FOUND_PROC = 3
@ -25,8 +30,11 @@ NOT_FOUND_PROC = 3
class DhcpRelayd(object):
enabled_dhcp_interfaces = set()
dhcp_server_feature_enabled = None
dhcp_relay_supervisor_config = {}
supervisord_conf_path = ""
def __init__(self, db_connector, db_monitor):
def __init__(self, db_connector, db_monitor, supervisord_conf_path=SUPERVISORD_CONF_PATH):
"""
Args:
db_connector: db connector obj
@ -36,12 +44,21 @@ class DhcpRelayd(object):
self.last_refresh_time = None
self.dhcp_relayd_monitor = db_monitor
self.enabled_dhcp_interfaces = set()
self.dhcp_server_feature_enabled = None
self.supervisord_conf_path = supervisord_conf_path
def start(self):
"""
Start function
"""
self.refresh_dhcrelay()
self.dhcp_relay_supervisor_config = self._get_dhcp_relay_config()
self.dhcp_server_feature_enabled = self._is_dhcp_server_enabled()
# Sleep to wait dhcrelay process start
time.sleep(5)
if self.dhcp_server_feature_enabled:
# If dhcp_server is enabled, need to stop related relay processes start by supervisord
self._execute_supervisor_dhcp_relay_process("stop")
self.dhcp_relayd_monitor.enable_checkers([DHCP_SERVER_CHECKER, VLAN_CHECKER, VLAN_INTF_CHECKER])
def refresh_dhcrelay(self, force_kill=False):
"""
@ -74,14 +91,113 @@ class DhcpRelayd(object):
Wait function, check db change here
"""
while True:
res = (self.dhcp_relayd_monitor.check_db_update({"enabled_dhcp_interfaces": self.enabled_dhcp_interfaces}))
(dhcp_server_res, vlan_res, vlan_intf_res) = res
# vlan ip change require kill old dhcp_relay related processes
if vlan_intf_res:
check_param = {
"enabled_dhcp_interfaces": self.enabled_dhcp_interfaces,
"dhcp_server_feature_enabled": self.dhcp_server_feature_enabled
}
res = (self.dhcp_relayd_monitor.check_db_update(check_param))
dhcp_feature_statue_changed = False
if FEATURE_CHECKER in res:
dhcp_feature_statue_changed = res[FEATURE_CHECKER]
self.dhcp_server_feature_enabled = not self.dhcp_server_feature_enabled if dhcp_feature_statue_changed \
else self.dhcp_server_feature_enabled
# If dhcp_server feature is enabled, dhcprelayd will manage dhcpmon/dhcrelay process
if self.dhcp_server_feature_enabled:
# disabled -> enabled, we need to enable dhcp_server related checkers and do refresh processes
if dhcp_feature_statue_changed:
self.dhcp_relayd_monitor.enable_checkers([DHCP_SERVER_CHECKER, VLAN_CHECKER, VLAN_INTF_CHECKER])
# Stop dhcrelay process
self._execute_supervisor_dhcp_relay_process("stop")
self.refresh_dhcrelay()
# enabled -> enabled, just need to check dhcp_server related tables to see whether need to refresh
else:
# Check vlan_interface table change, if it changed, need to refresh with force kill
if res.get(VLAN_INTF_CHECKER, False):
self.refresh_dhcrelay(True)
elif dhcp_server_res or vlan_res:
elif res.get(VLAN_CHECKER, False) or res.get(DHCP_SERVER_CHECKER, False):
self.refresh_dhcrelay(False)
# If dhcp_server feature is disabled, dhcprelayd will checke whether dhcpmon/dhcrelay processes,
# if they are not running as expected, dhcprelayd will kill itself to make dhcp_relay container restart.
else:
# enabled -> disabled, we need to disable dhcp_server related checkers and start dhcrelay/dhcpmon
# processes follow supervisord configuration
if dhcp_feature_statue_changed:
self.dhcp_relayd_monitor.disable_checkers([DHCP_SERVER_CHECKER, VLAN_CHECKER, VLAN_INTF_CHECKER])
self._kill_exist_relay_releated_process([], "dhcpmon", True)
self._kill_exist_relay_releated_process([], "dhcrelay", True)
self._execute_supervisor_dhcp_relay_process("start")
# disabled -> disabled, to check whether dhcpmon/dhcrelay running status consistent with supervisord
# configuration
else:
self._check_dhcp_relay_processes()
def _is_dhcp_server_enabled(self):
"""
Check whether dhcp_server feature is enabled via running config_db
Returns:
If dhcp_server feature is enabled, return True. Else, return False
"""
feature_table = self.db_connector.get_config_db_table("FEATURE")
return feature_table.get("dhcp_server", {}).get("state", "disabled") == "enabled"
def _execute_supervisor_dhcp_relay_process(self, op):
"""
Start or stop relay releated processes managed by supervisord
Args:
op: string of operation, require to be "start" or "stop"
"""
if op not in ["stop", "start"]:
syslog.syslog(syslog.LOG_ERR, "Error operation: {}".format(op))
sys.exit(1)
for program in self.dhcp_relay_supervisor_config.keys():
cmds = ["supervisorctl", op, program]
syslog.syslog(syslog.LOG_INFO, "Starting stop {} by: {}".format(program, cmds))
res = subprocess.run(cmds, check=True)
if res.returncode != 0:
syslog.syslog(syslog.LOG_ERR, "Error in execute: {}".format(res))
sys.exit(1)
syslog.syslog(syslog.LOG_INFO, "Program {} stopped successfully".format(program))
def _check_dhcp_relay_processes(self):
"""
Check whether dhcrelay running as expected, if not, dhcprelayd will exit with code 1
"""
running_cmds = get_target_process_cmds("dhcrelay")
running_cmds.sort()
expected_cmds = [value for key, value in self.dhcp_relay_supervisor_config.items() if "isc-dhcpv4-relay" in key]
expected_cmds.sort()
if running_cmds != expected_cmds:
syslog.syslog(syslog.LOG_ERR, "Running processes is not as expected! Runnning: {}. Expected: {}"
.format(running_cmds, expected_cmds))
sys.exit(1)
def _get_dhcp_relay_config(self):
"""
Get supervisord configuration for dhcrelay/dhcpmon
Returns:
Dict of cmds, sample:{
'isc-dhcpv4-relay-Vlan1000': [
'/usr/sbin/dhcrelay', '-d', '-m', 'discard', '-a', '%h:%p', '%P', '--name-alias-map-file',
'/tmp/port-name-alias-map.txt', '-id', 'Vlan1000', '-iu', 'PortChannel101', '-iu',
'PortChannel102', '-iu', 'PortChannel103', '-iu', 'PortChannel104', '192.0.0.1', '192.0.0.2',
'192.0.0.3', '192.0.0.4'
],
'dhcpmon-Vlan1000': [
'/usr/sbin/dhcpmon', '-id', 'Vlan1000', '-iu', 'PortChannel101', '-iu', 'PortChannel102', '-iu',
'PortChannel103', '-iu', 'PortChannel104', '-im', 'eth0'
]
}
"""
res = {}
with open(self.supervisord_conf_path, "r") as conf_file:
content = conf_file.read()
cmds = re.findall(r"\[program:((isc-dhcpv4-relay|dhcpmon)-.+)\]\ncommand=(.+)", content)
for cmd in cmds:
key = "dhcpmon:{}".format(cmd[0]) if "dhcpmon" in cmd[0] else cmd[0]
res[key] = cmd[2].replace("%%", "%").split(" ")
return res
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)
@ -184,8 +300,9 @@ def main():
checkers.append(DhcpServerTableIntfEnablementEventChecker(sel, dhcp_db_connector.config_db))
checkers.append(VlanIntfTableEventChecker(sel, dhcp_db_connector.config_db))
checkers.append(VlanTableEventChecker(sel, dhcp_db_connector.config_db))
checkers.append(DhcpServerFeatureStateChecker(sel, dhcp_db_connector.config_db))
db_monitor = DhcpRelaydDbMonitor(dhcp_db_connector, sel, checkers, DEFAULT_SELECT_TIMEOUT)
db_monitor.enable_checker(DEFAULT_CHECKER)
db_monitor.enable_checkers([FEATURE_CHECKER])
dhcprelayd = DhcpRelayd(dhcp_db_connector, db_monitor)
dhcprelayd.start()
dhcprelayd.wait()

View File

@ -108,3 +108,8 @@ def get_subscribe_table_tested_data(test_name):
data["table"][i][2] = tuple(data["table"][i][2])
data["table"][i] = tuple(data["table"][i])
return tested_data
class MockSubprocessRes(object):
def __init__(self, returncode):
self.returncode = returncode

View File

@ -242,5 +242,52 @@
],
"exp_res": false
}
],
"test_feature_update": [
{
"table": [
["dhcp_server", "SET", [["state", "disabled"]]]
],
"exp_res": {
"pre_enabled": true,
"pre_disabled": false
}
},
{
"table": [
["dhcp_server", "SET", [["state", "enabled"]]]
],
"exp_res": {
"pre_enabled": false,
"pre_disabled": true
}
},
{
"table": [
["dhcp_server", "SET", [["states", "enabled"]]]
],
"exp_res": {
"pre_enabled": false,
"pre_disabled": false
}
},
{
"table": [
["dhcp_server", "DEL", [[]]]
],
"exp_res": {
"pre_enabled": true,
"pre_disabled": false
}
},
{
"table": [
["dhcp_relay", "SET", [["state", "disabled"]]]
],
"exp_res": {
"pre_enabled": false,
"pre_disabled": false
}
}
]
}

View File

@ -39,11 +39,9 @@ stderr_logfile=syslog
dependent_startup=true
dependent_startup_wait_for=rsyslogd:running
[group:dhcp-relay]
programs=dhcprelayd,dhcp6relay
[program:dhcp6relay]
command=/usr/sbin/dhcp6relay
[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 PortChannel101 -iu PortChannel102 -iu PortChannel103 -iu PortChannel104 192.0.0.1 192.0.0.2 192.0.0.3 192.0.0.4
priority=3
autostart=false
autorestart=false
@ -60,5 +58,31 @@ autorestart=false
stdout_logfile=syslog
stderr_logfile=syslog
dependent_startup=true
dependent_startup_wait_for=isc-dhcpv4-relay-Vlan1000:running
[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
[group:dhcp-relay]
programs=dhcprelayd,dhcp6relay
[group:dhcpmon]
programs=dhcpmon-Vlan1000
[program:dhcpmon-Vlan1000]
command=/usr/sbin/dhcpmon -id Vlan1000 -iu PortChannel101 -iu PortChannel102 -iu PortChannel103 -iu PortChannel104 -im eth0
priority=4
autostart=false
autorestart=false
stdout_logfile=syslog
stderr_logfile=syslog
dependent_startup=true
dependent_startup_wait_for=isc-dhcpv4-relay-Vlan1000:running

View File

@ -5,16 +5,18 @@ from common_utils import MockSubscribeTable, get_subscribe_table_tested_data, \
from dhcp_server.common.dhcp_db_monitor import DhcpRelaydDbMonitor, DhcpServdDbMonitor, ConfigDbEventChecker, \
DhcpServerTableIntfEnablementEventChecker, DhcpServerTableCfgChangeEventChecker, \
DhcpPortTableEventChecker, DhcpRangeTableEventChecker, DhcpOptionTableEventChecker, \
VlanTableEventChecker, VlanMemberTableEventChecker, VlanIntfTableEventChecker
VlanTableEventChecker, VlanMemberTableEventChecker, VlanIntfTableEventChecker, DhcpServerFeatureStateChecker
from dhcp_server.common.utils import DhcpDbConnector
from swsscommon import swsscommon
from unittest.mock import patch, ANY, PropertyMock, MagicMock
@pytest.mark.parametrize("checker_enabled", [True, False])
@pytest.mark.parametrize("select_result", [swsscommon.Select.TIMEOUT, swsscommon.Select.OBJECT])
def test_dhcp_relayd_monitor_check_db_update(mock_swsscommon_dbconnector_init, select_result):
def test_dhcp_relayd_monitor_check_db_update(mock_swsscommon_dbconnector_init, select_result, checker_enabled):
with patch.object(DhcpServerTableIntfEnablementEventChecker, "check_update_event") \
as mock_check_update_event, \
patch.object(ConfigDbEventChecker, "is_enabled", return_value=checker_enabled), \
patch.object(VlanTableEventChecker, "check_update_event") as mock_check_vlan_update, \
patch.object(VlanIntfTableEventChecker, "check_update_event") as mock_check_vlan_intf_update, \
patch.object(swsscommon.Select, "select", return_value=(select_result, None)), \
@ -25,13 +27,13 @@ def test_dhcp_relayd_monitor_check_db_update(mock_swsscommon_dbconnector_init, s
dhcp_relayd_db_monitor = DhcpRelaydDbMonitor(db_connector, swsscommon.Select(), checkers)
tested_db_snapshot = {"enabled_dhcp_interfaces": "dummy"}
dhcp_relayd_db_monitor.check_db_update(tested_db_snapshot)
if select_result == swsscommon.Select.OBJECT:
mock_check_update_event.assert_called_once_with(tested_db_snapshot)
if select_result == swsscommon.Select.OBJECT and checker_enabled:
mock_check_vlan_update.assert_called_once_with(tested_db_snapshot)
mock_check_update_event.assert_called_once_with(tested_db_snapshot)
mock_check_vlan_intf_update.assert_called_once_with(tested_db_snapshot)
else:
mock_check_update_event.assert_not_called()
mock_check_vlan_update.assert_not_called()
mock_check_update_event.assert_not_called()
mock_check_vlan_intf_update.assert_not_called()
@ -40,13 +42,27 @@ def test_dhcp_relayd_enable_checker(tables, mock_swsscommon_dbconnector_init):
with patch.object(ConfigDbEventChecker, "enable") as mock_enable:
db_connector = DhcpDbConnector()
dhcp_relayd_db_monitor = DhcpRelaydDbMonitor(db_connector, None, [VlanTableEventChecker(None, None)])
dhcp_relayd_db_monitor.enable_checker(set(tables))
dhcp_relayd_db_monitor.enable_checkers(set(tables))
if "VlanTableEventChecker" in tables:
mock_enable.assert_called_once()
else:
mock_enable.assert_not_called()
@pytest.mark.parametrize("tables", [["VlanTableEventChecker"], ["dummy"]])
def test_dhcp_relayd_disable_checker(tables, mock_swsscommon_dbconnector_init):
with patch.object(ConfigDbEventChecker, "disable") as mock_disable:
db_connector = DhcpDbConnector()
checker = VlanTableEventChecker(None, None)
checker.is_enabled = True
dhcp_relayd_db_monitor = DhcpRelaydDbMonitor(db_connector, None, [checker])
dhcp_relayd_db_monitor.disable_checkers(set(tables))
if "VlanTableEventChecker" in tables:
mock_disable.assert_called_once()
else:
mock_disable.assert_not_called()
@pytest.mark.parametrize("select_result", [swsscommon.Select.TIMEOUT, swsscommon.Select.OBJECT])
@pytest.mark.parametrize("is_checker_enabled", [True, False])
def test_dhcp_servd_monitor_check_db_update(mock_swsscommon_dbconnector_init, select_result,
@ -349,3 +365,24 @@ def test_vlan_member_table_checker(mock_swsscommon_dbconnector_init, tested_data
assert check_res
else:
assert expected_res == check_res
@pytest.mark.parametrize("tested_db_snapshot", [{"dhcp_server_feature_enabled": True},
{"dhcp_server_feature_enabled": False}, {}])
@pytest.mark.parametrize("tested_data", get_subscribe_table_tested_data("test_feature_update"))
@pytest.mark.parametrize("enabled", [True, False])
def test_feature_table_checker(mock_swsscommon_dbconnector_init, tested_data, tested_db_snapshot, enabled):
with patch.object(ConfigDbEventChecker, "enable"), \
patch.object(ConfigDbEventChecker, "subscriber_state_table",
return_value=MockSubscribeTable(tested_data["table"]), new_callable=PropertyMock), \
patch.object(ConfigDbEventChecker, "enabled", return_value=enabled, new_callable=PropertyMock), \
patch.object(sys, "exit"):
sel = swsscommon.Select()
db_event_checker = DhcpServerFeatureStateChecker(sel, MagicMock())
check_res = db_event_checker.check_update_event(tested_db_snapshot)
if "dhcp_server_feature_enabled" not in tested_db_snapshot:
assert check_res
else:
expected_res = tested_data["exp_res"]["pre_enabled"] if tested_db_snapshot["dhcp_server_feature_enabled"] \
else tested_data["exp_res"]["pre_disabled"]
assert expected_res == check_res

View File

@ -3,21 +3,32 @@ import pytest
import subprocess
import sys
import time
from common_utils import mock_get_config_db_table, MockProc, MockPopen
from common_utils import mock_get_config_db_table, MockProc, MockPopen, MockSubprocessRes, mock_exit_func
from dhcp_server.common.utils import DhcpDbConnector
from dhcp_server.common.dhcp_db_monitor import ConfigDbEventChecker
from dhcp_server.common.dhcp_db_monitor import ConfigDbEventChecker, DhcpRelaydDbMonitor
from dhcp_server.dhcprelayd.dhcprelayd import DhcpRelayd, KILLED_OLD, NOT_KILLED, NOT_FOUND_PROC
from swsscommon import swsscommon
from unittest.mock import patch, call
from unittest.mock import patch, call, ANY, PropertyMock
def test_start(mock_swsscommon_dbconnector_init):
with patch.object(DhcpRelayd, "refresh_dhcrelay", return_value=None) as mock_refresh, \
patch.object(ConfigDbEventChecker, "enable"):
@pytest.mark.parametrize("dhcp_server_enabled", [True, False])
def test_start(mock_swsscommon_dbconnector_init, dhcp_server_enabled):
with patch.object(DhcpRelayd, "_get_dhcp_relay_config") as mock_get_config, \
patch.object(DhcpRelayd, "_is_dhcp_server_enabled", return_value=dhcp_server_enabled) as mock_enabled, \
patch.object(DhcpRelayd, "_execute_supervisor_dhcp_relay_process") as mock_execute, \
patch.object(DhcpRelaydDbMonitor, "enable_checkers") as mock_enable_checkers, \
patch.object(time, "sleep"):
dhcp_db_connector = DhcpDbConnector()
dhcprelayd = DhcpRelayd(dhcp_db_connector, None)
dhcprelayd = DhcpRelayd(dhcp_db_connector, DhcpRelaydDbMonitor)
dhcprelayd.start()
mock_refresh.assert_called_once_with()
mock_get_config.assert_called_once_with()
mock_enabled.assert_called_once_with()
if dhcp_server_enabled:
mock_execute.assert_called_once_with("stop")
mock_enable_checkers.assert_called_once_with([ANY, ANY, ANY])
else:
mock_execute.assert_not_called()
mock_enable_checkers.assert_not_called()
def test_refresh_dhcrelay(mock_swsscommon_dbconnector_init):
@ -131,3 +142,93 @@ def test_get_dhcp_server_ip(mock_swsscommon_dbconnector_init, mock_swsscommon_ta
else:
mock_exit.assert_called_once_with(1)
mock_sleep.assert_has_calls([call(10) for _ in range(10)])
tested_feature_table = [
{
"dhcp_server": {
"delayed": "True"
}
},
{
"dhcp_server": {
"state": "enabled"
}
},
{
"dhcp_server": {
"state": "disabled"
}
},
{}
]
@pytest.mark.parametrize("feature_table", tested_feature_table)
def test_is_dhcp_server_enabled(mock_swsscommon_dbconnector_init, mock_swsscommon_table_init, feature_table):
with patch.object(DhcpDbConnector, "get_config_db_table", return_value=feature_table):
dhcp_db_connector = DhcpDbConnector()
dhcprelayd = DhcpRelayd(dhcp_db_connector, None)
res = dhcprelayd._is_dhcp_server_enabled()
if "dhcp_server" in feature_table and "state" in feature_table["dhcp_server"] and \
feature_table["dhcp_server"]["state"] == "enabled":
assert res
else:
assert not res
@pytest.mark.parametrize("op", ["stop", "start", "starts"])
@pytest.mark.parametrize("return_code", [0, -1])
def test_execute_supervisor_dhcp_relay_process(mock_swsscommon_dbconnector_init, mock_swsscommon_table_init, op,
return_code):
with patch.object(sys, "exit", side_effect=mock_exit_func) as mock_exit, \
patch.object(subprocess, "run", return_value=MockSubprocessRes(return_code)) as mock_run, \
patch.object(DhcpRelayd, "dhcp_relay_supervisor_config", return_value={"dhcpmon-Vlan1000": ""},
new_callable=PropertyMock):
dhcp_db_connector = DhcpDbConnector()
dhcprelayd = DhcpRelayd(dhcp_db_connector, None)
try:
dhcprelayd._execute_supervisor_dhcp_relay_process(op)
except SystemExit:
mock_exit.assert_called_once_with(1)
assert op == "starts" or return_code != 0
else:
mock_run.assert_called_once_with(["supervisorctl", op, "dhcpmon-Vlan1000"], check=True)
@pytest.mark.parametrize("target_cmds", [[["/usr/bin/dhcrelay"]], [["/usr/bin/dhcpmon"]]])
def test_check_dhcp_relay_process(mock_swsscommon_dbconnector_init, mock_swsscommon_table_init, target_cmds):
exp_config = {"isc-dhcpv4-relay-Vlan1000": ["/usr/bin/dhcrelay"]}
with patch("dhcp_server.dhcprelayd.dhcprelayd.get_target_process_cmds", return_value=target_cmds), \
patch.object(DhcpRelayd, "dhcp_relay_supervisor_config",
return_value=exp_config, new_callable=PropertyMock), \
patch.object(sys, "exit", mock_exit_func):
dhcp_db_connector = DhcpDbConnector()
dhcprelayd = DhcpRelayd(dhcp_db_connector, None)
exp_cmds = [value for key, value in exp_config.items() if "isc-dhcpv4-relay" in key]
exp_cmds.sort()
try:
dhcprelayd._check_dhcp_relay_processes()
except SystemExit:
assert exp_cmds != target_cmds
else:
assert exp_cmds == target_cmds
def test_get_dhcp_relay_config(mock_swsscommon_dbconnector_init, mock_swsscommon_table_init):
with patch.object(DhcpRelayd, "supervisord_conf_path", return_value="tests/test_data/supervisor.conf",
new_callable=PropertyMock):
dhcp_db_connector = DhcpDbConnector()
dhcprelayd = DhcpRelayd(dhcp_db_connector, None)
res = dhcprelayd._get_dhcp_relay_config()
assert res == {
"isc-dhcpv4-relay-Vlan1000": [
"/usr/sbin/dhcrelay", "-d", "-m", "discard", "-a", "%h:%p", "%P", "--name-alias-map-file",
"/tmp/port-name-alias-map.txt", "-id", "Vlan1000", "-iu", "PortChannel101", "-iu", "PortChannel102",
"-iu", "PortChannel103", "-iu", "PortChannel104", "192.0.0.1", "192.0.0.2", "192.0.0.3", "192.0.0.4"
],
"dhcpmon:dhcpmon-Vlan1000": [
"/usr/sbin/dhcpmon", "-id", "Vlan1000", "-iu", "PortChannel101", "-iu", "PortChannel102", "-iu",
"PortChannel103", "-iu", "PortChannel104", "-im", "eth0"
]
}

View File

@ -1,8 +1,10 @@
import dhcp_server.common.utils as utils
import ipaddress
import psutil
import pytest
from swsscommon import swsscommon
from unittest.mock import patch, call
from common_utils import MockProc
from unittest.mock import patch, call, PropertyMock
interval_test_data = {
"ordered_with_overlap": {
@ -137,3 +139,8 @@ def convert_ip_address_intervals(intervals):
def test_validate_ttr_type(test_data):
res = utils.validate_str_type(test_data[0], test_data[1])
assert res == test_data[2]
def test_get_target_process_cmds():
with patch.object(psutil, "process_iter", return_value=[MockProc("dhcrelay", 1), MockProc("dhcpmon", 2)], new_callable=PropertyMock):
res = utils.get_target_process_cmds("dhcrelay")