[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:
parent
1dd0becda0
commit
73dd38a5ce
@ -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"]
|
||||
|
@ -0,0 +1 @@
|
||||
group:dhcp-server-ipv4
|
@ -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
|
||||
|
40
dockers/docker-dhcp-server/kea-dhcp4-init.conf
Normal file
40
dockers/docker-dhcp-server/kea-dhcp4-init.conf
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
87
dockers/docker-dhcp-server/kea-dhcp4.conf.j2
Normal file
87
dockers/docker-dhcp-server/kea-dhcp4.conf.j2
Normal 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 %}
|
||||
}
|
||||
}
|
12
dockers/docker-dhcp-server/lease_update.sh
Normal file
12
dockers/docker-dhcp-server/lease_update.sh
Normal 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
|
5
dockers/docker-dhcp-server/port-name-alias-map.txt.j2
Normal file
5
dockers/docker-dhcp-server/port-name-alias-map.txt.j2
Normal 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 -%}
|
27
dockers/docker-dhcp-server/rsyslog/default.conf
Normal file
27
dockers/docker-dhcp-server/rsyslog/default.conf
Normal 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
|
96
dockers/docker-dhcp-server/rsyslog/rsyslog.conf.j2
Normal file
96
dockers/docker-dhcp-server/rsyslog/rsyslog.conf.j2
Normal 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
|
19
dockers/docker-dhcp-server/start.sh
Executable file
19
dockers/docker-dhcp-server/start.sh
Executable 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
|
@ -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
|
||||
|
@ -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)
|
||||
|
10
rules/sonic-dhcp-server.dep
Normal file
10
rules/sonic-dhcp-server.dep
Normal 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)
|
10
rules/sonic-dhcp-server.mk
Normal file
10
rules/sonic-dhcp-server.mk
Normal 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
4
src/sonic-dhcp-server/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
.eggs/
|
||||
build/
|
||||
dist/
|
||||
sonic_dhcpservd.egg-info/
|
330
src/sonic-dhcp-server/dhcp_server/dhcp_cfggen.py
Executable file
330
src/sonic-dhcp-server/dhcp_server/dhcp_cfggen.py
Executable 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
|
154
src/sonic-dhcp-server/dhcp_server/dhcp_lease.py
Normal file
154
src/sonic-dhcp-server/dhcp_server/dhcp_lease.py
Normal 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()
|
99
src/sonic-dhcp-server/dhcp_server/dhcp_server_utils.py
Normal file
99
src/sonic-dhcp-server/dhcp_server/dhcp_server_utils.py
Normal 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
|
61
src/sonic-dhcp-server/dhcp_server/dhcpservd.py
Normal file
61
src/sonic-dhcp-server/dhcp_server/dhcpservd.py
Normal 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()
|
20
src/sonic-dhcp-server/setup.cfg
Normal file
20
src/sonic-dhcp-server/setup.cfg
Normal 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
|
49
src/sonic-dhcp-server/setup.py
Normal file
49
src/sonic-dhcp-server/setup.py
Normal 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"
|
||||
]
|
||||
)
|
12
src/sonic-dhcp-server/tests/common_utils.py
Normal file
12
src/sonic-dhcp-server/tests/common_utils.py
Normal 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, {})
|
35
src/sonic-dhcp-server/tests/conftest.py
Normal file
35
src/sonic-dhcp-server/tests/conftest.py
Normal 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
|
||||
|
||||
|
87
src/sonic-dhcp-server/tests/kea-dhcp4.conf.j2
Normal file
87
src/sonic-dhcp-server/tests/kea-dhcp4.conf.j2
Normal 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 %}
|
||||
}
|
||||
}
|
87
src/sonic-dhcp-server/tests/test_data/kea-dhcp4.conf.j2
Normal file
87
src/sonic-dhcp-server/tests/test_data/kea-dhcp4.conf.j2
Normal 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 %}
|
||||
}
|
||||
}
|
9
src/sonic-dhcp-server/tests/test_data/kea-lease.csv
Normal file
9
src/sonic-dhcp-server/tests/test_data/kea-lease.csv
Normal 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
|
|
159
src/sonic-dhcp-server/tests/test_data/mock_config_db.json
Normal file
159
src/sonic-dhcp-server/tests/test_data/mock_config_db.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@ -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": {
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
Ethernet24 etp7
|
||||
Ethernet28 etp8
|
||||
Ethernet32
|
265
src/sonic-dhcp-server/tests/test_dhcp_cfggen.py
Normal file
265
src/sonic-dhcp-server/tests/test_dhcp_cfggen.py
Normal 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
|
103
src/sonic-dhcp-server/tests/test_dhcp_lease.py
Normal file
103
src/sonic-dhcp-server/tests/test_dhcp_lease.py
Normal 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
|
84
src/sonic-dhcp-server/tests/test_dhcp_server_utils.py
Normal file
84
src/sonic-dhcp-server/tests/test_dhcp_server_utils.py
Normal 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
|
60
src/sonic-dhcp-server/tests/test_dhcpservd.py
Normal file
60
src/sonic-dhcp-server/tests/test_dhcpservd.py
Normal 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
|
Loading…
Reference in New Issue
Block a user