[dhcp_server] Add dhcpservd to dhcp_server container (#16560)

Why I did it
Part implementation of dhcp_server. HLD: sonic-net/SONiC#1282.
Add dhcpservd to dhcp_server container.

How I did it
Add installing required pkg (psutil) in Dockerfile.
Add copying required file to container in Dockerfile (kea-dhcp related and dhcpservd related)
Add critical_process and supervisor config.
Add support for generating kea config (only in dhcpservd.py) and updating lease table (in dhcpservd.py and lease_update.sh)

How to verify it
Build image with setting INCLUDE_DHCP_SERVER to y and enabled dhcp_server feature after installed image, container start as expected.
Enter container and found that all processes defined in supervisor configuration running as expected.
Kill processes defined in critical_processes, container exist.
This commit is contained in:
Yaqiang Zhu 2023-10-21 00:52:05 +08:00 committed by GitHub
parent 1dd0becda0
commit 73dd38a5ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 2139 additions and 5 deletions

View File

@ -3,6 +3,7 @@ FROM docker-config-engine-bullseye-{{DOCKER_USERNAME}}:{{DOCKER_USERTAG}}
ARG docker_container_name
ARG image_version
RUN [ -f /etc/rsyslog.conf ] && sed -ri "s/%syslogtag%/$docker_container_name#%syslogtag%/;" /etc/rsyslog.conf
## Make apt-get non-interactive
ENV DEBIAN_FRONTEND=noninteractive
@ -13,6 +14,7 @@ ENV IMAGE_VERSION=$image_version
RUN apt-get update && \
apt-get install -f -y \
tcpdump \
python3-dev \
# For kea build environment
automake \
libtool \
@ -47,7 +49,9 @@ RUN echo "/usr/local/lib/kea/hooks" > /etc/ld.so.conf.d/kea.conf && \
RUN cd /usr/local/sbin && rm -f kea-admin kea-ctrl-agent kea-dhcp-ddns kea-dhcp6 keactrl
# Remove hook lib we don't need
RUN cd /usr/local/lib/kea/hooks && rm -f libdhcp_bootp.so libdhcp_flex_option.so libdhcp_stat_cmds.so
# RUN pip3 install psutil
RUN pip3 install psutil
# TODO issue on remote rsyslog server in non-host container
RUN rm -f /etc/supervisor/conf.d/containercfgd.conf
{% if docker_dhcp_server_debs.strip() -%}
# Copy locally-built Debian package dependencies
@ -57,12 +61,21 @@ RUN cd /usr/local/lib/kea/hooks && rm -f libdhcp_bootp.so libdhcp_flex_option.so
{{ install_debian_packages(docker_dhcp_server_debs.split(' ')) }}
{%- endif %}
{% if docker_dhcp_server_whls.strip() %}
# Copy locally-built Python wheel dependencies
{{ copy_files("python-wheels/", docker_dhcp_server_whls.split(' '), "/python-wheels/") }}
# Install locally-built Python wheel dependencies
{{ install_python_wheels(docker_dhcp_server_whls.split(' ')) }}
{% endif %}
# Remove build stuff we don't need
RUN apt-get remove -y devscripts \
automake \
libtool \
pkg-config \
build-essential \
python3-dev \
ccache
RUN apt-get clean -y && \
@ -70,10 +83,14 @@ RUN apt-get clean -y && \
apt-get autoremove -y && \
rm -rf /debs
COPY ["docker_init.sh", "/usr/bin/"]
COPY ["docker_init.sh", "start.sh", "/usr/bin/"]
COPY ["supervisord.conf", "/etc/supervisor/conf.d/"]
COPY ["files/supervisor-proc-exit-listener", "/usr/bin"]
COPY ["port-name-alias-map.txt.j2", "rsyslog/rsyslog.conf.j2", "kea-dhcp4.conf.j2", "/usr/share/sonic/templates/"]
COPY ["critical_processes", "/etc/supervisor/"]
COPY ["lease_update.sh", "/etc/kea/"]
COPY ["kea-dhcp4-init.conf", "/etc/kea/kea-dhcp4.conf"]
COPY ["cli", "/cli/"]
COPY ["rsyslog/default.conf", "/etc/rsyslog.d"]
ENTRYPOINT ["/usr/bin/docker_init.sh"]

View File

@ -0,0 +1 @@
group:dhcp-server-ipv4

View File

@ -1,5 +1,21 @@
#!/usr/bin/env bash
# Generate supervisord config file
mkdir -p /etc/supervisor/conf.d/
# Generate kea folder
mkdir -p /etc/kea/
udp_server_ip=$(ip -j -4 addr list lo scope host | jq -r -M '.[0].addr_info[0].local')
hostname=$(hostname)
# Generate the following files from templates:
# port-to-alias name map
sonic-cfggen -d -t /usr/share/sonic/templates/rsyslog.conf.j2 \
-a "{\"udp_server_ip\": \"$udp_server_ip\", \"hostname\": \"$hostname\"}" \
> /etc/rsyslog.conf
sonic-cfggen -d -t /usr/share/sonic/templates/port-name-alias-map.txt.j2,/tmp/port-name-alias-map.txt
# Make the script that waits for all interfaces to come up executable
chmod +x /etc/kea/lease_update.sh /usr/bin/start.sh
# The docker container should start this script as PID 1, so now that supervisord is
# properly configured, we exec /usr/local/bin/supervisord so that it runs as PID 1 for the
# duration of the container's lifetime

View File

@ -0,0 +1,40 @@
{
"Dhcp4": {
"hooks-libraries": [
{
"library": "/usr/local/lib/kea/hooks/libdhcp_run_script.so",
"parameters": {
"name": "/etc/kea/lease_update.sh",
"sync": false
}
}
],
"interfaces-config": {
"interfaces": ["eth0"]
},
"control-socket": {
"socket-type": "unix",
"socket-name": "/run/kea/kea4-ctrl-socket"
},
"lease-database": {
"type": "memfile",
"persist": true,
"name": "/tmp/kea-lease.csv",
"lfc-interval": 3600
},
"subnet4": [],
"loggers": [
{
"name": "kea-dhcp4",
"output_options": [
{
"output": "/tmp/kea-dhcp.log",
"pattern": "%-5p %m\n"
}
],
"severity": "INFO",
"debuglevel": 0
}
]
}
}

View File

@ -0,0 +1,87 @@
{%- set default_lease_time = 900 -%}
{
"Dhcp4": {
"hooks-libraries": [
{
"library": "/usr/local/lib/kea/hooks/libdhcp_run_script.so",
"parameters": {
"name": "{{ lease_update_script_path }}",
"sync": false
}
}
],
"interfaces-config": {
"interfaces": [
"eth0"
]
},
"control-socket": {
"socket-type": "unix",
"socket-name": "/run/kea/kea4-ctrl-socket"
},
"lease-database": {
"type": "memfile",
"persist": true,
"name": "{{ lease_path }}",
"lfc-interval": 3600
},
"subnet4": [
{%- set add_subnet_preceding_comma = { 'flag': False } %}
{%- for subnet_info in subnets %}
{%- if add_subnet_preceding_comma.flag -%},{%- endif -%}
{%- set _dummy = add_subnet_preceding_comma.update({'flag': True}) %}
{
"subnet": "{{ subnet_info["subnet"] }}",
"pools": [
{%- set add_pool_preceding_comma = { 'flag': False } %}
{%- for pool in subnet_info["pools"] %}
{%- if add_pool_preceding_comma.flag -%},{%- endif -%}
{%- set _dummy = add_pool_preceding_comma.update({'flag': True}) %}
{
"pool": "{{ pool["range"] }}",
"client-class": "{{ pool["client_class"] }}"
}
{%- endfor%}
],
"option-data": [
{
"name": "routers",
"data": "{{ subnet_info["gateway"] if "gateway" in subnet_info else subnet_info["server_id"] }}"
},
{
"name": "dhcp-server-identifier",
"data": "{{ subnet_info["server_id"] }}"
}
],
"valid-lifetime": {{ subnet_info["lease_time"] if "lease_time" in subnet_info else default_lease_time }},
"reservations": []
}
{%- endfor %}
],
"loggers": [
{
"name": "kea-dhcp4",
"output_options": [
{
"output": "/var/log/kea-dhcp.log",
"pattern": "%-5p %m\n"
}
],
"severity": "INFO",
"debuglevel": 0
}
]{%- if client_classes -%},
"client-classes": [
{%- set add_preceding_comma = { 'flag': False } %}
{%- for class in client_classes %}
{%- if add_preceding_comma.flag -%},{%- endif -%}
{%- set _dummy = add_preceding_comma.update({'flag': True}) %}
{
"name": "{{ class["name"] }}",
"test": "{{ class["condition"] }}"
}
{%- endfor %}
]
{%- endif %}
}
}

View File

@ -0,0 +1,12 @@
#!/bin/bash
# This script would run once kea-dhcp4 lease change (defined in kea-dhcp4.conf),
# it is to find running process dhcpservd.py, and send SIGUSR1 signal to this
# process to inform it to update lease table in state_db (defined in dhcpservd.py)
pid=`ps aux | grep 'dhcpservd' | grep -nv 'grep' | awk '{print $2}'`
if [ -z "$pid" ]; then
logger -p daemon.error Cannot find running dhcpservd.py.
else
# Send SIGUSR1 signal to dhcpservd.py
kill -s 10 ${pid}
fi

View File

@ -0,0 +1,5 @@
{# Generate port name-alias map for isc-dhcp-relay to parse. Each line contains one #}
{# name-alias pair of the form "<name> <alias>" #}
{% for port, config in PORT.items() %}
{{- port }} {% if "alias" in config %}{{ config["alias"] }}{% else %}{{ port }}{% endif %} {{- "\n" -}}
{% endfor -%}

View File

@ -0,0 +1,27 @@
#
# First some standard log files. Log by facility.
#
# Log all facilities to /var/log/syslog except cron, auth
# and authpriv. They are noisy - log them to their own files
*.*;cron,auth,authpriv.none -/var/log/syslog
auth,authpriv.* /var/log/auth.log
cron.* /var/log/cron.log
#
# Emergencies are sent to everybody logged in.
#
*.emerg :omusrmsg:*
# The named pipe /dev/xconsole is for the `xconsole' utility. To use it,
# you must invoke `xconsole' with the `-file' option:
#
# $ xconsole -file /dev/xconsole [...]
#
# NOTE: adjust the list below, or you'll go crazy if you have a reasonably
# busy site..
#
#daemon.*;mail.*;\
# news.err;\
# *.=debug;*.=info;\
# *.=notice;*.=warn |/dev/xconsole

View File

@ -0,0 +1,96 @@
###############################################################################
# Managed by Ansible
# file: ansible/roles/acs/templates/rsyslog.conf.j2
###############################################################################
#
# /etc/rsyslog.conf Configuration file for rsyslog.
#
# For more information see
# /usr/share/doc/rsyslog-doc/html/rsyslog_conf.html
#################
#### MODULES ####
#################
$ModLoad imuxsock # provides support for local system logging
{% set gconf = (SYSLOG_CONFIG | d({})).get('GLOBAL', {}) -%}
{% set rate_limit_interval = gconf.get('rate_limit_interval') %}
{% set rate_limit_burst = gconf.get('rate_limit_burst') %}
{% if rate_limit_interval is not none %}
$SystemLogRateLimitInterval {{ rate_limit_interval }}
{% endif %}
{% if rate_limit_burst is not none %}
$SystemLogRateLimitBurst {{ rate_limit_burst }}
{% endif %}
$ModLoad imklog # provides kernel logging support
#$ModLoad immark # provides --MARK-- message capability
# provides UDP syslog reception
$ModLoad imudp
$UDPServerAddress {{udp_server_ip}} #bind to localhost before udp server run
$UDPServerRun 514
# provides TCP syslog reception
#$ModLoad imtcp
#$InputTCPServerRun 514
###########################
#### GLOBAL DIRECTIVES ####
###########################
{% set format = gconf.get('format', 'standard') -%}
{% set fw_name = gconf.get('welf_firewall_name', hostname) -%}
#
# Use traditional timestamp format.
# To enable high precision timestamps, comment out the following line.
#
#$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat
# Define a custom template
$template SONiCFileFormat,"%TIMESTAMP%.%timestamp:::date-subseconds% %HOSTNAME% %syslogseverity-text:::uppercase% dhcp_server#%syslogtag%%msg:::sp-if-no-1st-sp%%msg:::drop-last-lf%\n"
$ActionFileDefaultTemplate SONiCFileFormat
template(name="WelfRemoteFormat" type="string" string="%TIMESTAMP% id=firewall time=\"%timereported\
:::date-year%-%timereported:::date-month%-%timereported:::date-day% %timereported:::date-hour%:%timereported:::date-minute%:%timereported\
:::date-second%\" fw=\"{{ fw_name }}\" pri=%syslogpriority% msg=\"%syslogtag%%msg:::sp-if-no-1st-sp%%msg:::drop-last-lf%\"\n")
#
# Set the default permissions for all log files.
#
$FileOwner root
$FileGroup adm
$FileCreateMode 0640
$DirCreateMode 0755
$Umask 0022
#
# Where to place spool and state files
#
$WorkDirectory /var/spool/rsyslog
#
# Include all config files in /etc/rsyslog.d/
#
$IncludeConfig /etc/rsyslog.d/*.conf
#
# Suppress duplicate messages and report "message repeated n times"
#
$RepeatedMsgReduction on
###############
#### RULES ####
###############
#
# Remote syslog logging
#
# The omfwd plug-in provides the core functionality of traditional message
# forwarding via UDP and plain TCP. It is a built-in module that does not need
# to be loaded.
# TODO rsyslog issue in bridge mode container, don't update to remote server for now

View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
if [ "${RUNTIME_OWNER}" == "" ]; then
RUNTIME_OWNER="kube"
fi
# This script is to basicly check for if this starting-container can be allowed
# to run based on current state, and owner & version of this starting container.
# If allowed, update feature info in state_db and then processes in supervisord.conf
# after this process can start up.
CTR_SCRIPT="/usr/share/sonic/scripts/container_startup.py"
if test -f ${CTR_SCRIPT}
then
${CTR_SCRIPT} -f dhcp_server -o ${RUNTIME_OWNER} -v ${IMAGE_VERSION}
fi
TZ=$(cat /etc/timezone)
rm -rf /etc/localtime
ln -sf /usr/share/zoneinfo/$TZ /etc/localtime

View File

@ -13,7 +13,7 @@ events=PROCESS_STATE
buffer_size=1024
[eventlistener:supervisor-proc-exit-listener]
command=/usr/bin/supervisor-proc-exit-listener --container-name dhcp_server
command=/usr/bin/supervisor-proc-exit-listener --container-name dhcp_server --use-unix-socket-path
events=PROCESS_STATE_EXITED,PROCESS_STATE_RUNNING
autostart=true
autorestart=unexpected
@ -38,3 +38,26 @@ stdout_logfile=syslog
stderr_logfile=syslog
dependent_startup=true
dependent_startup_wait_for=rsyslogd:running
[group:dhcp-server-ipv4]
programs=dhcpservd,kea-dhcp4
[program:dhcpservd]
command=/usr/local/bin/dhcpservd
priority=3
autostart=false
autorestart=false
stdout_logfile=syslog
stderr_logfile=syslog
dependent_startup=true
dependent_startup_wait_for=start:exited
[program:kea-dhcp4]
command=/usr/local/sbin/kea-dhcp4 -c /etc/kea/kea-dhcp4.conf
priority=3
autostart=false
autorestart=false
stdout_logfile=syslog
stderr_logfile=syslog
dependent_startup=true
dependent_startup_wait_for=start:exited

View File

@ -13,8 +13,8 @@ $(DOCKER_DHCP_SERVER)_DBG_IMAGE_PACKAGES = $($(DOCKER_CONFIG_ENGINE_BULLSEYE)_DB
$(DOCKER_DHCP_SERVER)_LOAD_DOCKERS = $(DOCKER_CONFIG_ENGINE_BULLSEYE)
$(DOCKER_DHCP_SERVER)_INSTALL_PYTHON_WHEELS = $(SONIC_UTILITIES_PY3)
$(DOCKER_DHCP_SERVER)_INSTALL_DEBS = $(PYTHON3_SWSSCOMMON)
$(DOCKER_DHCP_SERVER)_PYTHON_WHEELS += $(SONIC_DHCP_SERVER_PY3)
SONIC_DOCKER_IMAGES += $(DOCKER_DHCP_SERVER)
SONIC_DOCKER_DBG_IMAGES += $(DOCKER_DHCP_SERVER_DBG)
@ -40,7 +40,7 @@ $(DOCKER_DHCP_SERVER)_PACKAGE_NAME = dhcp-server
$(DOCKER_MACSEC)_SERVICE_REQUIRES = updategraph
$(DOCKER_MACSEC)_SERVICE_AFTER = swss syncd
$(DOCKER_DHCP_SERVER)_CONTAINER_PRIVILEGED = true
$(DOCKER_DHCP_SERVER)_CONTAINER_PRIVILEGED = false
$(DOCKER_DHCP_SERVER)_CONTAINER_VOLUMES += /etc/sonic:/etc/sonic:ro
$(DOCKER_DHCP_SERVER)_CONTAINER_TMPFS += /tmp/
$(DOCKER_DHCP_SERVER)_CONTAINER_TMPFS += /var/tmp/
@ -48,5 +48,6 @@ $(DOCKER_DHCP_SERVER)_CONTAINER_TMPFS += /var/tmp/
$(DOCKER_DHCP_SERVER)_CLI_CONFIG_PLUGIN = /cli/config/plugins/dhcp_server.py
$(DOCKER_DHCP_SERVER)_CLI_SHOW_PLUGIN = /cli/show/plugins/show_dhcp_server.py
$(DOCKER_DHCP_SERVER)_CLI_CLEAR_PLUGIN = /cli/clear/plugins/clear_dhcp_server.py
$(DOCKER_DHCP_SERVER)_SUPPORT_RATE_LIMIT = false
$(DOCKER_DHCP_SERVER)_FILES += $(SUPERVISOR_PROC_EXIT_LISTENER_SCRIPT)

View File

@ -0,0 +1,10 @@
SPATH := $($(SONIC_DHCPSERVD)_SRC_PATH)
DEP_FILES := $(SONIC_COMMON_FILES_LIST) rules/sonic-dhcp-server.mk rules/sonic-dhcp-server.dep
DEP_FILES += $(SONIC_COMMON_BASE_FILES_LIST)
SMDEP_FILES := $(addprefix $(SPATH)/,$(shell cd $(SPATH) && git ls-files))
$(SONIC_DHCPSERVD)_CACHE_MODE := GIT_CONTENT_SHA
$(SONIC_DHCPSERVD)_DEP_FLAGS := $(SONIC_COMMON_FLAGS_LIST)
$(SONIC_DHCPSERVD)_DEP_FILES := $(DEP_FILES)
$(SONIC_DHCPSERVD)_SMDEP_FILES := $(SMDEP_FILES)
$(SONIC_DHCPSERVD)_SMDEP_PATHS := $(SPATH)

View File

@ -0,0 +1,10 @@
# sonic-dhcp-server package
SONIC_DHCP_SERVER_PY3 = sonic_dhcp_server-1.0-py3-none-any.whl
$(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

4
src/sonic-dhcp-server/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.eggs/
build/
dist/
sonic_dhcpservd.egg-info/

View File

@ -0,0 +1,330 @@
#!/usr/bin/env python
import ipaddress
import json
import os
import syslog
from jinja2 import Environment, FileSystemLoader
from .dhcp_server_utils import merge_intervals
PORT_MAP_PATH = "/tmp/port-name-alias-map.txt"
UNICODE_TYPE = str
DHCP_SERVER_IPV4 = "DHCP_SERVER_IPV4"
DHCP_SERVER_IPV4_CUSTOMIZED_OPTIONS = "DHCP_SERVER_IPV4_CUSTOMIZED_OPTIONS"
DHCP_SERVER_IPV4_RANGE = "DHCP_SERVER_IPV4_RANGE"
DHCP_SERVER_IPV4_PORT = "DHCP_SERVER_IPV4_PORT"
DHCP_SERVER_IPV4_LEASE = "DHCP_SERVER_IPV4_LEASE"
LEASE_UPDATE_SCRIPT_PATH = "/etc/kea/lease_update.sh"
DEFAULT_LEASE_PATH = "/tmp/kea-lease.csv"
KEA_DHCP4_CONF_TEMPLATE_PATH = "/usr/share/sonic/templates/kea-dhcp4.conf.j2"
# Default lease time of DHCP
DEFAULT_LEASE_TIME = 900
class DhcpServCfgGenerator(object):
port_alias_map = {}
lease_update_script_path = ""
lease_path = ""
def __init__(self, dhcp_db_connector, lease_path=DEFAULT_LEASE_PATH, port_map_path=PORT_MAP_PATH,
lease_update_script_path=LEASE_UPDATE_SCRIPT_PATH,
kea_conf_template_path=KEA_DHCP4_CONF_TEMPLATE_PATH):
self.db_connector = dhcp_db_connector
self.lease_path = lease_path
self.lease_update_script_path = lease_update_script_path
# Read port alias map file, this file is render after container start, so it would not change any more
self._parse_port_map_alias(port_map_path)
# Get kea config template
self._get_render_template(kea_conf_template_path)
def generate(self):
"""
Generate dhcp server config
Returns:
config dict
"""
# Generate from running config_db
# Get host name
device_metadata = self.db_connector.get_config_db_table("DEVICE_METADATA")
hostname = self._parse_hostname(device_metadata)
# Get ip information of vlan
vlan_interface = self.db_connector.get_config_db_table("VLAN_INTERFACE")
vlan_member_table = self.db_connector.get_config_db_table("VLAN_MEMBER")
vlan_interfaces, vlan_members = self._parse_vlan(vlan_interface, vlan_member_table)
dhcp_server_ipv4, customized_options_ipv4, range_ipv4, port_ipv4 = self._get_dhcp_ipv4_tables_from_db()
# Parse range table
ranges = self._parse_range(range_ipv4)
# TODO Add support for customizing options
# Parse port table
port_ips = self._parse_port(port_ipv4, vlan_interfaces, vlan_members, ranges)
render_obj = self._construct_obj_for_template(dhcp_server_ipv4, port_ips, hostname)
return self._render_config(render_obj)
def _render_config(self, render_obj):
output = self.kea_template.render(render_obj)
return output
def _parse_vlan(self, vlan_interface, vlan_member):
vlan_interfaces = self._get_vlan_ipv4_interface(vlan_interface.keys())
vlan_members = vlan_member.keys()
return vlan_interfaces, vlan_members
def _parse_hostname(self, device_metadata):
localhost_entry = device_metadata.get("localhost", {})
if localhost_entry is None or "hostname" not in localhost_entry:
syslog.syslog(syslog.LOG_ERR, "Cannot get hostname")
raise Exception("Cannot get hostname")
return localhost_entry["hostname"]
def _get_render_template(self, kea_conf_template_path):
# Semgrep does not allow to use jinja2 directly, but we do need jinja2 for SONiC
env = Environment(loader=FileSystemLoader(os.path.dirname(kea_conf_template_path))) # nosemgrep
self.kea_template = env.get_template(os.path.basename(kea_conf_template_path))
def _parse_port_map_alias(self, port_map_path):
with open(port_map_path, "r") as file:
lines = file.readlines()
for line in lines:
splits = line.strip().split(" ")
if len(splits) != 2:
continue
self.port_alias_map[splits[0]] = splits[1]
def _construct_obj_for_template(self, dhcp_server_ipv4, port_ips, hostname):
subnets = []
client_classes = []
for dhcp_interface_name, dhcp_config in dhcp_server_ipv4.items():
if "state" not in dhcp_config or dhcp_config["state"] != "enabled":
continue
if dhcp_config["mode"] == "PORT":
if dhcp_interface_name not in port_ips:
syslog.syslog(syslog.LOG_WARNING, "Cannot get DHCP port config for {}"
.format(dhcp_interface_name))
continue
for dhcp_interface_ip, port_config in port_ips[dhcp_interface_name].items():
pools = []
for port_name, ip_ranges in port_config.items():
ip_range = None
for ip_range in ip_ranges:
client_class = "{}:{}".format(hostname, port_name)
ip_range = {
"range": "{} - {}".format(ip_range[0], ip_range[1]),
"client_class": client_class
}
pools.append(ip_range)
if ip_range is not None:
class_len = len(client_class)
client_classes.append({
"name": client_class,
"condition": "substring(relay4[1].hex, -{}, {}) == '{}'".format(class_len, class_len,
client_class)
})
subnet_obj = {
"subnet": str(ipaddress.ip_network(dhcp_interface_ip, strict=False)),
"pools": pools,
"gateway": dhcp_config["gateway"],
"server_id": dhcp_interface_ip.split("/")[0],
"lease_time": dhcp_config["lease_time"]
}
subnets.append(subnet_obj)
render_obj = {
"subnets": subnets,
"client_classes": client_classes,
"lease_update_script_path": self.lease_update_script_path,
"lease_path": self.lease_path
}
return render_obj
def _get_dhcp_ipv4_tables_from_db(self):
"""
Get DHCP Server IPv4 related table from config_db.
Returns:
Four table objects.
"""
dhcp_server_ipv4 = self.db_connector.get_config_db_table(DHCP_SERVER_IPV4)
customized_options_ipv4 = self.db_connector.get_config_db_table(DHCP_SERVER_IPV4_CUSTOMIZED_OPTIONS)
range_ipv4 = self.db_connector.get_config_db_table(DHCP_SERVER_IPV4_RANGE)
port_ipv4 = self.db_connector.get_config_db_table(DHCP_SERVER_IPV4_PORT)
return dhcp_server_ipv4, customized_options_ipv4, range_ipv4, port_ipv4
def _get_vlan_ipv4_interface(self, vlan_interface_keys):
"""
Get ipv4 info of vlans
Args:
vlan_interface_keys: Keys of vlan_interfaces, sample:
[
"Vlan1000|192.168.0.1/21",
"Vlan1000|fc02:1000::1/64"
]
Returns:
Vlans infomation, sample:
{
'Vlan1000': [{
'network': IPv4Network('192.168.0.0/24'),
'ip': '192.168.0.1/24'
}]
}
"""
ret = {}
for key in vlan_interface_keys:
splits = key.split("|")
# Skip with no ip address
if len(splits) != 2:
continue
network = ipaddress.ip_network(UNICODE_TYPE(splits[1]), False)
# Skip ipv6
if network.version != 4:
continue
if splits[0] not in ret:
ret[splits[0]] = []
ret[splits[0]].append({"network": network, "ip": splits[1]})
return ret
def _parse_range(self, range_ipv4):
"""
Parse content in DHCP_SERVER_IPV4_RANGE table to below format:
{
'range2': [IPv4Address('192.168.0.3'), IPv4Address('192.168.0.6')],
'range1': [IPv4Address('192.168.0.2'), IPv4Address('192.168.0.5')],
'range3': [IPv4Address('192.168.0.10'), IPv4Address('192.168.0.10')]
}
Args:
range_ipv4: Table object or dict of range.
"""
ranges = {}
for range in list(range_ipv4.keys()):
curr_range = range_ipv4.get(range, {}).get("range", {})
if len(curr_range) != 2:
syslog.syslog(syslog.LOG_WARNING, f"Length of {curr_range} != 2")
continue
address_1 = ipaddress.ip_address(curr_range[0])
address_2 = ipaddress.ip_address(curr_range[1])
# To make sure order of range is correct
range_start = address_1 if address_1 < address_2 else address_2
range_end = address_2 if address_1 < address_2 else address_1
ranges[range] = [range_start, range_end]
return ranges
def _match_range_network(self, dhcp_interface, dhcp_interface_name, port, range, port_ips):
"""
Loop the IP of the dhcp interface and find the network that target range is in this network. And to construct
below data to record range - port map
{
'Vlan1000': {
'192.168.0.1/24': {
'etp2': [
[IPv4Address('192.168.0.7'), IPv4Address('192.168.0.7')]
]
}
}
}
Args:
dhcp_interface: Ip and network information of current DHCP interface, sample:
[{
'network': IPv4Network('192.168.0.0/24'),
'ip': '192.168.0.1/24'
}]
dhcp_interface_name: Name of DHCP interface.
port: Name of DHCP member port.
range: Ip Range, sample:
[IPv4Address('192.168.0.2'), IPv4Address('192.168.0.5')]
"""
for dhcp_interface_ip in dhcp_interface:
if not range[0] in dhcp_interface_ip["network"] or \
not range[1] in dhcp_interface_ip["network"]:
continue
dhcp_interface_ip_str = dhcp_interface_ip["ip"]
if dhcp_interface_ip_str not in port_ips[dhcp_interface_name]:
port_ips[dhcp_interface_name][dhcp_interface_ip_str] = {}
if port not in port_ips[dhcp_interface_name][dhcp_interface_ip_str]:
port_ips[dhcp_interface_name][dhcp_interface_ip_str][port] = []
port_ips[dhcp_interface_name][dhcp_interface_ip_str][port].append([range[0], range[1]])
break
def _parse_port(self, port_ipv4, vlan_interfaces, vlan_members, ranges):
"""
Parse content in DHCP_SERVER_IPV4_PORT table to below format, which indicate ip ranges assign to interface.
Args:
port_ipv4: Table object.
vlan_interfaces: Vlan information, sample:
{
'Vlan1000': [{
'network': IPv4Network('192.168.0.0/24'),
'ip': '192.168.0.1/24'
}]
}
vlan_members: List of vlan members
ranges: Dict of ranges
Returns:
Dict of dhcp conf, sample:
{
'Vlan1000': {
'192.168.0.1/24': {
'etp2': [
['192.168.0.7', '192.168.0.7']
],
'etp3': [
['192.168.0.2', '192.168.0.6'],
['192.168.0.10', '192.168.0.10']
]
}
}
}
"""
port_ips = {}
ip_ports = {}
for port_key in list(port_ipv4.keys()):
port_config = port_ipv4.get(port_key, {})
# Cannot specify both 'ips' and 'ranges'
if "ips" in port_config and len(port_config["ips"]) != 0 and "ranges" in port_config \
and len(port_config["ranges"]) != 0:
syslog.syslog(syslog.LOG_WARNING, f"Port config for {port_key} contains both ips and ranges, skip")
continue
splits = port_key.split("|")
# Skip port not in correct vlan
if port_key not in vlan_members:
syslog.syslog(syslog.LOG_WARNING, f"Port {splits[1]} is not in {splits[0]}")
continue
# Get dhcp interface name like Vlan1000
dhcp_interface_name = splits[0]
# Get dhcp member interface name like etp1
if splits[1] not in self.port_alias_map:
syslog.syslog(syslog.LOG_WARNING, f"Cannot find {splits[1]} in port_alias_map")
continue
port = self.port_alias_map[splits[1]]
if dhcp_interface_name not in port_ips:
port_ips[dhcp_interface_name] = {}
# Get ip information of Vlan
dhcp_interface = vlan_interfaces[dhcp_interface_name]
for dhcp_interface_ip in dhcp_interface:
ip_ports[str(dhcp_interface_ip["network"])] = dhcp_interface_name
if "ips" in port_config and len(port_config["ips"]) != 0:
for ip in set(port_config["ips"]):
ip_address = ipaddress.ip_address(ip)
# Loop the IP of the dhcp interface and find the network that target ip is in this network.
self._match_range_network(dhcp_interface, dhcp_interface_name, port, [ip_address, ip_address],
port_ips)
if "ranges" in port_config and len(port_config["ranges"]) != 0:
for range_name in list(port_config["ranges"]):
if range_name not in ranges:
syslog.syslog(syslog.LOG_WARNING, f"Range {range_name} is not in range table, skip")
continue
range = ranges[range_name]
# Loop the IP of the dhcp interface and find the network that target range is in this network.
self._match_range_network(dhcp_interface, dhcp_interface_name, port, range, port_ips)
# Merge ranges to avoid overlap
for dhcp_interface_name, value in port_ips.items():
for dhcp_interface_ip, port_range in value.items():
for port_name, ip_range in port_range.items():
ranges = merge_intervals(ip_range)
ranges = [[str(range[0]), str(range[1])] for range in ranges]
port_ips[dhcp_interface_name][dhcp_interface_ip][port_name] = ranges
return port_ips

View File

@ -0,0 +1,154 @@
import ipaddress
import json
import signal
import syslog
import threading
import time
from abc import abstractmethod
from collections import deque
from datetime import datetime
DHCP_SERVER_IPV4_LEASE = "DHCP_SERVER_IPV4_LEASE"
KEA_LEASE_FILE_PATH = "/tmp/kea-lease.csv"
DEFAULE_LEASE_UPDATE_INTERVAL = 2 # unit: sec
class LeaseManager(object):
def __init__(self, db_connector, kea_lease_file=KEA_LEASE_FILE_PATH):
self.lease_handlers = [KeaDhcp4LeaseHandler(db_connector, kea_lease_file)]
def start(self):
"""
Register lease hanlder
"""
for handler in self.lease_handlers:
handler.register()
class LeaseHanlder(object):
def __init__(self, db_connector, lease_update_interval=DEFAULE_LEASE_UPDATE_INTERVAL):
self.db_connector = db_connector
self.lease_update_interval = lease_update_interval
self.last_update_time = None
self.lock = threading.Lock()
@abstractmethod
def _read(self):
"""
Read lease file to get newest lease information
"""
raise NotImplementedError
@abstractmethod
def register(self):
"""
Register callback function
"""
raise NotImplementedError
def update_lease(self):
"""
Update lease table in STATE_DB
"""
last_update_time = self.last_update_time
curr_time = datetime.now()
# If the time since the last update is less than self.lease_update_interval, then wait for a
# self.lease_update_interval
if last_update_time is not None and (curr_time - last_update_time).seconds < self.lease_update_interval:
time.sleep(self.lease_update_interval)
if self.last_update_time != last_update_time:
# Means lease has been updated during sleep, no need to update this lease
return
if not self.lock.acquire(False):
return
new_lease = self._read()
# Store old lease key
old_lease_table = self.db_connector.get_state_db_table(DHCP_SERVER_IPV4_LEASE)
old_lease_key = set(old_lease_table.keys())
# 1.1 If start time equal to end time, means lease has been released
# 1.1.1 If current lease table has this old lease, delete it
# 1.1.2 Else skip
# 1.2 Else, means lease valid, save it.
for key, value in new_lease.items():
if value["lease_start"] == value["lease_end"]:
if key in old_lease_key:
self.db_connector.state_db.delete("{}|{}".format(DHCP_SERVER_IPV4_LEASE, key))
continue
new_key = "{}|{}".format(DHCP_SERVER_IPV4_LEASE, key)
for k, v in new_lease[key].items():
self.db_connector.state_db.hset(new_key, k, v)
# Delete old lease not in new lease set
for key in old_lease_key:
if key not in new_lease.keys():
# Delete entry
self.db_connector.state_db.delete("{}|{}".format(DHCP_SERVER_IPV4_LEASE, key))
self.last_update_time = datetime.now()
self.lock.release()
class KeaDhcp4LeaseHandler(LeaseHanlder):
def __init__(self, db_connector, lease_file=KEA_LEASE_FILE_PATH):
LeaseHanlder.__init__(self, db_connector)
self.lease_file = lease_file
def register(self):
"""
Register callback function of signal
"""
signal.signal(signal.SIGUSR1, self._update_lease)
def _read(self):
# Read lease file generated by kea-dhcp4
try:
with open(self.lease_file, "r", encoding="utf-8") as fb:
dq = deque(fb)
except FileNotFoundError as err:
syslog.syslog(syslog.LOG_ERR, "Cannot find lease file: {}".format(self.lease_file))
raise err
fdb_info = self._get_fdb_info()
new_lease = {}
# Get newest lease information of each client
while dq:
last_row = dq.pop()
splits = last_row.split(",")
# Skip header
if splits[0] == "address":
break
ip_str = splits[0]
mac_address = splits[1]
valid_lifetime = splits[3]
lease_end = splits[4]
if mac_address not in fdb_info:
syslog.syslog(syslog.LOG_WARNING, "Cannot not find {} in fdb table".format(mac_address))
continue
new_key = "{}|{}".format(fdb_info[mac_address], mac_address)
if new_key in new_lease:
continue
new_lease[new_key] = {
"lease_start": str(int(lease_end) - int(valid_lifetime)),
"lease_end": lease_end,
"ip": ip_str
}
return new_lease
def _get_fdb_info(self):
"""
Get fdb information, indicate that mac address comes from which dhcp interface.
Returns:
Dict of fdb information, sample:
{
"aa:bb:cc:dd:ee:ff": "Vlan1000"
}
"""
fdb_table = self.db_connector.get_state_db_table("FDB_TABLE")
ret = {}
for key in fdb_table.keys():
splits = key.split(":", 1)
ret[splits[1]] = splits[0]
return ret
def _update_lease(self, signum, frame):
self.update_lease()

View File

@ -0,0 +1,99 @@
from swsscommon import swsscommon
DEFAULT_REDIS_HOST = "127.0.0.1"
DEFAULT_REDIS_PORT = 6379
class DhcpDbConnector(object):
def __init__(self, redis_host=DEFAULT_REDIS_HOST, redis_port=DEFAULT_REDIS_PORT, redis_sock=None):
if redis_sock is not None:
self.redis_sock = redis_sock
self.config_db = swsscommon.DBConnector(swsscommon.CONFIG_DB, redis_sock, 0)
self.state_db = swsscommon.DBConnector(swsscommon.STATE_DB, redis_sock, 0)
else:
self.config_db = swsscommon.DBConnector(swsscommon.CONFIG_DB, redis_host, redis_port, 0)
self.state_db = swsscommon.DBConnector(swsscommon.STATE_DB, redis_host, redis_port, 0)
def get_config_db_table(self, table_name):
"""
Get table from config_db.
Args:
table_name: Name of table want to get.
Return:
Table objects.
"""
return _parse_table_to_dict(swsscommon.Table(self.config_db, table_name))
def get_state_db_table(self, table_name):
"""
Get table from state_db.
Args:
table_name: Name of table want to get.
Return:
Table objects.
"""
return _parse_table_to_dict(swsscommon.Table(self.state_db, table_name))
def get_entry(table, entry_name):
"""
Get dict entry from Table object.
Args:
table: Table object.
entry_name: Name of entry.
Returns:
Dict of entry, sample:
{
"customized_options": "option60,option223",
"gateway": "192.168.0.1",
"lease_time": "900",
"mode": "PORT",
"netmask": "255.255.255.0",
"state": "enabled"
}
"""
(_, entry) = table.get(entry_name)
return dict(entry)
def merge_intervals(intervals):
"""
Merge ip range intervals.
Args:
intervals: Ip ranges, may have overlaps, sample:
[
[IPv4Address('192.168.0.2'), IPv4Address('192.168.0.5')],
[IPv4Address('192.168.0.3'), IPv4Address('192.168.0.6')],
[IPv4Address('192.168.0.10'), IPv4Address('192.168.0.10')]
]
Returns:
Merged ip ranges, sample:
[
[IPv4Address('192.168.0.2'), IPv4Address('192.168.0.6')],
[IPv4Address('192.168.0.10'), IPv4Address('192.168.0.10')]
]
"""
intervals.sort(key=lambda x: x[0])
ret = []
for interval in intervals:
if len(ret) == 0 or interval[0] > ret[-1][-1]:
ret.append(interval)
else:
ret[-1][-1] = max(ret[-1][-1], interval[-1])
return ret
def _parse_table_to_dict(table):
ret = {}
for key in table.getKeys():
entry = get_entry(table, key)
new_entry = {}
for field, value in entry.items():
# if value of this field is list, field end with @, so cannot found by hget
if table.hget(key, field)[0]:
new_entry[field] = value
else:
new_entry[field] = value.split(",")
ret[key] = new_entry
return ret

View File

@ -0,0 +1,61 @@
#!/usr/bin/env python
import psutil
import signal
import time
from .dhcp_cfggen import DhcpServCfgGenerator
from .dhcp_lease import LeaseManager
from .dhcp_server_utils import DhcpDbConnector
KEA_DHCP4_CONFIG = "/etc/kea/kea-dhcp4.conf"
KEA_DHCP4_PROC_NAME = "kea-dhcp4"
KEA_LEASE_FILE_PATH = "/tmp/kea-lease.csv"
REDIS_SOCK_PATH = "/var/run/redis/redis.sock"
class DhcpServd(object):
def __init__(self, dhcp_cfg_generator, db_connector, kea_dhcp4_config_path=KEA_DHCP4_CONFIG):
self.dhcp_cfg_generator = dhcp_cfg_generator
self.db_connector = db_connector
self.kea_dhcp4_config_path = kea_dhcp4_config_path
def _notify_kea_dhcp4_proc(self):
"""
Send SIGHUP signal to kea-dhcp4 process
"""
for proc in psutil.process_iter():
if KEA_DHCP4_PROC_NAME in proc.name():
proc.send_signal(signal.SIGHUP)
break
def dump_dhcp4_config(self):
"""
Generate kea-dhcp4 config file and dump it to config folder
"""
kea_dhcp4_config = self.dhcp_cfg_generator.generate()
with open(self.kea_dhcp4_config_path, "w") as write_file:
write_file.write(kea_dhcp4_config)
# After refresh kea-config, we need to SIGHUP kea-dhcp4 process to read new config
self._notify_kea_dhcp4_proc()
def start(self):
self.dump_dhcp4_config()
lease_manager = LeaseManager(self.db_connector, KEA_LEASE_FILE_PATH)
lease_manager.start()
# TODO Add config db subcribe to re-generate kea-dhcp4 config after config_db change.
def wait(self):
while True:
time.sleep(5)
def main():
dhcp_db_connector = DhcpDbConnector(redis_sock=REDIS_SOCK_PATH)
dhcp_cfg_generator = DhcpServCfgGenerator(dhcp_db_connector)
dhcpservd = DhcpServd(dhcp_cfg_generator, dhcp_db_connector)
dhcpservd.start()
dhcpservd.wait()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,20 @@
[aliases]
test=pytest
[tool:pytest]
addopts = --durations=5 --cov
testpaths = tests
[coverage:run]
branch = True
source = dhcp_server
[coverage:report]
exclude_lines =
main()
if __name__ == .__main__.:
precision = 2
show_missing = True
skip_covered = True
sort = Cover
fail_under = 80

View File

@ -0,0 +1,49 @@
from setuptools import setup
dependencies = [
"psutil",
"coverage"
]
test_deps = [
"pytest"
]
py_modules = [
"dhcp_server_utils",
"dhcp_cfggen",
"dhcp_lease"
]
setup(
name="sonic-dhcp-server",
install_requires=dependencies,
description="Module of SONiC built-in dhcp_server",
version="1.0",
url="https://github.com/Azure/sonic-buildimage",
tests_require=test_deps,
author="SONiC Team",
author_email="yaqiangzhu@microsoft.com",
setup_requires=[
"pytest-runner",
"wheel",
],
packages=[
"dhcp_server"
],
entry_points={
"console_scripts": [
"dhcpservd = dhcp_server.dhcpservd:main"
]
},
py_modules=py_modules,
classifiers=[
"Intended Audience :: Developers",
"Operating System :: Linux",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8"
]
)

View File

@ -0,0 +1,12 @@
import json
MOCK_CONFIG_DB_PATH = "tests/test_data/mock_config_db.json"
class MockConfigDb(object):
def __init__(self, config_db_path=MOCK_CONFIG_DB_PATH):
with open(config_db_path, "r", encoding="utf8") as file:
self.config_db = json.load(file)
def get_config_db_table(self, table_name):
return self.config_db.get(table_name, {})

View File

@ -0,0 +1,35 @@
import pytest
import dhcp_server.dhcp_server_utils as dhcp_server_utils
from unittest.mock import patch, PropertyMock
from dhcp_server.dhcp_cfggen import DhcpServCfgGenerator
@pytest.fixture(scope="function")
def mock_swsscommon_dbconnector_init():
with patch.object(dhcp_server_utils.swsscommon.DBConnector, "__init__", return_value=None) as mock_dbconnector_init:
yield mock_dbconnector_init
@pytest.fixture(scope="function")
def mock_swsscommon_table_init():
with patch.object(dhcp_server_utils.swsscommon.Table, "__init__", return_value=None) as mock_table_init:
yield mock_table_init
@pytest.fixture(scope="function")
def mock_get_render_template():
with patch("dhcp_server.dhcp_cfggen.DhcpServCfgGenerator._get_render_template", return_value=None) as mock_template:
yield mock_template
@pytest.fixture
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, \
patch.object(DhcpServCfgGenerator, "port_alias_map", return_value={"Ethernet24": "etp7", "Ethernet28": "etp8"},
new_callable=PropertyMock), \
patch.object(DhcpServCfgGenerator, "lease_update_script_path", return_value="/etc/kea/lease_update.sh",
new_callable=PropertyMock), \
patch.object(DhcpServCfgGenerator, "lease_path", return_value="/tmp/kea-lease.csv", new_callable=PropertyMock):
yield mock_map

View File

@ -0,0 +1,87 @@
{%- set default_lease_time = 900 -%}
{
"Dhcp4": {
"hooks-libraries": [
{
"library": "/usr/local/lib/kea/hooks/libdhcp_run_script.so",
"parameters": {
"name": "{{ lease_update_script_path }}",
"sync": false
}
}
],
"interfaces-config": {
"interfaces": [
"eth0"
]
},
"control-socket": {
"socket-type": "unix",
"socket-name": "/run/kea/kea4-ctrl-socket"
},
"lease-database": {
"type": "memfile",
"persist": true,
"name": "{{ lease_path }}",
"lfc-interval": 3600
},
"subnet4": [
{%- set add_subnet_preceding_comma = { 'flag': False } %}
{%- for subnet_info in subnets %}
{%- if add_subnet_preceding_comma.flag -%},{%- endif -%}
{%- set _dummy = add_subnet_preceding_comma.update({'flag': True}) %}
{
"subnet": "{{ subnet_info["subnet"] }}",
"pools": [
{%- set add_pool_preceding_comma = { 'flag': False } %}
{%- for pool in subnet_info["pools"] %}
{%- if add_pool_preceding_comma.flag -%},{%- endif -%}
{%- set _dummy = add_pool_preceding_comma.update({'flag': True}) %}
{
"pool": "{{ pool["range"] }}",
"client-class": "{{ pool["client_class"] }}"
}
{%- endfor%}
],
"option-data": [
{
"name": "routers",
"data": "{{ subnet_info["gateway"] if "gateway" in subnet_info else subnet_info["server_id"] }}"
},
{
"name": "dhcp-server-identifier",
"data": "{{ subnet_info["server_id"] }}"
}
],
"valid-lifetime": {{ subnet_info["lease_time"] if "lease_time" in subnet_info else default_lease_time }},
"reservations": []
}
{%- endfor %}
],
"loggers": [
{
"name": "kea-dhcp4",
"output_options": [
{
"output": "/var/log/kea-dhcp.log",
"pattern": "%-5p %m\n"
}
],
"severity": "INFO",
"debuglevel": 0
}
]{%- if client_classes -%},
"client-classes": [
{%- set add_preceding_comma = { 'flag': False } %}
{%- for class in client_classes %}
{%- if add_preceding_comma.flag -%},{%- endif -%}
{%- set _dummy = add_preceding_comma.update({'flag': True}) %}
{
"name": "{{ class["name"] }}",
"test": "{{ class["condition"] }}"
}
{%- endfor %}
]
{%- endif %}
}
}

View File

@ -0,0 +1,87 @@
{%- set default_lease_time = 900 -%}
{
"Dhcp4": {
"hooks-libraries": [
{
"library": "/usr/local/lib/kea/hooks/libdhcp_run_script.so",
"parameters": {
"name": "{{ lease_update_script_path }}",
"sync": false
}
}
],
"interfaces-config": {
"interfaces": [
"eth0"
]
},
"control-socket": {
"socket-type": "unix",
"socket-name": "/run/kea/kea4-ctrl-socket"
},
"lease-database": {
"type": "memfile",
"persist": true,
"name": "{{ lease_path }}",
"lfc-interval": 3600
},
"subnet4": [
{%- set add_subnet_preceding_comma = { 'flag': False } %}
{%- for subnet_info in subnets %}
{%- if add_subnet_preceding_comma.flag -%},{%- endif -%}
{%- set _dummy = add_subnet_preceding_comma.update({'flag': True}) %}
{
"subnet": "{{ subnet_info["subnet"] }}",
"pools": [
{%- set add_pool_preceding_comma = { 'flag': False } %}
{%- for pool in subnet_info["pools"] %}
{%- if add_pool_preceding_comma.flag -%},{%- endif -%}
{%- set _dummy = add_pool_preceding_comma.update({'flag': True}) %}
{
"pool": "{{ pool["range"] }}",
"client-class": "{{ pool["client_class"] }}"
}
{%- endfor%}
],
"option-data": [
{
"name": "routers",
"data": "{{ subnet_info["gateway"] if "gateway" in subnet_info else subnet_info["server_id"] }}"
},
{
"name": "dhcp-server-identifier",
"data": "{{ subnet_info["server_id"] }}"
}
],
"valid-lifetime": {{ subnet_info["lease_time"] if "lease_time" in subnet_info else default_lease_time }},
"reservations": []
}
{%- endfor %}
],
"loggers": [
{
"name": "kea-dhcp4",
"output_options": [
{
"output": "/var/log/kea-dhcp.log",
"pattern": "%-5p %m\n"
}
],
"severity": "INFO",
"debuglevel": 0
}
]{%- if client_classes -%},
"client-classes": [
{%- set add_preceding_comma = { 'flag': False } %}
{%- for class in client_classes %}
{%- if add_preceding_comma.flag -%},{%- endif -%}
{%- set _dummy = add_preceding_comma.update({'flag': True}) %}
{
"name": "{{ class["name"] }}",
"test": "{{ class["condition"] }}"
}
{%- endfor %}
]
{%- endif %}
}
}

View File

@ -0,0 +1,9 @@
address,hwaddr,client_id,valid_lifetime,expire,subnet_id,fqdn_fwd,fqdn_rev,hostname,state,user_context,pool_id
192.168.0.2,10:70:fd:b6:13:00,,3600,1694000905,1,0,0,7626dced293e,0,,0
192.168.0.131,10:70:fd:b6:13:17,,3600,1694000909,1,0,0,7626dced293e,0,,1
192.168.0.131,10:70:fd:b6:13:17,,0,1693997309,1,0,0,7626dced293e,0,,1
192.168.0.131,10:70:fd:b6:13:17,,0,1693997309,1,0,0,,2,,1
192.168.0.131,10:70:fd:b6:13:17,,3600,1694000915,1,0,0,7626dced293e,0,,1
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.3,10:70:fd:b6:13:20,,3600,1693999305,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
2 192.168.0.2 10:70:fd:b6:13:00 3600 1694000905 1 0 0 7626dced293e 0 0
3 192.168.0.131 10:70:fd:b6:13:17 3600 1694000909 1 0 0 7626dced293e 0 1
4 192.168.0.131 10:70:fd:b6:13:17 0 1693997309 1 0 0 7626dced293e 0 1
5 192.168.0.131 10:70:fd:b6:13:17 0 1693997309 1 0 0 2 1
6 192.168.0.131 10:70:fd:b6:13:17 3600 1694000915 1 0 0 7626dced293e 0 1
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

View File

@ -0,0 +1,159 @@
{
"DEVICE_METADATA": {
"localhost": {
"hostname": "sonic-host"
}
},
"VLAN_INTERFACE": {
"Vlan1000|192.168.0.1/21": {
"NULL": "NULL"
},
"Vlan1000": {
"NULL": "NULL"
},
"Vlan1000|fc02:1000::1/64": {
"NULL": "NULL"
},
"Vlan2000|192.168.1.1/21": {
"NULL": "NULL"
},
"Vlan2000|192.168.2.1/21": {
"NULL": "NULL"
},
"Vlan2000": {
"NULL": "NULL"
}
},
"VLAN_MEMBER": {
"Vlan1000|Ethernet24": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet28": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet40": {
"tagging_mode": "untagged"
}
},
"DHCP_SERVER_IPV4": {
"Vlan1000": {
"customized_options": [
"option60",
"option223"
],
"gateway": "192.168.0.1",
"lease_time": "900",
"mode": "PORT",
"netmask": "255.255.255.0",
"state": "enabled"
},
"Vlan2000": {
"customized_options": [
"option60"
],
"gateway": "192.168.1.1",
"lease_time": "900",
"mode": "PORT",
"netmask": "255.255.255.0",
"state": "disabled"
},
"Vlan3000": {
"customized_options": [
"option60"
],
"gateway": "192.168.2.1",
"lease_time": "900",
"mode": "STATIC",
"netmask": "255.255.255.0",
"state": "enabled"
},
"Vlan4000": {
"customized_options": [
"option60",
"option223"
],
"gateway": "192.168.3.1",
"lease_time": "900",
"mode": "PORT",
"netmask": "255.255.255.0",
"state": "enabled"
}
},
"DHCP_SERVER_IPV4_CUSTOMIZED_OPTIONS": {
"option223": {
"id": "223",
"type": "text",
"value": "dummy_value"
},
"option60": {
"id": "60",
"type": "text",
"value": "dummy_value"
}
},
"DHCP_SERVER_IPV4_RANGE": {
"range3": {
"range": [
"192.168.0.10",
"192.168.0.10"
]
},
"range2": {
"range": [
"192.168.0.6",
"192.168.0.3"
]
},
"range1": {
"range": [
"192.168.0.2",
"192.168.0.5"
]
},
"range4": {
"range": [
"192.168.0.1"
]
},
"range0": {
"range": [
"192.168.8.2",
"192.168.8.3"
]
}
},
"DHCP_SERVER_IPV4_PORT": {
"Vlan1000|Ethernet24": {
"ips": [
"192.168.0.7"
]
},
"Vlan1000|Ethernet28": {
"ranges": [
"range0",
"range1",
"range2",
"range3",
"range6"
]
},
"Vlan1000|Ethernet32": {
"ips": [
"192.168.0.8"
],
"ranges": [
"range0"
]
},
"Vlan1000|Ethernet36": {
"ips": [
"192.168.0.9"
]
},
"Vlan1000|Ethernet40": {
"ips": [
"192.168.0.10"
]
}
}
}

View File

@ -0,0 +1,149 @@
{
"DEVICE_METADATA": {
"localhost": {
"hostname": "sonic-host"
}
},
"VLAN_INTERFACE": {
"Vlan1000|192.168.0.1/21": {
"NULL": "NULL"
},
"Vlan1000": {
"NULL": "NULL"
},
"Vlan1000|fc02:1000::1/64": {
"NULL": "NULL"
}
},
"VLAN_MEMBER": {
"Vlan1000|Ethernet68": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet72": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet8": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet80": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet96": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet48": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet32": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet4": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet24": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet40": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet52": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet28": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet44": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet60": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet16": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet12": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet84": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet36": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet64": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet92": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet56": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet20": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet88": {
"tagging_mode": "untagged"
},
"Vlan1000|Ethernet76": {
"tagging_mode": "untagged"
}
},
"DHCP_SERVER_IPV4": {
"Vlan1000": {
"customized_options": [
"option60",
"option223"
],
"gateway": "192.168.0.1",
"lease_time": "900",
"mode": "PORT",
"netmask": "255.255.255.0",
"state": "enabled"
},
"Vlan2000": {
"customized_options": [
"option60"
],
"gateway": "192.168.0.1",
"lease_time": "900",
"mode": "PORT",
"netmask": "255.255.255.0",
"state": "disabled"
}
},
"DHCP_SERVER_IPV4_CUSTOMIZED_OPTIONS": {
"option223": {
"id": "223",
"type": "text",
"value": "dummy_value"
},
"option60": {
"id": "60",
"type": "text",
"value": "dummy_value"
}
},
"DHCP_SERVER_IPV4_RANGE": {
"range3": {
"range": [
"192.168.0.10",
"192.168.0.10"
]
},
"range2": {
"range": [
"192.168.0.6",
"192.168.0.3"
]
},
"range1": {
"range": [
"192.168.0.2",
"192.168.0.5"
]
}
},
"DHCP_SERVER_IPV4_PORT": {
}
}

View File

@ -0,0 +1,3 @@
Ethernet24 etp7
Ethernet28 etp8
Ethernet32

View File

@ -0,0 +1,265 @@
import copy
import ipaddress
import json
import pytest
from common_utils import MockConfigDb
from dhcp_server.dhcp_server_utils import DhcpDbConnector
from dhcp_server.dhcp_cfggen import DhcpServCfgGenerator
from unittest.mock import patch, MagicMock
expected_dhcp_config = {
"Dhcp4": {
"hooks-libraries": [
{
"library": "/usr/local/lib/kea/hooks/libdhcp_run_script.so",
"parameters": {
"name": "/etc/kea/lease_update.sh",
"sync": False
}
}
],
"interfaces-config": {
"interfaces": [
"eth0"
]
},
"control-socket": {
"socket-type": "unix",
"socket-name": "/run/kea/kea4-ctrl-socket"
},
"lease-database": {
"type": "memfile",
"persist": True,
"name": "/tmp/kea-lease.csv",
"lfc-interval": 3600
},
"subnet4": [
{
"subnet": "192.168.0.0/21",
"pools": [
{
"pool": "192.168.0.2 - 192.168.0.6",
"client-class": "sonic-host:etp8"
},
{
"pool": "192.168.0.10 - 192.168.0.10",
"client-class": "sonic-host:etp8"
},
{
"pool": "192.168.0.7 - 192.168.0.7",
"client-class": "sonic-host:etp7"
}
],
"option-data": [
{
"name": "routers",
"data": "192.168.0.1"
},
{
"name": "dhcp-server-identifier",
"data": "192.168.0.1"
}
],
"valid-lifetime": 900,
"reservations": []
}
],
"loggers": [
{
"name": "kea-dhcp4",
"output_options": [
{
"output": "/var/log/kea-dhcp.log",
"pattern": "%-5p %m\n"
}
],
"severity": "INFO",
"debuglevel": 0
}
],
"client-classes": [
{
"name": "sonic-host:etp8",
"test": "substring(relay4[1].hex, -15, 15) == 'sonic-host:etp8'"
},
{
"name": "sonic-host:etp7",
"test": "substring(relay4[1].hex, -15, 15) == 'sonic-host:etp7'"
}
]
}
}
expected_dhcp_config_without_port_config = {
"Dhcp4": {
"hooks-libraries": [
{
"library": "/usr/local/lib/kea/hooks/libdhcp_run_script.so",
"parameters": {
"name": "/etc/kea/lease_update.sh",
"sync": False
}
}
],
"interfaces-config": {
"interfaces": [
"eth0"
]
},
"control-socket": {
"socket-type": "unix",
"socket-name": "/run/kea/kea4-ctrl-socket"
},
"lease-database": {
"type": "memfile",
"persist": True,
"name": "/tmp/kea-lease.csv",
"lfc-interval": 3600
},
"subnet4": [
],
"loggers": [
{
"name": "kea-dhcp4",
"output_options": [
{
"output": "/var/log/kea-dhcp.log",
"pattern": "%-5p %m\n"
}
],
"severity": "INFO",
"debuglevel": 0
}
]
}
}
expected_parsed_range = {
"range2": [ipaddress.IPv4Address("192.168.0.3"), ipaddress.IPv4Address("192.168.0.6")],
"range3": [ipaddress.IPv4Address("192.168.0.10"), ipaddress.IPv4Address("192.168.0.10")],
"range1": [ipaddress.IPv4Address("192.168.0.2"), ipaddress.IPv4Address("192.168.0.5")],
"range0": [ipaddress.IPv4Address("192.168.8.2"), ipaddress.IPv4Address("192.168.8.3")]
}
expected_vlan_ipv4_interface = {
"Vlan1000": [{
"ip": "192.168.0.1/21",
"network": ipaddress.ip_network("192.168.0.1/21", strict=False)
}],
"Vlan2000": [
{
"ip": "192.168.1.1/21",
"network": ipaddress.ip_network("192.168.1.1/21", strict=False)
},
{
"ip": "192.168.2.1/21",
"network": ipaddress.ip_network("192.168.2.1/21", strict=False)
}
]
}
expected_parsed_port = {
"Vlan1000": {
"192.168.0.1/21": {
"etp8": [["192.168.0.2", "192.168.0.6"], ["192.168.0.10", "192.168.0.10"]],
"etp7": [["192.168.0.7", "192.168.0.7"]]
}
}
}
tested_parsed_port = {
"Vlan1000": {
"192.168.0.1/21": {
"etp8": [["192.168.0.2", "192.168.0.6"], ["192.168.0.10", "192.168.0.10"]],
"etp7": [["192.168.0.7", "192.168.0.7"]],
"etp9": []
}
}
}
expected_render_obj = {
"subnets": [{
"subnet": "192.168.0.0/21",
"pools": [{"range": "192.168.0.2 - 192.168.0.6", "client_class": "sonic-host:etp8"},
{"range": "192.168.0.10 - 192.168.0.10", "client_class": "sonic-host:etp8"},
{"range": "192.168.0.7 - 192.168.0.7", "client_class": "sonic-host:etp7"}],
"gateway": "192.168.0.1", "server_id": "192.168.0.1", "lease_time": "900"
}],
"client_classes": [
{"name": "sonic-host:etp8", "condition": "substring(relay4[1].hex, -15, 15) == 'sonic-host:etp8'"},
{"name": "sonic-host:etp7", "condition": "substring(relay4[1].hex, -15, 15) == 'sonic-host:etp7'"}
],
"lease_update_script_path": "/etc/kea/lease_update.sh",
"lease_path": "/tmp/kea-lease.csv"
}
def test_parse_port_alias(mock_swsscommon_dbconnector_init, mock_get_render_template):
dhcp_db_connector = DhcpDbConnector()
dhcp_cfg_generator = DhcpServCfgGenerator(dhcp_db_connector,
port_map_path="tests/test_data/port-name-alias-map.txt")
assert dhcp_cfg_generator.port_alias_map == {'Ethernet24': 'etp7', 'Ethernet28': 'etp8'}
@pytest.mark.parametrize("is_success", [True, False])
def test_parse_hostname(is_success, mock_swsscommon_dbconnector_init, mock_parse_port_map_alias,
mock_get_render_template):
mock_config_db = MockConfigDb(config_db_path="tests/test_data/mock_config_db.json")
dhcp_db_connector = DhcpDbConnector()
dhcp_cfg_generator = DhcpServCfgGenerator(dhcp_db_connector)
device_metadata = mock_config_db.config_db.get("DEVICE_METADATA") if is_success else {}
try:
hostname = dhcp_cfg_generator._parse_hostname(device_metadata)
assert hostname == "sonic-host"
except Exception as err:
assert str(err) == "Cannot get hostname"
def test_parse_range(mock_swsscommon_dbconnector_init, mock_parse_port_map_alias, mock_get_render_template):
mock_config_db = MockConfigDb(config_db_path="tests/test_data/mock_config_db.json")
dhcp_db_connector = DhcpDbConnector()
dhcp_cfg_generator = DhcpServCfgGenerator(dhcp_db_connector)
parse_result = dhcp_cfg_generator._parse_range(mock_config_db.config_db.get("DHCP_SERVER_IPV4_RANGE"))
assert parse_result == expected_parsed_range
def test_parse_vlan(mock_swsscommon_dbconnector_init, mock_parse_port_map_alias, mock_get_render_template):
mock_config_db = MockConfigDb(config_db_path="tests/test_data/mock_config_db.json")
dhcp_db_connector = DhcpDbConnector()
dhcp_cfg_generator = DhcpServCfgGenerator(dhcp_db_connector)
vlan_interfaces, vlan_members = dhcp_cfg_generator._parse_vlan(mock_config_db.config_db.get("VLAN_INTERFACE"),
mock_config_db.config_db.get("VLAN_MEMBER"))
assert vlan_interfaces == expected_vlan_ipv4_interface
assert list(vlan_members) == ["Vlan1000|Ethernet24", "Vlan1000|Ethernet28", "Vlan1000|Ethernet40"]
@pytest.mark.parametrize("test_config_db", ["mock_config_db.json", "mock_config_db_without_port_config.json"])
def test_parse_port(test_config_db, mock_swsscommon_dbconnector_init, mock_get_render_template,
mock_parse_port_map_alias):
mock_config_db = MockConfigDb(config_db_path="tests/test_data/{}".format(test_config_db))
dhcp_db_connector = DhcpDbConnector()
dhcp_cfg_generator = DhcpServCfgGenerator(dhcp_db_connector)
tested_vlan_interfaces = expected_vlan_ipv4_interface
tested_ranges = expected_parsed_range
ipv4_port = mock_config_db.config_db.get("DHCP_SERVER_IPV4_PORT")
vlan_members = mock_config_db.config_db.get("VLAN_MEMBER").keys()
parse_result = dhcp_cfg_generator._parse_port(ipv4_port, tested_vlan_interfaces, vlan_members, tested_ranges)
assert parse_result == (expected_parsed_port if test_config_db == "mock_config_db.json" else {})
def test_construct_obj_for_template(mock_swsscommon_dbconnector_init, mock_parse_port_map_alias,
mock_get_render_template):
mock_config_db = MockConfigDb(config_db_path="tests/test_data/mock_config_db.json")
dhcp_db_connector = DhcpDbConnector()
dhcp_cfg_generator = DhcpServCfgGenerator(dhcp_db_connector)
tested_hostname = "sonic-host"
render_obj = dhcp_cfg_generator._construct_obj_for_template(mock_config_db.config_db.get("DHCP_SERVER_IPV4"),
tested_parsed_port, tested_hostname)
assert render_obj == expected_render_obj
@pytest.mark.parametrize("with_port_config", [True, False])
def test_render_config(mock_swsscommon_dbconnector_init, mock_parse_port_map_alias, with_port_config):
dhcp_db_connector = DhcpDbConnector()
dhcp_cfg_generator = DhcpServCfgGenerator(dhcp_db_connector,
kea_conf_template_path="tests/test_data/kea-dhcp4.conf.j2")
render_obj = copy.deepcopy(expected_render_obj)
if not with_port_config:
render_obj["client_classes"] = []
render_obj["subnets"] = []
config = dhcp_cfg_generator._render_config(render_obj)
assert json.loads(config) == expected_dhcp_config if with_port_config else expected_dhcp_config_without_port_config

View File

@ -0,0 +1,103 @@
from dhcp_server.dhcp_server_utils import DhcpDbConnector
from dhcp_server.dhcp_lease import KeaDhcp4LeaseHandler, LeaseHanlder
from swsscommon import swsscommon
from unittest.mock import patch, call, MagicMock
expected_lease = {
"Vlan1000|10:70:fd:b6:13:00": {
"lease_start": "1693997305",
"lease_end": "1693997305",
"ip": "192.168.0.2"
},
"Vlan1000|10:70:fd:b6:13:17": {
"lease_start": "1693997315",
"lease_end": "1694000915",
"ip": "192.168.0.131"
},
"Vlan2000|10:70:fd:b6:13:15": {
"lease_start": "1693995705",
"lease_end": "1693999305",
"ip": "193.168.2.2"
}
}
expected_fdb_info = {
"10:70:fd:b6:13:00": "Vlan1000",
"10:70:fd:b6:13:15": "Vlan2000",
"10:70:fd:b6:13:17": "Vlan1000",
}
def test_read_kea_lease_with_file_not_found(mock_swsscommon_dbconnector_init):
db_connector = DhcpDbConnector()
kea_lease_handler = KeaDhcp4LeaseHandler(db_connector)
try:
kea_lease_handler._read()
except FileNotFoundError:
pass
def test_read_kea_lease(mock_swsscommon_dbconnector_init):
tested_fdb_info = expected_fdb_info
with patch.object(KeaDhcp4LeaseHandler, "_get_fdb_info", return_value=tested_fdb_info):
db_connector = DhcpDbConnector()
kea_lease_handler = KeaDhcp4LeaseHandler(db_connector, lease_file="tests/test_data/kea-lease.csv")
# Verify whether lease information read is as expected
lease = kea_lease_handler._read()
print(lease)
print(expected_lease)
assert lease == expected_lease
def test_get_fdb_info(mock_swsscommon_dbconnector_init):
mock_fdb_table = {
"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:17": {"port": "Ethernet32", "type": "dynamic"}
}
with patch("dhcp_server.dhcp_server_utils.DhcpDbConnector.get_state_db_table", return_value=mock_fdb_table):
db_connector = DhcpDbConnector()
kea_lease_handler = KeaDhcp4LeaseHandler(db_connector, lease_file="tests/test_data/kea-lease.csv")
# Verify whether lease information read is as expected
fdb_info = kea_lease_handler._get_fdb_info()
assert fdb_info == expected_fdb_info
def test_update_kea_lease(mock_swsscommon_dbconnector_init, mock_swsscommon_table_init):
tested_lease = expected_lease
with patch.object(swsscommon.Table, "getKeys"), \
patch.object(swsscommon.DBConnector, "hset") as mock_hset, \
patch.object(KeaDhcp4LeaseHandler, "_read", MagicMock(return_value=tested_lease)), \
patch.object(DhcpDbConnector, "get_state_db_table",
return_value={"Vlan1000|aa:bb:cc:dd:ee:ff": {}, "Vlan1000|10:70:fd:b6:13:00": {}}), \
patch.object(swsscommon.DBConnector, "delete") as mock_delete, \
patch("time.sleep", return_value=None) as mock_sleep:
db_connector = DhcpDbConnector()
kea_lease_handler = KeaDhcp4LeaseHandler(db_connector)
kea_lease_handler.update_lease()
# Verify that old key was deleted
mock_delete.assert_has_calls([
call("DHCP_SERVER_IPV4_LEASE|Vlan1000|10:70:fd:b6:13:00"),
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
# lease_start equals to lease_end
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:17', 'lease_end', '1694000915'),
call('DHCP_SERVER_IPV4_LEASE|Vlan1000|10:70:fd:b6:13:17', 'ip', '192.168.0.131')
])
kea_lease_handler.update_lease()
mock_sleep.assert_called_once_with(2)
def test_no_implement(mock_swsscommon_dbconnector_init):
db_connector = DhcpDbConnector()
lease_handler = LeaseHanlder(db_connector)
try:
lease_handler._read()
except NotImplementedError:
pass
try:
lease_handler.register()
except NotImplementedError:
pass

View File

@ -0,0 +1,84 @@
import dhcp_server.dhcp_server_utils as dhcp_server_utils
import ipaddress
import pytest
from swsscommon import swsscommon
from unittest.mock import patch, call
interval_test_data = {
"ordered_with_overlap": {
"intervals": [["192.168.0.2", "192.168.0.5"], ["192.168.0.3", "192.168.0.6"], ["192.168.0.10", "192.168.0.10"]],
"expected_res": [["192.168.0.2", "192.168.0.6"], ["192.168.0.10", "192.168.0.10"]]
},
"not_order_with_overlap": {
"intervals": [["192.168.0.3", "192.168.0.6"], ["192.168.0.2", "192.168.0.5"], ["192.168.0.10", "192.168.0.10"]],
"expected_res": [["192.168.0.2", "192.168.0.6"], ["192.168.0.10", "192.168.0.10"]]
},
"ordered_without_overlap": {
"intervals": [["192.168.0.2", "192.168.0.5"], ["192.168.0.10", "192.168.0.10"]],
"expected_res": [["192.168.0.2", "192.168.0.5"], ["192.168.0.10", "192.168.0.10"]]
},
"not_ordered_without_overlap": {
"intervals": [["192.168.0.10", "192.168.0.10"], ["192.168.0.2", "192.168.0.5"]],
"expected_res": [["192.168.0.2", "192.168.0.5"], ["192.168.0.10", "192.168.0.10"]]
},
"single_interval": {
"intervals": [["192.168.0.10", "192.168.0.10"]],
"expected_res": [["192.168.0.10", "192.168.0.10"]]
}
}
def test_construct_without_sock(mock_swsscommon_dbconnector_init):
dhcp_server_utils.DhcpDbConnector()
mock_swsscommon_dbconnector_init.assert_has_calls([
call(swsscommon.CONFIG_DB, "127.0.0.1", 6379, 0),
call(swsscommon.STATE_DB, "127.0.0.1", 6379, 0)
])
def test_construct_sock(mock_swsscommon_dbconnector_init):
redis_sock = "/var/run/redis/redis.sock"
dhcp_db_connector = dhcp_server_utils.DhcpDbConnector(redis_sock=redis_sock)
assert dhcp_db_connector.redis_sock == redis_sock
mock_swsscommon_dbconnector_init.assert_has_calls([
call(swsscommon.CONFIG_DB, redis_sock, 0),
call(swsscommon.STATE_DB, redis_sock, 0)
])
def test_get_config_db_table(mock_swsscommon_dbconnector_init, mock_swsscommon_table_init):
dhcp_db_connector = dhcp_server_utils.DhcpDbConnector()
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(swsscommon.Table, "hget", side_effect=mock_hget):
ret = dhcp_db_connector.get_config_db_table("VLAN")
mock_swsscommon_table_init.assert_called_once_with(dhcp_db_connector.config_db, "VLAN")
print(ret)
mock_get_keys.assert_called_once_with()
print(ret)
assert ret == {
"key1": {"list": ["1", "2"], "value": "3,4"},
"key2": {"list": ["1", "2"], "value": "3,4"}
}
@pytest.mark.parametrize("test_type", interval_test_data.keys())
def test_merge_intervals(test_type):
intervals = convert_ip_address_intervals(interval_test_data[test_type]["intervals"])
expected_res = convert_ip_address_intervals(interval_test_data[test_type]["expected_res"])
assert dhcp_server_utils.merge_intervals(intervals) == expected_res
def mock_hget(_, field):
if field == "list":
return False, ""
else:
return True, ""
def convert_ip_address_intervals(intervals):
ret = []
for interval in intervals:
ret.append([ipaddress.ip_address(interval[0]), ipaddress.ip_address(interval[1])])
return ret

View File

@ -0,0 +1,60 @@
import pytest
import psutil
import signal
from dhcp_server.dhcp_server_utils import DhcpDbConnector
from dhcp_server.dhcp_cfggen import DhcpServCfgGenerator
from dhcp_server.dhcpservd import DhcpServd
from unittest.mock import patch, call, MagicMock
def test_dump_dhcp4_config(mock_swsscommon_dbconnector_init):
with patch("dhcp_server.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:
dhcp_db_connector = DhcpDbConnector()
dhcp_cfg_generator = DhcpServCfgGenerator(dhcp_db_connector,
port_map_path="tests/test_data/port-name-alias-map.txt",
kea_conf_template_path="tests/test_data/kea-dhcp4.conf.j2")
dhcpservd = DhcpServd(dhcp_cfg_generator, dhcp_db_connector, kea_dhcp4_config_path="/tmp/kea-dhcp4.conf")
dhcpservd.dump_dhcp4_config()
# Verfiy whether generate() func of dhcp_cfggen is called
mock_generate.assert_called_once_with()
# Verify whether notify func of dhcpservd is called, which is expected to call after new config generated
mock_notify_kea_dhcp4_proc.assert_called_once_with()
@pytest.mark.parametrize("process_list", [["proc1", "proc2", "kea-dhcp4"], ["proc1", "proc2"]])
def test_notify_kea_dhcp4_proc(process_list, mock_swsscommon_dbconnector_init, mock_get_render_template,
mock_parse_port_map_alias):
proc_list = [MockProc(process_name) for process_name in process_list]
with patch.object(psutil, "process_iter", return_value=proc_list), \
patch.object(MockProc, "send_signal", MagicMock()) as mock_send_signal:
dhcp_db_connector = DhcpDbConnector()
dhcp_cfg_generator = DhcpServCfgGenerator(dhcp_db_connector)
dhcpservd = DhcpServd(dhcp_cfg_generator, dhcp_db_connector)
dhcpservd._notify_kea_dhcp4_proc()
if "kea-dhcp4" in process_list:
mock_send_signal.assert_has_calls([
call(signal.SIGHUP)
])
else:
mock_send_signal.assert_not_called()
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:
dhcp_db_connector = DhcpDbConnector()
dhcp_cfg_generator = DhcpServCfgGenerator(dhcp_db_connector)
dhcpservd = DhcpServd(dhcp_cfg_generator, dhcp_db_connector)
dhcpservd.start()
mock_dump.assert_called_once_with()
class MockProc(object):
def __init__(self, name):
self.proc_name = name
def name(self):
return self.proc_name
def send_signal(self, sig_num):
pass