mirror of
				https://github.com/Telecominfraproject/ols-nos.git
				synced 2025-11-03 19:47:56 +00:00 
			
		
		
		
	[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:
		@@ -3,6 +3,7 @@ FROM docker-config-engine-bullseye-{{DOCKER_USERNAME}}:{{DOCKER_USERTAG}}
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
ARG docker_container_name
 | 
					ARG docker_container_name
 | 
				
			||||||
ARG image_version
 | 
					ARG image_version
 | 
				
			||||||
 | 
					RUN [ -f /etc/rsyslog.conf ] && sed -ri "s/%syslogtag%/$docker_container_name#%syslogtag%/;" /etc/rsyslog.conf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Make apt-get non-interactive
 | 
					## Make apt-get non-interactive
 | 
				
			||||||
ENV DEBIAN_FRONTEND=noninteractive
 | 
					ENV DEBIAN_FRONTEND=noninteractive
 | 
				
			||||||
@@ -13,6 +14,7 @@ ENV IMAGE_VERSION=$image_version
 | 
				
			|||||||
RUN apt-get update &&       \
 | 
					RUN apt-get update &&       \
 | 
				
			||||||
    apt-get install -f -y   \
 | 
					    apt-get install -f -y   \
 | 
				
			||||||
        tcpdump             \
 | 
					        tcpdump             \
 | 
				
			||||||
 | 
					        python3-dev         \
 | 
				
			||||||
# For kea build environment
 | 
					# For kea build environment
 | 
				
			||||||
        automake            \
 | 
					        automake            \
 | 
				
			||||||
        libtool             \
 | 
					        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
 | 
					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
 | 
					# 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 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() -%}
 | 
					{% if docker_dhcp_server_debs.strip() -%}
 | 
				
			||||||
# Copy locally-built Debian package dependencies
 | 
					# 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(' ')) }}
 | 
					{{ install_debian_packages(docker_dhcp_server_debs.split(' ')) }}
 | 
				
			||||||
{%- endif %}
 | 
					{%- 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
 | 
					# Remove build stuff we don't need
 | 
				
			||||||
RUN apt-get remove -y devscripts    \
 | 
					RUN apt-get remove -y devscripts    \
 | 
				
			||||||
                automake            \
 | 
					                automake            \
 | 
				
			||||||
                libtool             \
 | 
					                libtool             \
 | 
				
			||||||
                pkg-config          \
 | 
					                pkg-config          \
 | 
				
			||||||
                build-essential     \
 | 
					                build-essential     \
 | 
				
			||||||
 | 
					                python3-dev         \
 | 
				
			||||||
                ccache
 | 
					                ccache
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RUN apt-get clean -y        && \
 | 
					RUN apt-get clean -y        && \
 | 
				
			||||||
@@ -70,10 +83,14 @@ RUN apt-get clean -y        && \
 | 
				
			|||||||
    apt-get autoremove -y   && \
 | 
					    apt-get autoremove -y   && \
 | 
				
			||||||
    rm -rf /debs
 | 
					    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 ["supervisord.conf", "/etc/supervisor/conf.d/"]
 | 
				
			||||||
COPY ["files/supervisor-proc-exit-listener", "/usr/bin"]
 | 
					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 ["critical_processes", "/etc/supervisor/"]
 | 
				
			||||||
 | 
					COPY ["lease_update.sh", "/etc/kea/"]
 | 
				
			||||||
 | 
					COPY ["kea-dhcp4-init.conf", "/etc/kea/kea-dhcp4.conf"]
 | 
				
			||||||
COPY ["cli", "/cli/"]
 | 
					COPY ["cli", "/cli/"]
 | 
				
			||||||
 | 
					COPY ["rsyslog/default.conf", "/etc/rsyslog.d"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ENTRYPOINT ["/usr/bin/docker_init.sh"]
 | 
					ENTRYPOINT ["/usr/bin/docker_init.sh"]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					group:dhcp-server-ipv4
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,21 @@
 | 
				
			|||||||
#!/usr/bin/env bash
 | 
					#!/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
 | 
					# 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
 | 
					# properly configured, we exec /usr/local/bin/supervisord so that it runs as PID 1 for the
 | 
				
			||||||
# duration of the container's lifetime
 | 
					# 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
 | 
					buffer_size=1024
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[eventlistener:supervisor-proc-exit-listener]
 | 
					[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
 | 
					events=PROCESS_STATE_EXITED,PROCESS_STATE_RUNNING
 | 
				
			||||||
autostart=true
 | 
					autostart=true
 | 
				
			||||||
autorestart=unexpected
 | 
					autorestart=unexpected
 | 
				
			||||||
@@ -38,3 +38,26 @@ stdout_logfile=syslog
 | 
				
			|||||||
stderr_logfile=syslog
 | 
					stderr_logfile=syslog
 | 
				
			||||||
dependent_startup=true
 | 
					dependent_startup=true
 | 
				
			||||||
dependent_startup_wait_for=rsyslogd:running
 | 
					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)_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)_INSTALL_DEBS = $(PYTHON3_SWSSCOMMON)
 | 
				
			||||||
 | 
					$(DOCKER_DHCP_SERVER)_PYTHON_WHEELS += $(SONIC_DHCP_SERVER_PY3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
SONIC_DOCKER_IMAGES += $(DOCKER_DHCP_SERVER)
 | 
					SONIC_DOCKER_IMAGES += $(DOCKER_DHCP_SERVER)
 | 
				
			||||||
SONIC_DOCKER_DBG_IMAGES += $(DOCKER_DHCP_SERVER_DBG)
 | 
					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_REQUIRES = updategraph
 | 
				
			||||||
$(DOCKER_MACSEC)_SERVICE_AFTER = swss syncd
 | 
					$(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_VOLUMES += /etc/sonic:/etc/sonic:ro
 | 
				
			||||||
$(DOCKER_DHCP_SERVER)_CONTAINER_TMPFS += /tmp/
 | 
					$(DOCKER_DHCP_SERVER)_CONTAINER_TMPFS += /tmp/
 | 
				
			||||||
$(DOCKER_DHCP_SERVER)_CONTAINER_TMPFS += /var/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_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_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)_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)
 | 
					$(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
 | 
				
			||||||
		Reference in New Issue
	
	Block a user