[dhcp_server] Add config_db monitor and customize options for dhcpservd (#17051)

Why I did it
Add config_db monitor and customize options for dhcpservd. HLD: sonic-net/SONiC#1282

Work item tracking
Microsoft ADO (number only): 25600859
How I did it
Add support to customize unassigned DHCP options. Current support type: binary, boolean, ipv4-address, string, uint8, uint16, uint32
Add db config change monitor for dhcpservd
How to verify it
Unit tests in sonic-dhcp-server all passed
This commit is contained in:
Yaqiang Zhu 2023-11-16 11:56:50 -05:00 committed by GitHub
parent 682057945f
commit 3223ca0156
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1777 additions and 502 deletions

View File

@ -1,6 +1,17 @@
{%- set default_lease_time = 900 -%}
{
"Dhcp4": {
{%- if customized_options %}
"option-def": [
{%- for option_name, config in customized_options.items() %}
{
"name": "{{ option_name }}",
"code": {{ config["id"] }},
"type": "{{ config["type"] }}"
}{% if not loop.last %},{% endif %}
{%- endfor %}
],
{%- endif %}
"hooks-libraries": [
{
"library": "/usr/local/lib/kea/hooks/libdhcp_run_script.so",
@ -44,6 +55,13 @@
{%- endfor%}
],
"option-data": [
{%- for option_name, config in subnet_info["customized_options"].items() %}
{
"name": "{{ option_name }}",
"data": "{{ config["value"] }}",
"always-send": {{ config["always_send"] }}
},
{%- endfor %}
{
"name": "routers",
"data": "{{ subnet_info["gateway"] if "gateway" in subnet_info else subnet_info["server_id"] }}"

View File

@ -1,131 +1,439 @@
import ipaddress
import sys
import syslog
from abc import abstractmethod
from swsscommon import swsscommon
DEFAULT_SELECT_TIMEOUT = 5000 # millisecond
DHCP_SERVER_IPV4 = "DHCP_SERVER_IPV4"
DHCP_SERVER_IPV4_PORT = "DHCP_SERVER_IPV4_PORT"
DHCP_SERVER_IPV4_RANGE = "DHCP_SERVER_IPV4_RANGE"
DHCP_SERVER_IPV4_CUSTOMIZED_OPTIONS = "DHCP_SERVER_IPV4_CUSTOMIZED_OPTIONS"
VLAN = "VLAN"
VLAN_MEMBER = "VLAN_MEMBER"
VLAN_INTERFACE = "VLAN_INTERFACE"
DEFAULT_SELECT_TIMEOUT = 5000 # millisecond
class DhcpDbMonitor(object):
def __init__(self, db_connector, select_timeout=DEFAULT_SELECT_TIMEOUT):
class ConfigDbEventChecker(object):
table_name = ""
subscriber_state_table = None
enabled = False
def __init__(self, sel, db):
"""
Init function
Args:
sel: select object to manage subscribe table
db_connector: db connector
"""
self.sel = sel
self.db = db
self.subscriber_state_table = None
self.enabled = False
@classmethod
def get_parameter_by_name(cls, db_snapshot, param_name):
"""
Check whether db_snapshot valid
Args:
db_snapshot: dict contains db_snapshot param
param_name: parameter name need to check
Returns:
If param_name in db_snapshot return tuple of (True, parameter), else return tuple of (False, None)
"""
if param_name not in db_snapshot:
return False, None
return True, db_snapshot[param_name]
def is_enabled(self):
"""
Check whether checker is enabled
Returns:
If enabled, return True. Else return False
"""
return self.enabled
def enable(self):
"""
Enable checker by subscribe table
Args:
db: db object
"""
if self.enabled:
syslog.syslog(syslog.LOG_ERR, "Cannot enable {} checker due to it is enabled"
.format(self.table_name))
sys.exit(1)
self.subscriber_state_table = swsscommon.SubscriberStateTable(self.db, self.table_name)
self.sel.addSelectable(self.subscriber_state_table)
self.enabled = True
def disable(self):
"""
Disable checker
"""
if not self.enabled:
syslog.syslog(syslog.LOG_ERR, "Cannot disable {} checker due to it is disabled"
.format(self.table_name))
sys.exit(1)
self.sel.removeSelectable(self.subscriber_state_table)
self.enabled = False
def clear_event(self):
"""
Clear update event of subscirbe table
"""
if not self.enabled:
syslog.syslog(syslog.LOG_ERR, "Cannot clear event for table {} due to it is disabled"
.format(self.table_name))
sys.exit(1)
while self.subscriber_state_table.hasData():
_, _, _ = self.subscriber_state_table.pop()
@abstractmethod
def _get_parameter(self, db_snapshot):
"""
Get paramter depends on subclass
Args:
db_snapshot: dict of db snapshot
"""
raise NotImplementedError
@abstractmethod
def _process_check(self, key, op, entry, parameter):
"""
Check whether this event contains value we interested
Args:
key: key of event
op: operation of event
entry: operation entry of event
paramter: parameter used in check
Returns:
If contains, return True, else return False
"""
raise NotImplementedError
def check_update_event(self, db_snapshot):
"""
Function to check whether interested field changed in subscribe table
Args:
db_snapshot: dict contains db_snapshot param
Returns:
If changed, return True, else return False
"""
res, parameter = self._get_parameter(db_snapshot)
if not res:
return True
need_refresh = False
while self.subscriber_state_table.hasData():
key, op, entry = self.subscriber_state_table.pop()
need_refresh |= self._process_check(key, op, entry, parameter)
if need_refresh:
self.clear_event()
return True
return False
def _check_db_snapshot(self, db_snapshot, param_name):
"""
Check whether db_snapshot valid
Args:
db_snapshot: dict contains db_snapshot param
param_name: parameter name need to check
Returns:
If param_name in db_snapshot return True, else return False
"""
if param_name not in db_snapshot:
syslog.syslog(syslog.LOG_ERR, "Expected param: {} is not in db_snapshot".format(param_name))
return False
return True
def get_class_name(self):
"""
Get class name of this object
"""
return type(self).__name__
class DhcpServerTableCfgChangeEventChecker(ConfigDbEventChecker):
"""
This event checker interested in all DHCP server related config change event in DHCP_SERVER_IPV4 table
"""
table_name = DHCP_SERVER_IPV4
def __init__(self, sel, db):
self.table_name = DHCP_SERVER_IPV4
ConfigDbEventChecker.__init__(self, sel, db)
def _get_parameter(self, db_snapshot):
return ConfigDbEventChecker.get_parameter_by_name(db_snapshot, "enabled_dhcp_interfaces")
def _process_check(self, key, op, entry, enabled_dhcp_interfaces):
# If old state is enabled, need refresh
if key in enabled_dhcp_interfaces:
return True
elif op == "SET":
for field, value in entry:
if field != "state":
continue
# If old state is not consistent with new state, need refresh
if value == "enabled":
return True
return False
class DhcpServerTableIntfEnablementEventChecker(ConfigDbEventChecker):
"""
This event checker only interested in DHCP interface enabled/disabled in DHCP_SERVER_IPV4 table
"""
table_name = DHCP_SERVER_IPV4
def __init__(self, sel, db):
self.table_name = DHCP_SERVER_IPV4
ConfigDbEventChecker.__init__(self, sel, db)
def _get_parameter(self, db_snapshot):
return ConfigDbEventChecker.get_parameter_by_name(db_snapshot, "enabled_dhcp_interfaces")
def _process_check(self, key, op, entry, enabled_dhcp_interfaces):
if op == "SET":
for field, value in entry:
if field != "state":
continue
# Only if new state is not consistent with old state, we need to refresh
if key in enabled_dhcp_interfaces and value == "disabled":
return True
elif key not in enabled_dhcp_interfaces and value == "enabled":
return True
# For del operation, we can skip disabled change
if op == "DEL":
if key in enabled_dhcp_interfaces:
return True
return False
class DhcpPortTableEventChecker(ConfigDbEventChecker):
"""
This event checker interested in changes in DHCP_SERVER_IPV4_PORT table
"""
table_name = DHCP_SERVER_IPV4_PORT
def __init__(self, sel, db):
self.table_name = DHCP_SERVER_IPV4_PORT
ConfigDbEventChecker.__init__(self, sel, db)
def _get_parameter(self, db_snapshot):
return ConfigDbEventChecker.get_parameter_by_name(db_snapshot, "enabled_dhcp_interfaces")
def _process_check(self, key, op, entry, enabled_dhcp_interfaces):
dhcp_interface = key.split("|")[0]
# If dhcp interface is enabled, need to generate new configuration
if dhcp_interface in enabled_dhcp_interfaces:
self.clear_event()
return True
return False
class DhcpRangeTableEventChecker(ConfigDbEventChecker):
"""
This event checker interested in changes in DHCP_SERVER_IPV4_RANGE table
"""
table_name = DHCP_SERVER_IPV4_RANGE
def __init__(self, sel, db):
self.table_name = DHCP_SERVER_IPV4_RANGE
ConfigDbEventChecker.__init__(self, sel, db)
def _get_parameter(self, db_snapshot):
return ConfigDbEventChecker.get_parameter_by_name(db_snapshot, "used_range")
def _process_check(self, key, op, entry, used_range):
# If range is used, need to generate new configuration
if key in used_range:
self.clear_event()
return True
return False
class DhcpOptionTableEventChecker(ConfigDbEventChecker):
"""
This event checker interested in changes in DHCP_SERVER_IPV4_CUSTOMIZED_OPTIONS table
"""
table_name = DHCP_SERVER_IPV4_CUSTOMIZED_OPTIONS
def __init__(self, sel, db):
self.table_name = DHCP_SERVER_IPV4_CUSTOMIZED_OPTIONS
ConfigDbEventChecker.__init__(self, sel, db)
def _get_parameter(self, db_snapshot):
return ConfigDbEventChecker.get_parameter_by_name(db_snapshot, "used_options")
def _process_check(self, key, op, entry, used_options):
# If option is used, need to generate new configuration
if key in used_options:
self.clear_event()
return True
return False
class VlanTableEventChecker(ConfigDbEventChecker):
"""
This event checker interested in changes in VLAN table
"""
table_name = VLAN
def __init__(self, sel, db):
self.table_name = VLAN
ConfigDbEventChecker.__init__(self, sel, db)
def _get_parameter(self, db_snapshot):
return ConfigDbEventChecker.get_parameter_by_name(db_snapshot, "enabled_dhcp_interfaces")
def _process_check(self, key, op, entry, enabled_dhcp_interfaces):
# For vlan doesn't have related dhcp entry, not need to refresh dhcrelay process
if key in enabled_dhcp_interfaces:
self.clear_event()
return True
return False
class VlanIntfTableEventChecker(ConfigDbEventChecker):
"""
This event checker interested in changes in VLAN_INTERFACE table
"""
table_name = VLAN_INTERFACE
def __init__(self, sel, db):
self.table_name = VLAN_INTERFACE
ConfigDbEventChecker.__init__(self, sel, db)
def _get_parameter(self, db_snapshot):
return ConfigDbEventChecker.get_parameter_by_name(db_snapshot, "enabled_dhcp_interfaces")
def _process_check(self, key, op, entry, enabled_dhcp_interfaces):
splits = key.split("|")
vlan_name = splits[0]
ip_address = splits[1].split("/")[0] if len(splits) > 1 else None
# For vlan doesn't have related dhcp entry, not need to refresh dhcrelay process
if vlan_name in enabled_dhcp_interfaces and ip_address is not None and \
ipaddress.ip_address(ip_address).version == 4:
self.clear_event()
return True
return False
class VlanMemberTableEventChecker(ConfigDbEventChecker):
"""
This event checker interested in changes in VLAN_MEMBER table
"""
table_name = VLAN_MEMBER
def __init__(self, sel, db):
self.table_name = VLAN_MEMBER
ConfigDbEventChecker.__init__(self, sel, db)
def _get_parameter(self, db_snapshot):
return ConfigDbEventChecker.get_parameter_by_name(db_snapshot, "enabled_dhcp_interfaces")
def _process_check(self, key, op, entry, enabled_dhcp_interfaces):
dhcp_interface = key.split("|")[0]
# If dhcp interface is enabled, need to generate new configuration
if dhcp_interface in enabled_dhcp_interfaces:
self.clear_event()
return True
return False
class DhcpRelaydDbMonitor(object):
checker_dict = {}
def __init__(self, db_connector, sel, checkers, select_timeout=DEFAULT_SELECT_TIMEOUT):
self.db_connector = db_connector
self.sel = swsscommon.Select()
self.sel = sel
self.select_timeout = select_timeout
self.checker_dict = {}
for checker in checkers:
self.checker_dict[checker.get_class_name()] = checker
@abstractmethod
def subscribe_table(self):
def enable_checker(self, checker_names):
"""
Subcribe db table to monitor
Enable checkers
Args:
checker_names: set of tables checker to be enable
"""
raise NotImplementedError
for table in checker_names:
if table not in self.checker_dict:
syslog.syslog(syslog.LOG_ERR, "Cannot find checker for {} in checker_dict".format(table))
continue
self.checker_dict[table].enable()
@abstractmethod
def _do_check(self):
"""
Check whether interested table content changed
"""
raise NotImplementedError
def check_db_update(self, check_param):
def check_db_update(self, db_snapshot):
"""
Fetch db and check update
Args:
db_snapshot: dict contains db snapshot parameter
Returns:
Tuple of dhcp_server table result, vlan table result, vlan_intf table result
"""
state, _ = self.sel.select(self.select_timeout)
if state == swsscommon.Select.TIMEOUT or state != swsscommon.Select.OBJECT:
return (False, False, False)
return (self.checker_dict["DhcpServerTableIntfEnablementEventChecker"].check_update_event(db_snapshot),
self.checker_dict["VlanTableEventChecker"].check_update_event(db_snapshot),
self.checker_dict["VlanIntfTableEventChecker"].check_update_event(db_snapshot))
class DhcpServdDbMonitor(object):
checker_dict = {}
def __init__(self, db_connector, sel, checkers, select_timeout=DEFAULT_SELECT_TIMEOUT):
self.db_connector = db_connector
self.sel = sel
self.select_timeout = select_timeout
self.checker_dict = {}
for checker in checkers:
self.checker_dict[checker.get_class_name()] = checker
def disable_checkers(self, checker_names):
"""
Disable checkers
Args:
checker_names: set contains name of tables need to be disable
"""
for table in checker_names:
if table not in self.checker_dict:
syslog.syslog(syslog.LOG_ERR, "Cannot find checker for {} in checker_dict".format(table))
continue
self.checker_dict[table].disable()
def enable_checkers(self, checker_names):
"""
Enable checkers
Args:
checker_names: set contains name of tables need to be enable
"""
for table in checker_names:
if table not in self.checker_dict:
syslog.syslog(syslog.LOG_ERR, "Cannot find checker for {} in checker_dict".format(table))
continue
self.checker_dict[table].enable()
def check_db_update(self, db_snapshot):
"""
Fetch db and check update
Args:
db_snapshot: dict contains db snapshot parameter
Returns:
Whether need to refresh config file for kea-dhcp-server
"""
state, _ = self.sel.select(self.select_timeout)
if state == swsscommon.Select.TIMEOUT or state != swsscommon.Select.OBJECT:
return False
return self._do_check(check_param)
class DhcpRelaydDbMonitor(DhcpDbMonitor):
subscribe_dhcp_server_table = None
subscribe_vlan_table = None
subscribe_vlan_intf_table = None
def subscribe_table(self):
self.subscribe_dhcp_server_table = swsscommon.SubscriberStateTable(self.db_connector.config_db,
DHCP_SERVER_IPV4)
self.subscribe_vlan_table = swsscommon.SubscriberStateTable(self.db_connector.config_db, VLAN)
self.subscribe_vlan_intf_table = swsscommon.SubscriberStateTable(self.db_connector.config_db, VLAN_INTERFACE)
# Subscribe dhcp_server_ipv4 and vlan/vlan_interface table. No need to subscribe vlan_member table
self.sel.addSelectable(self.subscribe_dhcp_server_table)
self.sel.addSelectable(self.subscribe_vlan_table)
self.sel.addSelectable(self.subscribe_vlan_intf_table)
def _do_check(self, check_param):
if "enabled_dhcp_interfaces" not in check_param:
syslog.syslog(syslog.LOG_ERR, "Cannot get enabled_dhcp_interfaces")
return (True, True, True)
enabled_dhcp_interfaces = check_param["enabled_dhcp_interfaces"]
return (self._check_dhcp_server_update(enabled_dhcp_interfaces),
self._check_vlan_update(enabled_dhcp_interfaces),
self._check_vlan_intf_update(enabled_dhcp_interfaces))
def _check_dhcp_server_update(self, enabled_dhcp_interfaces):
"""
Check dhcp_server_ipv4 table
Args:
enabled_dhcp_interfaces: DHCP interface that enabled dhcp_server
Returns:
Whether need to refresh
"""
need_refresh = False
while self.subscribe_dhcp_server_table.hasData():
key, op, entry = self.subscribe_dhcp_server_table.pop()
if op == "SET":
for field, value in entry:
if field != "state":
continue
# Only if new state is not consistent with old state, we need to refresh
if key in enabled_dhcp_interfaces and value == "disabled":
need_refresh = True
elif key not in enabled_dhcp_interfaces and value == "enabled":
need_refresh = True
# For del operation, we can skip disabled change
if op == "DEL":
if key in enabled_dhcp_interfaces:
need_refresh = True
return need_refresh
def _check_vlan_update(self, enabled_dhcp_interfaces):
"""
Check vlan table
Args:
enabled_dhcp_interfaces: DHCP interface that enabled dhcp_server
Returns:
Whether need to refresh
"""
need_refresh = False
while self.subscribe_vlan_table.hasData():
key, op, _ = self.subscribe_vlan_table.pop()
# For vlan doesn't have related dhcp entry, not need to refresh dhcrelay process
if key not in enabled_dhcp_interfaces:
for checker in self.checker_dict.values():
if not checker.is_enabled():
continue
need_refresh = True
return need_refresh
def _check_vlan_intf_update(self, enabled_dhcp_interfaces):
"""
Check vlan_interface table
Args:
enabled_dhcp_interfaces: DHCP interface that enabled dhcp_server
Returns:
Whether need to refresh
"""
need_refresh = False
while self.subscribe_vlan_intf_table.hasData():
key, _, _ = self.subscribe_vlan_intf_table.pop()
splits = key.split("|")
vlan_name = splits[0]
ip_address = splits[1].split("/")[0] if len(splits) > 1 else None
if vlan_name not in enabled_dhcp_interfaces:
continue
if ip_address is None or ipaddress.ip_address(ip_address).version != 4:
continue
need_refresh = True
if need_refresh:
checker.clear_event()
else:
need_refresh |= checker.check_update_event(db_snapshot)
return need_refresh

View File

@ -1,7 +1,10 @@
import ipaddress
import string
from swsscommon import swsscommon
DEFAULT_REDIS_HOST = "127.0.0.1"
DEFAULT_REDIS_PORT = 6379
SUPPORT_TYPE = ["binary", "boolean", "ipv4-address", "string", "uint8", "uint16", "uint32"]
class DhcpDbConnector(object):
@ -93,6 +96,43 @@ def merge_intervals(intervals):
return ret
def validate_str_type(type, value):
"""
To validate whether type is consistent with string value
Args:
type: string, value type
value: checked value
Returns:
True, type consistent with value
False, type not consistent with value
"""
if not isinstance(value, str):
return False
if type not in SUPPORT_TYPE:
return False
if type == "string":
return True
if type == "binary":
if len(value) == 0 or len(value) % 2 != 0:
return False
return all(c in set(string.hexdigits) for c in value)
if type == "boolean":
return value in ["true", "false"]
if type == "ipv4-address":
try:
if len(value.split(".")) != 4:
return False
return ipaddress.ip_address(value).version == 4
except ValueError:
return False
if type.startswith("uint"):
if not value.isdigit():
return False
length = int("".join([c for c in type if c.isdigit()]))
return 0 <= int(value) <= int(pow(2, length)) - 1
return False
def _parse_table_to_dict(table):
ret = {}
for key in table.getKeys():

View File

@ -8,27 +8,25 @@ import syslog
import time
from swsscommon import swsscommon
from dhcp_server.common.utils import DhcpDbConnector, terminate_proc
from dhcp_server.common.dhcp_db_monitor import DhcpRelaydDbMonitor
from dhcp_server.common.dhcp_db_monitor import DhcpRelaydDbMonitor, DhcpServerTableIntfEnablementEventChecker, \
VlanTableEventChecker, VlanIntfTableEventChecker
REDIS_SOCK_PATH = "/var/run/redis/redis.sock"
DHCP_SERVER_IPV4_SERVER_IP = "DHCP_SERVER_IPV4_SERVER_IP"
DHCP_SERVER_IPV4 = "DHCP_SERVER_IPV4"
VLAN = "VLAN"
VLAN_INTERFACE = "VLAN_INTERFACE"
DEFAULT_SELECT_TIMEOUT = 5000 # millisecond
DHCP_SERVER_INTERFACE = "eth0"
DEFAULT_REFRESH_INTERVAL = 2
DEFAULT_CHECKER = ["DhcpServerTableIntfEnablementEventChecker", "VlanTableEventChecker", "VlanIntfTableEventChecker"]
KILLED_OLD = 1
NOT_KILLED = 2
NOT_FOUND_PROC = 3
class DhcpRelayd(object):
sel = None
enabled_dhcp_interfaces = set()
def __init__(self, db_connector, select_timeout=DEFAULT_SELECT_TIMEOUT):
def __init__(self, db_connector, db_monitor):
"""
Args:
db_connector: db connector obj
@ -36,18 +34,20 @@ class DhcpRelayd(object):
"""
self.db_connector = db_connector
self.last_refresh_time = None
self.dhcp_relayd_monitor = DhcpRelaydDbMonitor(db_connector, select_timeout)
self.dhcp_relayd_monitor = db_monitor
self.enabled_dhcp_interfaces = set()
def start(self):
"""
Start function
"""
self.refresh_dhcrelay()
self.dhcp_relayd_monitor.subscribe_table()
def refresh_dhcrelay(self, force_kill=False):
"""
To refresh dhcrelay/dhcpmon process (start or restart)
Args:
force_kill: if True, force kill old processes
"""
syslog.syslog(syslog.LOG_INFO, "Start to refresh dhcrelay related processes")
dhcp_server_ip = self._get_dhcp_server_ip()
@ -75,9 +75,6 @@ class DhcpRelayd(object):
"""
while True:
res = (self.dhcp_relayd_monitor.check_db_update({"enabled_dhcp_interfaces": self.enabled_dhcp_interfaces}))
# Select timeout or no successful
if isinstance(res, bool):
continue
(dhcp_server_res, vlan_res, vlan_intf_res) = res
# vlan ip change require kill old dhcp_relay related processes
if vlan_intf_res:
@ -182,7 +179,14 @@ class DhcpRelayd(object):
def main():
dhcp_db_connector = DhcpDbConnector(redis_sock=REDIS_SOCK_PATH)
dhcprelayd = DhcpRelayd(dhcp_db_connector)
sel = swsscommon.Select()
checkers = []
checkers.append(DhcpServerTableIntfEnablementEventChecker(sel, dhcp_db_connector.config_db))
checkers.append(VlanIntfTableEventChecker(sel, dhcp_db_connector.config_db))
checkers.append(VlanTableEventChecker(sel, dhcp_db_connector.config_db))
db_monitor = DhcpRelaydDbMonitor(dhcp_db_connector, sel, checkers, DEFAULT_SELECT_TIMEOUT)
db_monitor.enable_checker(DEFAULT_CHECKER)
dhcprelayd = DhcpRelayd(dhcp_db_connector, db_monitor)
dhcprelayd.start()
dhcprelayd.wait()

View File

@ -5,7 +5,7 @@ import os
import syslog
from jinja2 import Environment, FileSystemLoader
from dhcp_server.common.utils import merge_intervals
from dhcp_server.common.utils import merge_intervals, validate_str_type
PORT_MAP_PATH = "/tmp/port-name-alias-map.txt"
UNICODE_TYPE = str
@ -13,12 +13,18 @@ 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"
VLAN_INTERFACE = "VLAN_INTERFACE"
VLAN_MEMBER = "VLAN_MEMBER"
PORT_MODE_CHECKER = ["DhcpServerTableCfgChangeEventChecker", "DhcpPortTableEventChecker", "DhcpRangeTableEventChecker",
"DhcpOptionTableEventChecker", "VlanTableEventChecker", "VlanIntfTableEventChecker",
"VlanMemberTableEventChecker"]
LEASE_UPDATE_SCRIPT_PATH = "/etc/kea/lease_update.sh"
DEFAULT_LEASE_TIME = 900
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
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
DHCP_OPTION_FILE = f"{SCRIPT_DIR}/dhcp_option.csv"
SUPPORT_DHCP_OPTION_TYPE = ["binary", "boolean", "ipv4-address", "string", "uint8", "uint16", "uint32"]
class DhcpServCfgGenerator(object):
@ -27,7 +33,7 @@ class DhcpServCfgGenerator(object):
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,
lease_update_script_path=LEASE_UPDATE_SCRIPT_PATH, dhcp_option_path=DHCP_OPTION_FILE,
kea_conf_template_path=KEA_DHCP4_CONF_TEMPLATE_PATH):
self.db_connector = dhcp_db_connector
self.lease_path = lease_path
@ -36,33 +42,64 @@ class DhcpServCfgGenerator(object):
self._parse_port_map_alias(port_map_path)
# Get kea config template
self._get_render_template(kea_conf_template_path)
self._read_dhcp_option(dhcp_option_path)
def generate(self):
"""
Generate dhcp server config
Returns:
config dict
config string
set of ranges used
set of enabled dhcp interface
set of used options
set of db table need to be monitored
"""
# 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_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)
port_ips, used_ranges = self._parse_port(port_ipv4, vlan_interfaces, vlan_members, ranges)
customized_options = self._parse_customized_options(customized_options_ipv4)
render_obj, enabled_dhcp_interfaces, used_options, subscribe_table = \
self._construct_obj_for_template(dhcp_server_ipv4, port_ips, hostname, customized_options)
return self._render_config(render_obj), used_ranges, enabled_dhcp_interfaces, used_options, subscribe_table
return self._render_config(render_obj)
def _parse_customized_options(self, customized_options_ipv4):
customized_options = {}
for option_name, config in customized_options_ipv4.items():
if config["id"] not in self.dhcp_option.keys():
syslog.syslog(syslog.LOG_ERR, "Unsupported option: {}, currently only support unassigned options"
.format(config["id"]))
continue
option_type = config["type"] if "type" in config else "string"
if option_type not in SUPPORT_DHCP_OPTION_TYPE:
syslog.syslog(syslog.LOG_ERR, "Unsupported type: {}, currently only support {}"
.format(option_type, SUPPORT_DHCP_OPTION_TYPE))
continue
if not validate_str_type(option_type, config["value"]):
syslog.syslog(syslog.LOG_ERR, "Option type [{}] and value [{}] are not consistent"
.format(option_type, config["value"]))
continue
if option_type == "string" and len(config["value"]) > 253:
syslog.syslog(syslog.LOG_ERR, "String option value too long: {}".format(option_name))
continue
always_send = config["always_send"] if "always_send" in config else "true"
customized_options[option_name] = {
"id": config["id"],
"value": config["value"],
"type": option_type,
"always_send": always_send
}
return customized_options
def _render_config(self, render_obj):
output = self.kea_template.render(render_obj)
@ -94,17 +131,30 @@ class DhcpServCfgGenerator(object):
continue
self.port_alias_map[splits[0]] = splits[1]
def _construct_obj_for_template(self, dhcp_server_ipv4, port_ips, hostname):
def _construct_obj_for_template(self, dhcp_server_ipv4, port_ips, hostname, customized_options):
subnets = []
client_classes = []
enabled_dhcp_interfaces = set()
used_options = set()
# Different mode would subscribe different table, always subscribe DHCP_SERVER_IPV4
subscribe_table = set(["DhcpServerTableCfgChangeEventChecker"])
for dhcp_interface_name, dhcp_config in dhcp_server_ipv4.items():
if "state" not in dhcp_config or dhcp_config["state"] != "enabled":
continue
enabled_dhcp_interfaces.add(dhcp_interface_name)
if dhcp_config["mode"] == "PORT":
subscribe_table |= set(PORT_MODE_CHECKER)
if dhcp_interface_name not in port_ips:
syslog.syslog(syslog.LOG_WARNING, "Cannot get DHCP port config for {}"
.format(dhcp_interface_name))
continue
curr_options = {}
for option in dhcp_config["customized_options"]:
if option in customized_options.keys():
curr_options[option] = {
"always_send": customized_options[option]["always_send"],
"value": customized_options[option]["value"]
}
for dhcp_interface_ip, port_config in port_ips[dhcp_interface_name].items():
pools = []
for port_name, ip_ranges in port_config.items():
@ -128,16 +178,19 @@ class DhcpServCfgGenerator(object):
"pools": pools,
"gateway": dhcp_config["gateway"],
"server_id": dhcp_interface_ip.split("/")[0],
"lease_time": dhcp_config["lease_time"]
"lease_time": dhcp_config["lease_time"] if "lease_time" in dhcp_config else DEFAULT_LEASE_TIME,
"customized_options": curr_options
}
used_options = used_options | set(subnet_obj["customized_options"])
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
"lease_path": self.lease_path,
"customized_options": customized_options
}
return render_obj
return render_obj, enabled_dhcp_interfaces, used_options, subscribe_table
def _get_dhcp_ipv4_tables_from_db(self):
"""
@ -262,22 +315,24 @@ class DhcpServCfgGenerator(object):
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']
]
{
'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']
]
}
}
}
}
Set of used ranges.
"""
port_ips = {}
ip_ports = {}
used_ranges = set()
for port_key in list(port_ipv4.keys()):
port_config = port_ipv4.get(port_key, {})
# Cannot specify both 'ips' and 'ranges'
@ -316,6 +371,7 @@ class DhcpServCfgGenerator(object):
if range_name not in ranges:
syslog.syslog(syslog.LOG_WARNING, f"Range {range_name} is not in range table, skip")
continue
used_ranges.add(range_name)
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)
@ -326,4 +382,17 @@ class DhcpServCfgGenerator(object):
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
return port_ips, used_ranges
def _read_dhcp_option(self, file_path):
# TODO current only support unassigned options, use dict in case support more options in the future
# key: option cod, value: option type list
self.dhcp_option = {}
with open(file_path, "r") as file:
lines = file.readlines()
for line in lines:
if "Code,Type,Customized Type" in line:
continue
splits = line.strip().split(",")
if splits[-1] == "unassigned":
self.dhcp_option[splits[0]] = []

View File

@ -0,0 +1,255 @@
Code,Type,Customized Type
1,ipv4-address,defined_unsupported
2,int32,defined_supported
3,ipv4-address,defined_supported
4,ipv4-address,defined_supported
5,ipv4-address,defined_supported
6,ipv4-address,defined_supported
7,ipv4-address,defined_supported
8,ipv4-address,defined_supported
9,ipv4-address,defined_supported
10,ipv4-address,defined_supported
11,ipv4-address,defined_supported
12,string,defined_unsupported
13,uint16,defined_supported
14,string,defined_supported
15,fqdn,defined_supported
16,ipv4-address,defined_supported
17,string,defined_supported
18,string,defined_supported
19,boolean,defined_supported
20,boolean,defined_supported
21,ipv4-address,defined_supported
22,uint16,defined_supported
23,uint8,defined_supported
24,uint32,defined_supported
25,uint16,defined_supported
26,uint16,defined_supported
27,boolean,defined_supported
28,ipv4-address,defined_supported
29,boolean,defined_supported
30,boolean,defined_supported
31,boolean,defined_supported
32,ipv4-address,defined_supported
33,ipv4-address,defined_supported
34,boolean,defined_supported
35,uint32,defined_supported
36,boolean,defined_supported
37,uint8,defined_supported
38,uint32,defined_supported
39,boolean,defined_supported
40,string,defined_supported
41,ipv4-address,defined_supported
42,ipv4-address,defined_supported
43,empty,defined_supported
44,ipv4-address,defined_supported
45,ipv4-address,defined_supported
46,uint8,defined_supported
47,string,defined_supported
48,ipv4-address,defined_supported
49,ipv4-address,defined_supported
50,ipv4-address,defined_unsupported
51,uint32,defined_unsupported
52,uint8,defined_supported
53,string,defined_unsupported
54,ipv4-address,defined_supported
55,uint8 array,defined_unsupported
56,string,defined_supported
57,uint16,defined_supported
58,uint32,defined_unsupported
59,uint32,defined_unsupported
60,string,defined_supported
61,binary,defined_unsupported
62,string,defined_supported
63,binary,defined_supported
64,string,defined_supported
65,ipv4-address,defined_supported
66,string,defined_supported
67,string,defined_supported
68,ipv4-address,defined_supported
69,ipv4-address,defined_supported
70,ipv4-address,defined_supported
71,ipv4-address,defined_supported
72,ipv4-address,defined_supported
73,ipv4-address,defined_supported
74,ipv4-address,defined_supported
75,ipv4-address,defined_supported
76,ipv4-address,defined_supported
77,binary,defined_supported
78,"record (boolean, ipv4-address)",defined_supported
79,"record (boolean, string)",defined_supported
80,,assigned
81,"record (uint8, uint8, uint8, fqdn)",defined_unsupported
82,empty,defined_unsupported
83,,assigned
84,,assigned_unused
85,ipv4-address,defined_supported
86,string,defined_supported
87,string,defined_supported
88,fqdn,defined_supported
89,ipv4-address,defined_supported
90,binary,defined_unsupported
91,uint32,defined_unsupported
92,ipv4-address array,defined_unsupported
93,uint16,defined_supported
94,"record (uint8, uint8, uint8)",defined_supported
95,,assigned_unused
96,,assigned_unused
97,"record (uint8, binary)",defined_supported
98,string,defined_supported
99,binary,defined_supported
100,string,defined_supported
101,string,defined_supported
102,,assigned_unused
103,,assigned_unused
104,,assigned_unused
105,,assigned_unused
106,,assigned_unused
107,,assigned_unused
108,uint32,defined_supported
109,,assigned_unused
110,,assigned_unused
111,,assigned_unused
112,ipv4-address,defined_supported
113,string,defined_supported
114,string,defined_supported
115,,assigned_unused
116,uint8,defined_supported
117,uint16,defined_supported
118,ipv4-address,defined_unsupported
118,,assigned
119,fqdn,defined_supported
120,,assigned
122,,assigned
123,,assigned
124,"record (uint32, binary)",defined_supported
125,uint32,defined_supported
126,,assigned_unused
127,,assigned_unused
128,,assigned
129,,assigned
130,,assigned
131,,assigned
132,,assigned
133,,assigned
134,,assigned
135,,assigned
136,ipv4-address,defined_supported
137,fqdn,defined_supported
138,ipv4-address,defined_supported
139,,assigned
140,,assigned
141,fqdn,defined_supported
142,,assigned
143,tuple,defined_supported
144,,assigned
145,,assigned
146,"record (uint8, ipv4-address, ipv4-address, fqdn)",defined_supported
147,,unassigned
148,,unassigned
149,,unassigned
150,,assigned
151,,assigned
152,,assigned
153,,assigned
154,,assigned
155,,assigned
156,,assigned
157,,assigned
158,,assigned
159,"record (uint8, psid)",defined_supported
160,,assigned
161,,assigned
162,"record (uint16, uint16, uint8, fqdn, binary)",defined_supported
163,,unassigned
164,,unassigned
165,,unassigned
166,,unassigned
167,,unassigned
168,,unassigned
169,,unassigned
170,,unassigned
171,,unassigned
172,,unassigned
173,,unassigned
174,,unassigned
175,,assigned
176,,assigned
177,,assigned
178,,unassigned
179,,unassigned
180,,unassigned
181,,unassigned
182,,unassigned
183,,unassigned
184,,unassigned
185,,unassigned
186,,unassigned
187,,unassigned
188,,unassigned
189,,unassigned
190,,unassigned
191,,unassigned
192,,unassigned
193,,unassigned
194,,unassigned
195,,unassigned
196,,unassigned
197,,unassigned
198,,unassigned
199,,unassigned
200,,unassigned
201,,unassigned
202,,unassigned
203,,unassigned
204,,unassigned
205,,unassigned
206,,unassigned
207,,unassigned
208,,assigned
209,,assigned
210,,assigned
211,,assigned
212,"record (uint8, uint8, ipv6-address, ipv4-address)",defined_supported
213,fqdn,defined_supported
214,,unassigned
215,,unassigned
216,,unassigned
217,,unassigned
218,,unassigned
219,,unassigned
220,,assigned
221,,assigned
222,,unassigned
223,,unassigned
224,,reserved
225,,reserved
226,,reserved
227,,reserved
228,,reserved
229,,reserved
230,,reserved
231,,reserved
232,,reserved
233,,reserved
234,,reserved
235,,reserved
236,,reserved
237,,reserved
238,,reserved
239,,reserved
240,,reserved
241,,reserved
242,,reserved
243,,reserved
244,,reserved
245,,reserved
246,,reserved
247,,reserved
248,,reserved
249,,reserved
250,,reserved
251,,reserved
252,,reserved
253,,reserved
254,,reserved
1 Code Type Customized Type
2 1 ipv4-address defined_unsupported
3 2 int32 defined_supported
4 3 ipv4-address defined_supported
5 4 ipv4-address defined_supported
6 5 ipv4-address defined_supported
7 6 ipv4-address defined_supported
8 7 ipv4-address defined_supported
9 8 ipv4-address defined_supported
10 9 ipv4-address defined_supported
11 10 ipv4-address defined_supported
12 11 ipv4-address defined_supported
13 12 string defined_unsupported
14 13 uint16 defined_supported
15 14 string defined_supported
16 15 fqdn defined_supported
17 16 ipv4-address defined_supported
18 17 string defined_supported
19 18 string defined_supported
20 19 boolean defined_supported
21 20 boolean defined_supported
22 21 ipv4-address defined_supported
23 22 uint16 defined_supported
24 23 uint8 defined_supported
25 24 uint32 defined_supported
26 25 uint16 defined_supported
27 26 uint16 defined_supported
28 27 boolean defined_supported
29 28 ipv4-address defined_supported
30 29 boolean defined_supported
31 30 boolean defined_supported
32 31 boolean defined_supported
33 32 ipv4-address defined_supported
34 33 ipv4-address defined_supported
35 34 boolean defined_supported
36 35 uint32 defined_supported
37 36 boolean defined_supported
38 37 uint8 defined_supported
39 38 uint32 defined_supported
40 39 boolean defined_supported
41 40 string defined_supported
42 41 ipv4-address defined_supported
43 42 ipv4-address defined_supported
44 43 empty defined_supported
45 44 ipv4-address defined_supported
46 45 ipv4-address defined_supported
47 46 uint8 defined_supported
48 47 string defined_supported
49 48 ipv4-address defined_supported
50 49 ipv4-address defined_supported
51 50 ipv4-address defined_unsupported
52 51 uint32 defined_unsupported
53 52 uint8 defined_supported
54 53 string defined_unsupported
55 54 ipv4-address defined_supported
56 55 uint8 array defined_unsupported
57 56 string defined_supported
58 57 uint16 defined_supported
59 58 uint32 defined_unsupported
60 59 uint32 defined_unsupported
61 60 string defined_supported
62 61 binary defined_unsupported
63 62 string defined_supported
64 63 binary defined_supported
65 64 string defined_supported
66 65 ipv4-address defined_supported
67 66 string defined_supported
68 67 string defined_supported
69 68 ipv4-address defined_supported
70 69 ipv4-address defined_supported
71 70 ipv4-address defined_supported
72 71 ipv4-address defined_supported
73 72 ipv4-address defined_supported
74 73 ipv4-address defined_supported
75 74 ipv4-address defined_supported
76 75 ipv4-address defined_supported
77 76 ipv4-address defined_supported
78 77 binary defined_supported
79 78 record (boolean, ipv4-address) defined_supported
80 79 record (boolean, string) defined_supported
81 80 assigned
82 81 record (uint8, uint8, uint8, fqdn) defined_unsupported
83 82 empty defined_unsupported
84 83 assigned
85 84 assigned_unused
86 85 ipv4-address defined_supported
87 86 string defined_supported
88 87 string defined_supported
89 88 fqdn defined_supported
90 89 ipv4-address defined_supported
91 90 binary defined_unsupported
92 91 uint32 defined_unsupported
93 92 ipv4-address array defined_unsupported
94 93 uint16 defined_supported
95 94 record (uint8, uint8, uint8) defined_supported
96 95 assigned_unused
97 96 assigned_unused
98 97 record (uint8, binary) defined_supported
99 98 string defined_supported
100 99 binary defined_supported
101 100 string defined_supported
102 101 string defined_supported
103 102 assigned_unused
104 103 assigned_unused
105 104 assigned_unused
106 105 assigned_unused
107 106 assigned_unused
108 107 assigned_unused
109 108 uint32 defined_supported
110 109 assigned_unused
111 110 assigned_unused
112 111 assigned_unused
113 112 ipv4-address defined_supported
114 113 string defined_supported
115 114 string defined_supported
116 115 assigned_unused
117 116 uint8 defined_supported
118 117 uint16 defined_supported
119 118 ipv4-address defined_unsupported
120 118 assigned
121 119 fqdn defined_supported
122 120 assigned
123 122 assigned
124 123 assigned
125 124 record (uint32, binary) defined_supported
126 125 uint32 defined_supported
127 126 assigned_unused
128 127 assigned_unused
129 128 assigned
130 129 assigned
131 130 assigned
132 131 assigned
133 132 assigned
134 133 assigned
135 134 assigned
136 135 assigned
137 136 ipv4-address defined_supported
138 137 fqdn defined_supported
139 138 ipv4-address defined_supported
140 139 assigned
141 140 assigned
142 141 fqdn defined_supported
143 142 assigned
144 143 tuple defined_supported
145 144 assigned
146 145 assigned
147 146 record (uint8, ipv4-address, ipv4-address, fqdn) defined_supported
148 147 unassigned
149 148 unassigned
150 149 unassigned
151 150 assigned
152 151 assigned
153 152 assigned
154 153 assigned
155 154 assigned
156 155 assigned
157 156 assigned
158 157 assigned
159 158 assigned
160 159 record (uint8, psid) defined_supported
161 160 assigned
162 161 assigned
163 162 record (uint16, uint16, uint8, fqdn, binary) defined_supported
164 163 unassigned
165 164 unassigned
166 165 unassigned
167 166 unassigned
168 167 unassigned
169 168 unassigned
170 169 unassigned
171 170 unassigned
172 171 unassigned
173 172 unassigned
174 173 unassigned
175 174 unassigned
176 175 assigned
177 176 assigned
178 177 assigned
179 178 unassigned
180 179 unassigned
181 180 unassigned
182 181 unassigned
183 182 unassigned
184 183 unassigned
185 184 unassigned
186 185 unassigned
187 186 unassigned
188 187 unassigned
189 188 unassigned
190 189 unassigned
191 190 unassigned
192 191 unassigned
193 192 unassigned
194 193 unassigned
195 194 unassigned
196 195 unassigned
197 196 unassigned
198 197 unassigned
199 198 unassigned
200 199 unassigned
201 200 unassigned
202 201 unassigned
203 202 unassigned
204 203 unassigned
205 204 unassigned
206 205 unassigned
207 206 unassigned
208 207 unassigned
209 208 assigned
210 209 assigned
211 210 assigned
212 211 assigned
213 212 record (uint8, uint8, ipv6-address, ipv4-address) defined_supported
214 213 fqdn defined_supported
215 214 unassigned
216 215 unassigned
217 216 unassigned
218 217 unassigned
219 218 unassigned
220 219 unassigned
221 220 assigned
222 221 assigned
223 222 unassigned
224 223 unassigned
225 224 reserved
226 225 reserved
227 226 reserved
228 227 reserved
229 228 reserved
230 229 reserved
231 230 reserved
232 231 reserved
233 232 reserved
234 233 reserved
235 234 reserved
236 235 reserved
237 236 reserved
238 237 reserved
239 238 reserved
240 239 reserved
241 240 reserved
242 241 reserved
243 242 reserved
244 243 reserved
245 244 reserved
246 245 reserved
247 246 reserved
248 247 reserved
249 248 reserved
250 249 reserved
251 250 reserved
252 251 reserved
253 252 reserved
254 253 reserved
255 254 reserved

View File

@ -7,6 +7,10 @@ import syslog
from .dhcp_cfggen import DhcpServCfgGenerator
from .dhcp_lease import LeaseManager
from dhcp_server.common.utils import DhcpDbConnector
from dhcp_server.common.dhcp_db_monitor import DhcpServdDbMonitor, DhcpServerTableCfgChangeEventChecker, \
DhcpOptionTableEventChecker, DhcpRangeTableEventChecker, DhcpPortTableEventChecker, VlanIntfTableEventChecker, \
VlanMemberTableEventChecker, VlanTableEventChecker
from swsscommon import swsscommon
KEA_DHCP4_CONFIG = "/etc/kea/kea-dhcp4.conf"
KEA_DHCP4_PROC_NAME = "kea-dhcp4"
@ -15,13 +19,19 @@ REDIS_SOCK_PATH = "/var/run/redis/redis.sock"
DHCP_SERVER_IPV4_SERVER_IP = "DHCP_SERVER_IPV4_SERVER_IP"
DHCP_SERVER_INTERFACE = "eth0"
AF_INET = 2
DEFAULT_SELECT_TIMEOUT = 5000 # millisecond
class DhcpServd(object):
def __init__(self, dhcp_cfg_generator, db_connector, kea_dhcp4_config_path=KEA_DHCP4_CONFIG):
enabled_checker = None
dhcp_servd_monitor = None
def __init__(self, dhcp_cfg_generator, db_connector, monitor, 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
self.dhcp_servd_monitor = monitor
self.enabled_checker = None
def _notify_kea_dhcp4_proc(self):
"""
@ -36,7 +46,16 @@ class DhcpServd(object):
"""
Generate kea-dhcp4 config file and dump it to config folder
"""
kea_dhcp4_config = self.dhcp_cfg_generator.generate()
kea_dhcp4_config, used_ranges, enabled_dhcp_interfaces, used_options, enable_checker = \
self.dhcp_cfg_generator.generate()
if self.enabled_checker is not None and self.enabled_checker != enable_checker:
# Has subcribe table and no equal, need to resubscribe
self.dhcp_servd_monitor.disable_checkers(self.enabled_checker - enable_checker)
self.dhcp_servd_monitor.enable_checkers(enable_checker - self.enabled_checker)
self.enabled_checker = enable_checker
self.used_range = used_ranges
self.enabled_dhcp_interfaces = enabled_dhcp_interfaces
self.used_options = used_options
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
@ -63,20 +82,36 @@ class DhcpServd(object):
def start(self):
self.dump_dhcp4_config()
self._update_dhcp_server_ip()
self.dhcp_servd_monitor.enable_checkers(self.enabled_checker)
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)
db_snapshot = {
"enabled_dhcp_interfaces": self.enabled_dhcp_interfaces,
"used_range": self.used_range,
"used_options": self.used_options
}
res = self.dhcp_servd_monitor.check_db_update(db_snapshot)
if res:
self.dump_dhcp4_config()
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)
sel = swsscommon.Select()
checkers = []
checkers.append(DhcpServerTableCfgChangeEventChecker(sel, dhcp_db_connector.config_db))
checkers.append(DhcpPortTableEventChecker(sel, dhcp_db_connector.config_db))
checkers.append(DhcpOptionTableEventChecker(sel, dhcp_db_connector.config_db))
checkers.append(DhcpRangeTableEventChecker(sel, dhcp_db_connector.config_db))
checkers.append(VlanTableEventChecker(sel, dhcp_db_connector.config_db))
checkers.append(VlanIntfTableEventChecker(sel, dhcp_db_connector.config_db))
checkers.append(VlanMemberTableEventChecker(sel, dhcp_db_connector.config_db))
dhcp_servd_monitor = DhcpServdDbMonitor(dhcp_db_connector, sel, checkers, DEFAULT_SELECT_TIMEOUT)
dhcpservd = DhcpServd(dhcp_cfg_generator, dhcp_db_connector, dhcp_servd_monitor)
dhcpservd.start()
dhcpservd.wait()

View File

@ -41,5 +41,8 @@ setup(
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8"
]
],
package_data={
"dhcp_server.dhcpservd": ["dhcp_option.csv"]
}
)

View File

@ -3,7 +3,17 @@ import json
import psutil
MOCK_CONFIG_DB_PATH = "tests/test_data/mock_config_db.json"
MOCK_STATE_DB_PATH = "tests/test_data/mock_state_db.json"
TEST_DATA_PATH = "tests/test_data/dhcp_db_monitor_test_data.json"
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"
VLAN = "VLAN"
VLAN_INTERFACE = "VLAN_INTERFACE"
VLAN_MEMBER = "VLAN_MEMBER"
PORT_MODE_CHECKER = ["DhcpServerTableCfgChangeEventChecker", "DhcpPortTableEventChecker", "DhcpRangeTableEventChecker",
"DhcpOptionTableEventChecker", "VlanTableEventChecker", "VlanIntfTableEventChecker",
"VlanMemberTableEventChecker"]
class MockConfigDb(object):
@ -25,21 +35,16 @@ class MockSelect(object):
class MockSubscribeTable(object):
def __init__(self, tables):
"""
Args:
tables: table update event, sample: [
("Vlan1000", "SET", (("state", "enabled"),)),
("Vlan1000", "SET", (("customized_options", "option1"), ("state", "enabled"),))
]
"""
self.stack = []
for item in tables:
heapq.heappush(self.stack, item)
# if table_name == "DHCP_SERVER_IPV4":
# heapq.heappush(self.stack, ("Vlan1000", "SET", (("state", "enabled"),)))
# heapq.heappush(self.stack, ("Vlan1000", "SET", (("customized_options", "option1"), ("state", "enabled"),)))
# heapq.heappush(self.stack, ("Vlan2000", "SET", (("state", "enabled"),)))
# heapq.heappush(self.stack, ("Vlan1000", "DEL", ()))
# heapq.heappush(self.stack, ("Vlan2000", "DEL", ()))
# if table_name == "VLAN":
# heapq.heappush(self.stack, ("Vlan1000", "SET", (("vlanid", "1000"),)))
# heapq.heappush(self.stack, ("Vlan1001", "SET", (("vlanid", "1001"),)))
# heapq.heappush(self.stack, ("Vlan1001", "DEL", (("vlanid", "1001"),)))
# heapq.heappush(self.stack, ("Vlan1002", "SET", (("vlanid", "1002"),)))
# heapq.heappush(self.stack, ("Vlan2000", "SET", (("vlanid", "2000"),)))
def pop(self):
res = heapq.heappop(self.stack)
@ -85,3 +90,21 @@ class MockProc(object):
class MockPopen(object):
def __init__(self, pid):
self.pid = pid
def mock_exit_func(status):
raise SystemExit(status)
def get_subscribe_table_tested_data(test_name):
test_obj = {}
with open(TEST_DATA_PATH, "r") as file:
test_obj = json.loads(file.read())
tested_data = test_obj[test_name]
for data in tested_data:
for i in range(len(data["table"])):
for j in range(len(data["table"][i][2])):
data["table"][i][2][j] = tuple(data["table"][i][2][j])
data["table"][i][2] = tuple(data["table"][i][2])
data["table"][i] = tuple(data["table"][i])
return tested_data

View File

@ -5,11 +5,13 @@ import sys
from unittest.mock import patch, PropertyMock
from dhcp_server.dhcpservd.dhcp_cfggen import DhcpServCfgGenerator
test_path = os.path.dirname(os.path.abspath(__file__))
modules_path = os.path.dirname(test_path)
sys.path.insert(0, test_path)
sys.path.insert(0, modules_path)
@pytest.fixture(scope="function")
def mock_swsscommon_dbconnector_init():
with patch.object(utils.swsscommon.DBConnector, "__init__", return_value=None) as mock_dbconnector_init:
@ -39,4 +41,3 @@ def mock_parse_port_map_alias(scope="function"):
new_callable=PropertyMock), \
patch.object(DhcpServCfgGenerator, "lease_path", return_value="/tmp/kea-lease.csv", new_callable=PropertyMock):
yield mock_map

View File

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

View File

@ -0,0 +1,246 @@
{
"test_table_clear": [
{
"table": [
["range1", "SET", [["ranges", "192.168.0.1,192.168.0.5"]]]
]
}
],
"test_range_update": [
{
"table": [
["range1", "SET", [["ranges", "192.168.0.1,192.168.0.5"]]]
],
"exp_res": true
},
{
"table": [
["range2", "SET", [["ranges", "192.168.0.1,192.168.0.3"]]]
],
"exp_res": false
},
{
"table": [
["range1", "DEL", []]
],
"exp_res": true
},
{
"table": [
["range2", "DEL", []]
],
"exp_res": false
}
],
"test_port_update": [
{
"table": [
["Vlan1000|Ethernet15", "SET", [["ips", "192.168.0.1,192.168.0.2"]]]
],
"exp_res": true
},
{
"table": [
["Vlan2000|Ethernet15", "SET", [["ips", "192.168.0.1,192.168.0.2"]]]
],
"exp_res": false
},
{
"table": [
["Vlan2000|Ethernet15", "DEL", []]
],
"exp_res": false
},
{
"table": [
["Vlan1000|Ethernet15", "DEL", []]
],
"exp_res": true
}
],
"test_option_update": [
{
"table": [
["option223", "SET", [["type", "string"]]]
],
"exp_res": true
},
{
"table": [],
"exp_res": false
},
{
"table": [
["option222", "SET", [["type", "string"]]]
],
"exp_res": false
}
],
"test_vlan_intf_update": [
{
"table": [
["Vlan1000", "SET", []]
],
"exp_res": false
},
{
"table": [
["Vlan1000|192.168.0.1/24", "SET", []]
],
"exp_res": true
},
{
"table": [
["Vlan1000|fc02::8/64", "SET", []]
],
"exp_res": false
},
{
"table": [
["Vlan2000|192.168.0.1/24", "SET", []]
],
"exp_res": false
},
{
"table": [
["Vlan1000|192.168.0.1/24", "DEL", []]
],
"exp_res": true
},
{
"table": [
["Vlan1000", "DEL", []]
],
"exp_res": false
}
],
"test_vlan_update": [
{
"table": [
["Vlan1000", "SET", [["vlanid", "1000"]]]
],
"exp_res": true
},
{
"table": [
["Vlan1001", "SET", [["vlanid", "1001"]]]
],
"exp_res": false
},
{
"table": [
["Vlan1000", "SET", [["vlanid", "1000"]]],
["Vlan1002", "SET", [["vlanid", "1002"]]]
],
"exp_res": true
},
{
"table": [
["Vlan1001", "DEL", []]
],
"exp_res": false
},
{
"table": [
["Vlan1000", "DEL", []]
],
"exp_res": true
},
{
"table": [
["Vlan1000", "SET", [["vlanid", "1000"]]],
["Vlan1001", "DEL", []]
],
"exp_res": true
},
{
"table": [
["Vlan1003", "SET", [["vlanid", "1003"]]]
],
"exp_res": false
}
],
"test_dhcp_server_update": [
{
"table": [
["Vlan1000", "SET", [["customized_options", "option1"], ["state", "enabled"]]]
],
"exp_res": {"enablement": false, "cfg_change": true}
},
{
"table": [
["Vlan2000", "SET", [["customized_options", "option1"], ["state", "enabled"]]]
],
"exp_res": true
},
{
"table": [
["Vlan2000", "SET", [["state", "enabled"]]]
],
"exp_res": true
},
{
"table": [
["Vlan1000", "SET", [["state", "disabled"]]]
],
"exp_res": true
},
{
"table": [
["Vlan3000", "SET", [["state", "disabled"]]]
],
"exp_res": false
},
{
"table": [
["Vlan1000", "DEL", []]
],
"exp_res": true
},
{
"table": [
["Vlan2000", "DEL", []]
],
"exp_res": false
},
{
"table": [
["Vlan2000", "DEL", []],
["Vlan1000", "DEL", []]
],
"exp_res": true
},
{
"table": [
["Vlan3000", "SET", [["state", "enabled"]]]
],
"exp_res": true
}
],
"test_vlan_member_update": [
{
"table": [
["Vlan1000|Ethernet0", "SET", [["tagging_mode", "untagged"]]]
],
"exp_res": true
},
{
"table": [
["Vlan1000|Ethernet0", "DEL", []]
],
"exp_res": true
},
{
"table": [
["Vlan2000|Ethernet0", "SET", [["tagging_mode", "untagged"]]]
],
"exp_res": false
},
{
"table": [
["Vlan2000|Ethernet0", "DEL", []]
],
"exp_res": false
}
]
}

View File

@ -1,6 +1,17 @@
{%- set default_lease_time = 900 -%}
{
"Dhcp4": {
{%- if customized_options %}
"option-def": [
{%- for option_name, config in customized_options.items() %}
{
"name": "{{ option_name }}",
"code": {{ config["id"] }},
"type": "{{ config["type"] }}"
}{% if not loop.last %},{% endif %}
{%- endfor %}
],
{%- endif %}
"hooks-libraries": [
{
"library": "/usr/local/lib/kea/hooks/libdhcp_run_script.so",
@ -44,6 +55,13 @@
{%- endfor%}
],
"option-data": [
{%- for option_name, config in subnet_info["customized_options"].items() %}
{
"name": "{{ option_name }}",
"data": "{{ config["value"] }}",
"always-send": {{ config["always_send"] }}
},
{%- endfor %}
{
"name": "routers",
"data": "{{ subnet_info["gateway"] if "gateway" in subnet_info else subnet_info["server_id"] }}"

View File

@ -94,18 +94,6 @@
"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": [

View File

@ -2,12 +2,20 @@ import copy
import ipaddress
import json
import pytest
from common_utils import MockConfigDb
from common_utils import MockConfigDb, mock_get_config_db_table, PORT_MODE_CHECKER
from dhcp_server.common.utils import DhcpDbConnector
from dhcp_server.dhcpservd.dhcp_cfggen import DhcpServCfgGenerator
from unittest.mock import patch
expected_dhcp_config = {
"Dhcp4": {
"option-def": [
{
"name": "option223",
"code": 223,
"type": "string"
}
],
"hooks-libraries": [
{
"library": "/usr/local/lib/kea/hooks/libdhcp_run_script.so",
@ -171,27 +179,98 @@ tested_parsed_port = {
}
}
expected_render_obj = {
"subnets": [{
"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"
}],
"gateway": "192.168.0.1", "server_id": "192.168.0.1", "lease_time": "900",
"customized_options": {
"option223": {
"always_send": "true",
"value": "dummy_value"
}
}
}
],
"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"
"lease_path": "/tmp/kea-lease.csv",
"customized_options": {
"option223": {
"id": "223",
"value": "dummy_value",
"type": "string",
"always_send": "true"
}
}
}
tested_options_data = [
{
"data": {
"option223": {
"id": "223",
"type": "string",
"value": "dummy_value"
}
},
"res": True
},
{
"data": {
"option60": {
"id": "60",
"type": "string",
"value": "dummy_value"
}
},
"res": False
},
{
"data": {
"option222": {
"id": "222",
"type": "text",
"value": "dummy_value"
}
},
"res": False
},
{
"data": {
"option219": {
"id": "219",
"type": "uint8",
"value": "259"
}
},
"res": False
},
{
"data": {
"option223": {
"id": "223",
"type": "string",
"value": "long_valuelong_valuelong_valuelong_valuelong_valuelong_valuelong_valuelong_valuelong_value" +
"long_valuelong_valuelong_valuelong_valuelong_valuelong_valuelong_valuelong_valuelong_value" +
"long_valuelong_valuelong_valuelong_valuelong_valuelong_valuelong_valuelong_valuelong_value" +
"long_valuelong_valuelong_valuelong_valuelong_value"
}
},
"res": False
}
]
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'}
assert dhcp_cfg_generator.port_alias_map == {"Ethernet24": "etp7", "Ethernet28": "etp8"}
@pytest.mark.parametrize("is_success", [True, False])
@ -236,29 +315,90 @@ def test_parse_port(test_config_db, mock_swsscommon_dbconnector_init, mock_get_r
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 {})
parsed_port, used_ranges = dhcp_cfg_generator._parse_port(ipv4_port, tested_vlan_interfaces, vlan_members,
tested_ranges)
assert parsed_port == (expected_parsed_port if test_config_db == "mock_config_db.json" else {})
assert used_ranges == ({"range2", "range1", "range0", "range3"}
if test_config_db == "mock_config_db.json" else set())
def test_generate(mock_swsscommon_dbconnector_init, mock_parse_port_map_alias, mock_get_render_template):
with patch.object(DhcpServCfgGenerator, "_parse_hostname"), \
patch.object(DhcpServCfgGenerator, "_parse_vlan", return_value=(None, None)), \
patch.object(DhcpServCfgGenerator, "_get_dhcp_ipv4_tables_from_db", return_value=(None, None, None, None)), \
patch.object(DhcpServCfgGenerator, "_parse_range"), \
patch.object(DhcpServCfgGenerator, "_parse_port", return_value=(None, set(["range1"]))), \
patch.object(DhcpServCfgGenerator, "_parse_customized_options"), \
patch.object(DhcpServCfgGenerator, "_construct_obj_for_template",
return_value=(None, set(["Vlan1000"]), set(["option1"]), set(["dummy"]))), \
patch.object(DhcpServCfgGenerator, "_render_config", return_value="dummy_config"), \
patch.object(DhcpDbConnector, "get_config_db_table", side_effect=mock_get_config_db_table):
dhcp_db_connector = DhcpDbConnector()
dhcp_cfg_generator = DhcpServCfgGenerator(dhcp_db_connector)
kea_dhcp4_config, used_ranges, enabled_dhcp_interfaces, used_options, subscribe_table = \
dhcp_cfg_generator.generate()
assert kea_dhcp4_config == "dummy_config"
assert used_ranges == set(["range1"])
assert enabled_dhcp_interfaces == set(["Vlan1000"])
assert used_options == set(["option1"])
assert subscribe_table == set(["dummy"])
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()
customized_options = {"option223": {"id": "223", "value": "dummy_value", "type": "string", "always_send": "true"}}
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)
render_obj, enabled_dhcp_interfaces, used_options, subscribe_table = \
dhcp_cfg_generator._construct_obj_for_template(mock_config_db.config_db.get("DHCP_SERVER_IPV4"),
tested_parsed_port, tested_hostname, customized_options)
assert render_obj == expected_render_obj
assert enabled_dhcp_interfaces == {"Vlan1000", "Vlan4000", "Vlan3000"}
assert used_options == set(["option223"])
assert subscribe_table == set(PORT_MODE_CHECKER)
@pytest.mark.parametrize("with_port_config", [True, False])
def test_render_config(mock_swsscommon_dbconnector_init, mock_parse_port_map_alias, with_port_config):
@pytest.mark.parametrize("with_option_config", [True, False])
def test_render_config(mock_swsscommon_dbconnector_init, mock_parse_port_map_alias, with_port_config,
with_option_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)
expected_config = copy.deepcopy(expected_dhcp_config)
if not with_port_config:
render_obj["client_classes"] = []
render_obj["subnets"] = []
elif not with_option_config:
render_obj["subnets"][0]["customized_options"] = {}
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
if with_option_config:
expected_config["Dhcp4"]["subnet4"][0]["option-data"].insert(0, {
"name": "option223",
"data": "dummy_value",
"always-send": True
})
assert json.loads(config) == expected_config if with_port_config else expected_config
@pytest.mark.parametrize("tested_options_data", tested_options_data)
def test_parse_customized_options(mock_swsscommon_dbconnector_init, mock_get_render_template,
mock_parse_port_map_alias, tested_options_data):
dhcp_db_connector = DhcpDbConnector()
dhcp_cfg_generator = DhcpServCfgGenerator(dhcp_db_connector)
customized_options_ipv4 = tested_options_data["data"]
customized_options = dhcp_cfg_generator._parse_customized_options(customized_options_ipv4)
if tested_options_data["res"]:
assert customized_options == {
"option223": {
"id": "223",
"value": "dummy_value",
"type": "string",
"always_send": "true"
}
}
else:
assert customized_options == {}

View File

@ -1,219 +1,351 @@
import pytest
from common_utils import MockSubscribeTable
from dhcp_server.common.dhcp_db_monitor import DhcpDbMonitor, DhcpRelaydDbMonitor
import sys
from common_utils import MockSubscribeTable, get_subscribe_table_tested_data, \
PORT_MODE_CHECKER, mock_exit_func
from dhcp_server.common.dhcp_db_monitor import DhcpRelaydDbMonitor, DhcpServdDbMonitor, ConfigDbEventChecker, \
DhcpServerTableIntfEnablementEventChecker, DhcpServerTableCfgChangeEventChecker, \
DhcpPortTableEventChecker, DhcpRangeTableEventChecker, DhcpOptionTableEventChecker, \
VlanTableEventChecker, VlanMemberTableEventChecker, VlanIntfTableEventChecker
from dhcp_server.common.utils import DhcpDbConnector
from swsscommon import swsscommon
from unittest.mock import patch, call, ANY, PropertyMock
tested_subscribe_dhcp_server_table = [
{
"table": [
("Vlan1000", "SET", (("customized_options", "option1"), ("state", "enabled"),))
],
"exp_res": False
},
{
"table": [
("Vlan2000", "SET", (("state", "enabled"),))
],
"exp_res": True
},
{
"table": [
("Vlan1000", "DEL", ())
],
"exp_res": True
},
{
"table": [
("Vlan2000", "DEL", ())
],
"exp_res": False
},
{
"table": [
("Vlan2000", "DEL", ()),
("Vlan1000", "DEL", ())
],
"exp_res": True
},
{
"table": [
("Vlan3000", "SET", (("state", "enabled"),))
],
"exp_res": True
}
]
tested_subscribe_vlan_table = [
{
"table": [
("Vlan1000", "SET", (("vlanid", "1000"),))
],
"exp_res": True
},
{
"table": [
("Vlan1001", "SET", (("vlanid", "1001"),))
],
"exp_res": False
},
{
"table": [
("Vlan1000", "SET", (("vlanid", "1000"),)),
("Vlan1002", "SET", (("vlanid", "1002"),))
],
"exp_res": True
},
{
"table": [
("Vlan1001", "DEL", ())
],
"exp_res": False
},
{
"table": [
("Vlan1000", "DEL", ())
],
"exp_res": True
},
{
"table": [
("Vlan1000", "SET", (("vlanid", "1000"),)),
("Vlan1001", "DEL", ())
],
"exp_res": True
},
{
"table": [
("Vlan1003", "SET", (("vlanid", "1003"),))
],
"exp_res": False
}
]
tested_subscribe_vlan_intf_table = [
{
"table": [
("Vlan1000", "SET", ())
],
"exp_res": False
},
{
"table": [
("Vlan1000|192.168.0.1/24", "SET", ())
],
"exp_res": True
},
{
"table": [
("Vlan1000|fc02::8/64", "SET", ())
],
"exp_res": False
},
{
"table": [
("Vlan2000|192.168.0.1/24", "SET", ())
],
"exp_res": False
},
{
"table": [
("Vlan1001|192.168.0.1/24", "SET", ())
],
"exp_res": False
}
]
from unittest.mock import patch, ANY, PropertyMock, MagicMock
@pytest.mark.parametrize("select_result", [swsscommon.Select.TIMEOUT, swsscommon.Select.OBJECT])
def test_dhcp_db_monitor(mock_swsscommon_dbconnector_init, select_result):
db_connector = DhcpDbConnector()
dhcp_db_monitor = DhcpDbMonitor(db_connector)
try:
dhcp_db_monitor.subscribe_table()
except NotImplementedError:
pass
try:
dhcp_db_monitor._do_check()
except NotImplementedError:
pass
with patch.object(DhcpDbMonitor, "_do_check", return_value=None) as mock_do_check, \
patch.object(swsscommon.Select, "select", return_value=(select_result, None)):
dhcp_db_monitor.check_db_update("mock_param")
if select_result == swsscommon.Select.TIMEOUT:
mock_do_check.assert_not_called()
elif select_result == swsscommon.Select.OBJECT:
mock_do_check.assert_called_once_with("mock_param")
def test_dhcp_relayd_monitor_subscribe_table(mock_swsscommon_dbconnector_init):
with patch.object(swsscommon, "SubscriberStateTable", side_effect=mock_subscriber_state_table) as mock_subscribe, \
patch.object(swsscommon.Select, "addSelectable", return_value=None) as mock_add_select:
def test_dhcp_relayd_monitor_check_db_update(mock_swsscommon_dbconnector_init, select_result):
with patch.object(DhcpServerTableIntfEnablementEventChecker, "check_update_event") \
as mock_check_update_event, \
patch.object(VlanTableEventChecker, "check_update_event") as mock_check_vlan_update, \
patch.object(VlanIntfTableEventChecker, "check_update_event") as mock_check_vlan_intf_update, \
patch.object(swsscommon.Select, "select", return_value=(select_result, None)), \
patch.object(ConfigDbEventChecker, "enable"):
db_connector = DhcpDbConnector()
dhcp_relayd_db_monitor = DhcpRelaydDbMonitor(db_connector)
dhcp_relayd_db_monitor.subscribe_table()
mock_subscribe.assert_has_calls([
call(ANY, "DHCP_SERVER_IPV4"),
call(ANY, "VLAN"),
call(ANY, "VLAN_INTERFACE")
])
mock_add_select.assert_has_calls([
call("DHCP_SERVER_IPV4"),
call("VLAN"),
call("VLAN_INTERFACE")
])
@pytest.mark.parametrize("check_param", [{}, {"enabled_dhcp_interfaces": "dummy"}])
def test_dhcp_relayd_monitor_do_check(mock_swsscommon_dbconnector_init, check_param):
with patch.object(DhcpRelaydDbMonitor, "_check_dhcp_server_update") as mock_check_dhcp_server_update, \
patch.object(DhcpRelaydDbMonitor, "_check_vlan_update") as mock_check_vlan_update, \
patch.object(DhcpRelaydDbMonitor, "_check_vlan_intf_update") as mock_check_vlan_intf_update:
db_connector = DhcpDbConnector()
dhcp_relayd_db_monitor = DhcpRelaydDbMonitor(db_connector)
dhcp_relayd_db_monitor._do_check(check_param)
if "enabled_dhcp_interfaces" in check_param:
mock_check_dhcp_server_update.assert_called_once_with("dummy")
mock_check_vlan_update.assert_called_once_with("dummy")
mock_check_vlan_intf_update.assert_called_once_with("dummy")
checkers = [VlanTableEventChecker(None, None), VlanIntfTableEventChecker(None, None),
DhcpServerTableIntfEnablementEventChecker(None, None)]
dhcp_relayd_db_monitor = DhcpRelaydDbMonitor(db_connector, swsscommon.Select(), checkers)
tested_db_snapshot = {"enabled_dhcp_interfaces": "dummy"}
dhcp_relayd_db_monitor.check_db_update(tested_db_snapshot)
if select_result == swsscommon.Select.OBJECT:
mock_check_update_event.assert_called_once_with(tested_db_snapshot)
mock_check_vlan_update.assert_called_once_with(tested_db_snapshot)
mock_check_vlan_intf_update.assert_called_once_with(tested_db_snapshot)
else:
mock_check_dhcp_server_update.assert_not_called()
mock_check_update_event.assert_not_called()
mock_check_vlan_update.assert_not_called()
mock_check_vlan_intf_update.assert_not_called()
@pytest.mark.parametrize("dhcp_server_table_update", tested_subscribe_dhcp_server_table)
def test_dhcp_relayd_monitor_check_dhcp_server_update(mock_swsscommon_dbconnector_init, dhcp_server_table_update):
tested_table = dhcp_server_table_update["table"]
with patch.object(DhcpRelaydDbMonitor, "subscribe_dhcp_server_table",
return_value=MockSubscribeTable(tested_table),
new_callable=PropertyMock):
@pytest.mark.parametrize("tables", [["VlanTableEventChecker"], ["dummy"]])
def test_dhcp_relayd_enable_checker(tables, mock_swsscommon_dbconnector_init):
with patch.object(ConfigDbEventChecker, "enable") as mock_enable:
db_connector = DhcpDbConnector()
dhcp_relayd_db_monitor = DhcpRelaydDbMonitor(db_connector)
check_res = dhcp_relayd_db_monitor._check_dhcp_server_update(set(["Vlan1000"]))
assert check_res == dhcp_server_table_update["exp_res"]
dhcp_relayd_db_monitor = DhcpRelaydDbMonitor(db_connector, None, [VlanTableEventChecker(None, None)])
dhcp_relayd_db_monitor.enable_checker(set(tables))
if "VlanTableEventChecker" in tables:
mock_enable.assert_called_once()
else:
mock_enable.assert_not_called()
@pytest.mark.parametrize("vlan_table_update", tested_subscribe_vlan_table)
def test_dhcp_relayd_monitor_check_vlan_update(mock_swsscommon_dbconnector_init, vlan_table_update):
tested_table = vlan_table_update["table"]
with patch.object(DhcpRelaydDbMonitor, "subscribe_vlan_table", return_value=MockSubscribeTable(tested_table),
new_callable=PropertyMock):
@pytest.mark.parametrize("select_result", [swsscommon.Select.TIMEOUT, swsscommon.Select.OBJECT])
@pytest.mark.parametrize("is_checker_enabled", [True, False])
def test_dhcp_servd_monitor_check_db_update(mock_swsscommon_dbconnector_init, select_result,
is_checker_enabled):
with patch.object(DhcpServerTableCfgChangeEventChecker, "check_update_event") \
as mock_check_dhcp_server_update_event, \
patch.object(DhcpPortTableEventChecker, "check_update_event") as mock_check_dhcp_server_port_update, \
patch.object(DhcpRangeTableEventChecker, "check_update_event") as mock_check_dhcp_server_range_update, \
patch.object(DhcpOptionTableEventChecker, "check_update_event") as mock_check_dhcp_server_option_update, \
patch.object(VlanMemberTableEventChecker, "check_update_event") as mock_check_vlan_member_update, \
patch.object(VlanTableEventChecker, "check_update_event") as mock_check_vlan_update, \
patch.object(VlanIntfTableEventChecker, "check_update_event") as mock_check_vlan_intf_update, \
patch.object(ConfigDbEventChecker, "is_enabled", return_value=is_checker_enabled), \
patch.object(swsscommon.Select, "select", return_value=(select_result, None)), \
patch.object(ConfigDbEventChecker, "clear_event") as mock_clear:
db_connector = DhcpDbConnector()
dhcp_relayd_db_monitor = DhcpRelaydDbMonitor(db_connector)
check_res = dhcp_relayd_db_monitor._check_vlan_update(set(["Vlan1000"]))
assert check_res == vlan_table_update["exp_res"]
dhcp_checker = DhcpServerTableCfgChangeEventChecker(None, None)
dhcp_checker.enabled = True
vlan_checker = VlanIntfTableEventChecker(None, None)
db_monitor = DhcpServdDbMonitor(db_connector, swsscommon.Select(), [dhcp_checker, vlan_checker])
tested_db_snapshot = {"enabled_dhcp_interfaces": "dummy1", "used_range": "dummy2",
"used_options": "dummy3"}
db_monitor.check_db_update(tested_db_snapshot)
if select_result == swsscommon.Select.OBJECT and is_checker_enabled:
mock_check_dhcp_server_update_event.assert_called_once_with(tested_db_snapshot)
mock_check_vlan_update.assert_not_called()
mock_clear.assert_called_once_with()
else:
mock_check_dhcp_server_update_event.assert_not_called()
mock_check_dhcp_server_port_update.assert_not_called()
mock_check_dhcp_server_range_update.assert_not_called()
mock_check_vlan_member_update.assert_not_called()
mock_check_dhcp_server_option_update.assert_not_called()
mock_check_vlan_update.assert_not_called()
mock_check_vlan_intf_update.assert_not_called()
mock_clear.assert_not_called()
@pytest.mark.parametrize("vlan_intf_table_update", tested_subscribe_vlan_intf_table)
def test_dhcp_relayd_monitor_check_vlan_intf_update(mock_swsscommon_dbconnector_init, vlan_intf_table_update):
tested_table = vlan_intf_table_update["table"]
with patch.object(DhcpRelaydDbMonitor, "subscribe_vlan_intf_table", return_value=MockSubscribeTable(tested_table),
new_callable=PropertyMock):
@pytest.mark.parametrize("tables", [set(["VlanIntfTableEventChecker"]), set(["dummy1"])])
def test_dhcp_servd_monitor_enable_checkers(mock_swsscommon_dbconnector_init, tables):
with patch.object(ConfigDbEventChecker, "enable") as mock_enable:
db_connector = DhcpDbConnector()
dhcp_relayd_db_monitor = DhcpRelaydDbMonitor(db_connector)
check_res = dhcp_relayd_db_monitor._check_vlan_intf_update(set(["Vlan1000"]))
assert check_res == vlan_intf_table_update["exp_res"]
checker = VlanIntfTableEventChecker(None, None)
db_monitor = DhcpServdDbMonitor(db_connector, None, [checker])
db_monitor.enable_checkers(tables)
if tables == set(["VlanIntfTableEventChecker"]):
mock_enable.assert_called_once()
else:
mock_enable.assert_not_called()
def mock_subscriber_state_table(db, table_name):
return table_name
@pytest.mark.parametrize("tables", [set(PORT_MODE_CHECKER), set(["dummy"])])
def test_dhcp_servd_monitor_disble_checkers(mock_swsscommon_dbconnector_init, tables):
with patch.object(ConfigDbEventChecker, "disable") as mock_disable:
db_connector = DhcpDbConnector()
checker = DhcpPortTableEventChecker(None, None)
db_monitor = DhcpServdDbMonitor(db_connector, None, [checker])
db_monitor.disable_checkers(tables)
if tables == set(PORT_MODE_CHECKER):
mock_disable.assert_called_once_with()
else:
mock_disable.assert_not_called()
def test_db_event_checker_init(mock_swsscommon_dbconnector_init):
sel = swsscommon.Select()
db_event_checker = ConfigDbEventChecker(sel, MagicMock())
try:
db_event_checker._get_parameter(None)
except NotImplementedError:
pass
else:
pytest.fail("Run _get_parameter didn't get error")
try:
db_event_checker._process_check(None, None, None, None)
except NotImplementedError:
pass
else:
pytest.fail("Run _process_check didn't get error")
@pytest.mark.parametrize("tested_data", get_subscribe_table_tested_data("test_table_clear"))
def test_db_event_checker_clear_event(mock_swsscommon_dbconnector_init, tested_data):
with patch.object(ConfigDbEventChecker, "enable"), \
patch.object(ConfigDbEventChecker, "subscriber_state_table",
return_value=MockSubscribeTable(tested_data["table"]), new_callable=PropertyMock):
sel = swsscommon.Select()
db_event_checker = ConfigDbEventChecker(sel, MagicMock())
db_event_checker.enabled = True
assert db_event_checker.subscriber_state_table.hasData()
db_event_checker.clear_event()
assert not db_event_checker.subscriber_state_table.hasData()
@pytest.mark.parametrize("is_enabled", [True, False])
def test_db_event_checker_is_enabled(is_enabled):
sel = swsscommon.Select()
db_event_checker = ConfigDbEventChecker(sel, MagicMock())
db_event_checker.enabled = is_enabled
assert db_event_checker.is_enabled() == is_enabled
@pytest.mark.parametrize("param_name", ["param1", "param2"])
def test_db_event_checker_check_db_snapshot(mock_swsscommon_dbconnector_init, param_name):
sel = swsscommon.Select()
db_event_checker = ConfigDbEventChecker(sel, MagicMock())
tested_db_snapshot = {"param1": "value1"}
check_res = db_event_checker._check_db_snapshot(tested_db_snapshot, param_name)
assert check_res == (param_name in tested_db_snapshot)
@pytest.mark.parametrize("enabled", [True, False])
def test_db_event_checker_disable(mock_swsscommon_dbconnector_init, enabled):
with patch.object(swsscommon.Select, "removeSelectable") as mock_remove, \
patch.object(ConfigDbEventChecker, "enabled", return_value=enabled, new_callable=PropertyMock), \
patch.object(sys, "exit", side_effect=mock_exit_func) as mock_exit:
sel = swsscommon.Select()
db_event_checker = ConfigDbEventChecker(sel, MagicMock())
try:
db_event_checker.disable()
except SystemExit:
mock_remove.assert_not_called()
mock_exit.assert_called_once_with(1)
else:
mock_remove.assert_called_once_with(None)
mock_exit.assert_not_called()
@pytest.mark.parametrize("enabled", [True, False])
def test_db_event_checker_subscribe_table(mock_swsscommon_dbconnector_init, enabled):
with patch.object(ConfigDbEventChecker, "enabled", return_value=enabled, new_callable=PropertyMock), \
patch.object(sys, "exit", side_effect=mock_exit_func) as mock_exit, \
patch.object(swsscommon, "SubscriberStateTable") as mock_sub, \
patch.object(swsscommon.Select, "addSelectable") as mock_add_sel_tbl:
sel = swsscommon.Select()
db_event_checker = ConfigDbEventChecker(sel, MagicMock())
try:
db_event_checker.enable()
except SystemExit:
mock_exit.assert_called_once_with(1)
mock_add_sel_tbl.assert_not_called()
mock_sub.assert_not_called()
else:
mock_exit.assert_not_called()
mock_add_sel_tbl.assert_called_once_with(ANY)
mock_sub.assert_called_once_with(ANY, "")
@pytest.mark.parametrize("tested_db_snapshot", [{"enabled_dhcp_interfaces": "Vlan1000"}, {}])
@pytest.mark.parametrize("tested_data", get_subscribe_table_tested_data("test_dhcp_server_update"))
@pytest.mark.parametrize("enabled", [True, False])
def test_dhcp_server_table_cfg_change_checker(mock_swsscommon_dbconnector_init, tested_data, tested_db_snapshot,
enabled):
with patch.object(ConfigDbEventChecker, "enable"), \
patch.object(ConfigDbEventChecker, "subscriber_state_table",
return_value=MockSubscribeTable(tested_data["table"]), new_callable=PropertyMock), \
patch.object(ConfigDbEventChecker, "enabled", return_value=enabled, new_callable=PropertyMock), \
patch.object(sys, "exit"):
sel = swsscommon.Select()
db_event_checker = DhcpServerTableCfgChangeEventChecker(sel, MagicMock())
expected_res = tested_data["exp_res"] if isinstance(tested_data["exp_res"], bool) else \
tested_data["exp_res"]["cfg_change"]
check_res = db_event_checker.check_update_event(tested_db_snapshot)
if "enabled_dhcp_interfaces" not in tested_db_snapshot:
assert check_res
else:
assert expected_res == check_res
@pytest.mark.parametrize("tested_db_snapshot", [{"enabled_dhcp_interfaces": "Vlan1000"}, {}])
@pytest.mark.parametrize("tested_data", get_subscribe_table_tested_data("test_dhcp_server_update"))
@pytest.mark.parametrize("enabled", [True, False])
def test_dhcp_server_table_enablement_change_checker(mock_swsscommon_dbconnector_init, tested_data, tested_db_snapshot,
enabled):
with patch.object(ConfigDbEventChecker, "enable"), \
patch.object(ConfigDbEventChecker, "subscriber_state_table",
return_value=MockSubscribeTable(tested_data["table"]), new_callable=PropertyMock), \
patch.object(ConfigDbEventChecker, "enabled", return_value=enabled, new_callable=PropertyMock), \
patch.object(sys, "exit"):
sel = swsscommon.Select()
db_event_checker = DhcpServerTableIntfEnablementEventChecker(sel, MagicMock())
expected_res = tested_data["exp_res"] if isinstance(tested_data["exp_res"], bool) else \
tested_data["exp_res"]["enablement"]
check_res = db_event_checker.check_update_event(tested_db_snapshot)
if "enabled_dhcp_interfaces" not in tested_db_snapshot:
assert check_res
else:
assert expected_res == check_res
@pytest.mark.parametrize("tested_db_snapshot", [{"enabled_dhcp_interfaces": "Vlan1000"}, {}])
@pytest.mark.parametrize("tested_data", get_subscribe_table_tested_data("test_port_update"))
@pytest.mark.parametrize("enabled", [True, False])
def test_dhcp_port_table_checker(mock_swsscommon_dbconnector_init, tested_data, tested_db_snapshot, enabled):
with patch.object(ConfigDbEventChecker, "enable"), \
patch.object(ConfigDbEventChecker, "subscriber_state_table",
return_value=MockSubscribeTable(tested_data["table"]), new_callable=PropertyMock), \
patch.object(ConfigDbEventChecker, "enabled", return_value=enabled, new_callable=PropertyMock), \
patch.object(sys, "exit"):
sel = swsscommon.Select()
db_event_checker = DhcpPortTableEventChecker(sel, MagicMock())
expected_res = tested_data["exp_res"]
check_res = db_event_checker.check_update_event(tested_db_snapshot)
if "enabled_dhcp_interfaces" not in tested_db_snapshot:
assert check_res
else:
assert expected_res == check_res
@pytest.mark.parametrize("tested_db_snapshot", [{"used_range": "range1"}, {}])
@pytest.mark.parametrize("tested_data", get_subscribe_table_tested_data("test_range_update"))
@pytest.mark.parametrize("enabled", [True, False])
def test_dhcp_range_table_checker(mock_swsscommon_dbconnector_init, tested_data, tested_db_snapshot, enabled):
with patch.object(ConfigDbEventChecker, "enable"), \
patch.object(ConfigDbEventChecker, "subscriber_state_table",
return_value=MockSubscribeTable(tested_data["table"]), new_callable=PropertyMock), \
patch.object(ConfigDbEventChecker, "enabled", return_value=enabled, new_callable=PropertyMock), \
patch.object(sys, "exit"):
sel = swsscommon.Select()
db_event_checker = DhcpRangeTableEventChecker(sel, MagicMock())
expected_res = tested_data["exp_res"]
check_res = db_event_checker.check_update_event(tested_db_snapshot)
if "used_range" not in tested_db_snapshot:
assert check_res
else:
assert expected_res == check_res
@pytest.mark.parametrize("tested_db_snapshot", [{"used_options": "option223"}, {}])
@pytest.mark.parametrize("tested_data", get_subscribe_table_tested_data("test_option_update"))
@pytest.mark.parametrize("enabled", [True, False])
def test_dhcp_option_table_checker(mock_swsscommon_dbconnector_init, tested_data, tested_db_snapshot, enabled):
with patch.object(ConfigDbEventChecker, "enable"), \
patch.object(ConfigDbEventChecker, "subscriber_state_table",
return_value=MockSubscribeTable(tested_data["table"]), new_callable=PropertyMock), \
patch.object(ConfigDbEventChecker, "enabled", return_value=enabled, new_callable=PropertyMock), \
patch.object(sys, "exit"):
sel = swsscommon.Select()
db_event_checker = DhcpOptionTableEventChecker(sel, MagicMock())
expected_res = tested_data["exp_res"]
check_res = db_event_checker.check_update_event(tested_db_snapshot)
if "used_options" not in tested_db_snapshot:
assert check_res
else:
assert expected_res == check_res
@pytest.mark.parametrize("tested_db_snapshot", [{"enabled_dhcp_interfaces": "Vlan1000"}, {}])
@pytest.mark.parametrize("tested_data", get_subscribe_table_tested_data("test_vlan_update"))
@pytest.mark.parametrize("enabled", [True, False])
def test_vlan_table_checker(mock_swsscommon_dbconnector_init, tested_data, tested_db_snapshot, enabled):
with patch.object(ConfigDbEventChecker, "enable"), \
patch.object(ConfigDbEventChecker, "subscriber_state_table",
return_value=MockSubscribeTable(tested_data["table"]), new_callable=PropertyMock), \
patch.object(ConfigDbEventChecker, "enabled", return_value=enabled, new_callable=PropertyMock), \
patch.object(sys, "exit"):
sel = swsscommon.Select()
db_event_checker = VlanTableEventChecker(sel, MagicMock())
expected_res = tested_data["exp_res"]
check_res = db_event_checker.check_update_event(tested_db_snapshot)
if "enabled_dhcp_interfaces" not in tested_db_snapshot:
assert check_res
else:
assert expected_res == check_res
@pytest.mark.parametrize("tested_db_snapshot", [{"enabled_dhcp_interfaces": "Vlan1000"}, {}])
@pytest.mark.parametrize("tested_data", get_subscribe_table_tested_data("test_vlan_intf_update"))
@pytest.mark.parametrize("enabled", [True, False])
def test_vlan_intf_table_checker(mock_swsscommon_dbconnector_init, tested_data, tested_db_snapshot, enabled):
with patch.object(ConfigDbEventChecker, "enable"), \
patch.object(ConfigDbEventChecker, "subscriber_state_table",
return_value=MockSubscribeTable(tested_data["table"]), new_callable=PropertyMock), \
patch.object(ConfigDbEventChecker, "enabled", return_value=enabled, new_callable=PropertyMock), \
patch.object(sys, "exit"):
sel = swsscommon.Select()
db_event_checker = VlanIntfTableEventChecker(sel, MagicMock())
expected_res = tested_data["exp_res"]
check_res = db_event_checker.check_update_event(tested_db_snapshot)
if "enabled_dhcp_interfaces" not in tested_db_snapshot:
assert check_res
else:
assert expected_res == check_res
@pytest.mark.parametrize("tested_db_snapshot", [{"enabled_dhcp_interfaces": "Vlan1000"}, {}])
@pytest.mark.parametrize("tested_data", get_subscribe_table_tested_data("test_vlan_member_update"))
@pytest.mark.parametrize("enabled", [True, False])
def test_vlan_member_table_checker(mock_swsscommon_dbconnector_init, tested_data, tested_db_snapshot, enabled):
with patch.object(ConfigDbEventChecker, "enable"), \
patch.object(ConfigDbEventChecker, "subscriber_state_table",
return_value=MockSubscribeTable(tested_data["table"]), new_callable=PropertyMock), \
patch.object(ConfigDbEventChecker, "enabled", return_value=enabled, new_callable=PropertyMock), \
patch.object(sys, "exit"):
sel = swsscommon.Select()
db_event_checker = VlanMemberTableEventChecker(sel, MagicMock())
expected_res = tested_data["exp_res"]
check_res = db_event_checker.check_update_event(tested_db_snapshot)
if "enabled_dhcp_interfaces" not in tested_db_snapshot:
assert check_res
else:
assert expected_res == check_res

View File

@ -5,45 +5,46 @@ import sys
import time
from common_utils import mock_get_config_db_table, MockProc, MockPopen
from dhcp_server.common.utils import DhcpDbConnector
from dhcp_server.common.dhcp_db_monitor import ConfigDbEventChecker
from dhcp_server.dhcprelayd.dhcprelayd import DhcpRelayd, KILLED_OLD, NOT_KILLED, NOT_FOUND_PROC
from dhcp_server.common.dhcp_db_monitor import DhcpRelaydDbMonitor
from swsscommon import swsscommon
from unittest.mock import patch, call
def test_start(mock_swsscommon_dbconnector_init):
with patch.object(DhcpRelayd, "refresh_dhcrelay", return_value=None) as mock_refresh, \
patch.object(DhcpRelaydDbMonitor, "subscribe_table", return_value=None) as mock_subscribe:
patch.object(ConfigDbEventChecker, "enable"):
dhcp_db_connector = DhcpDbConnector()
dhcprelayd = DhcpRelayd(dhcp_db_connector)
dhcprelayd = DhcpRelayd(dhcp_db_connector, None)
dhcprelayd.start()
mock_refresh.assert_called_once_with()
mock_subscribe.assert_called_once_with()
def test_refresh_dhcrelay(mock_swsscommon_dbconnector_init):
with patch.object(DhcpRelayd, "_get_dhcp_server_ip", return_value="240.127.1.2"), \
patch.object(DhcpDbConnector, "get_config_db_table", side_effect=mock_get_config_db_table), \
patch.object(DhcpRelayd, "_start_dhcrelay_process", return_value=None), \
patch.object(DhcpRelayd, "_start_dhcpmon_process", return_value=None):
patch.object(DhcpRelayd, "_start_dhcpmon_process", return_value=None), \
patch.object(ConfigDbEventChecker, "enable"):
dhcp_db_connector = DhcpDbConnector()
dhcprelayd = DhcpRelayd(dhcp_db_connector)
dhcprelayd = DhcpRelayd(dhcp_db_connector, None)
dhcprelayd.refresh_dhcrelay()
@pytest.mark.parametrize("new_dhcp_interfaces", [[], ["Vlan1000"], ["Vlan1000", "Vlan2000"]])
@pytest.mark.parametrize("kill_res", [KILLED_OLD, NOT_KILLED, NOT_FOUND_PROC])
@pytest.mark.parametrize("proc_status", [psutil.STATUS_ZOMBIE, psutil.STATUS_RUNNING])
def test_start_dhcrelay_process(mock_swsscommon_dbconnector_init, new_dhcp_interfaces, kill_res, proc_status):
def test_start_dhcrelay_process(mock_swsscommon_dbconnector_init, new_dhcp_interfaces, kill_res, proc_status,):
with patch.object(DhcpRelayd, "_kill_exist_relay_releated_process", return_value=kill_res), \
patch.object(subprocess, "Popen", return_value=MockPopen(999)) as mock_popen, \
patch.object(time, "sleep"), \
patch("dhcp_server.dhcprelayd.dhcprelayd.terminate_proc", return_value=None) as mock_terminate, \
patch.object(psutil.Process, "__init__", return_value=None), \
patch.object(psutil.Process, "status", return_value=proc_status), \
patch.object(sys, "exit") as mock_exit:
patch.object(sys, "exit") as mock_exit, \
patch.object(ConfigDbEventChecker, "enable"):
dhcp_db_connector = DhcpDbConnector()
dhcprelayd = DhcpRelayd(dhcp_db_connector)
dhcprelayd = DhcpRelayd(dhcp_db_connector, None)
dhcprelayd._start_dhcrelay_process(new_dhcp_interfaces, "240.127.1.2", False)
if len(new_dhcp_interfaces) == 0 or kill_res == NOT_KILLED:
mock_popen.assert_not_called()
@ -72,9 +73,10 @@ def test_start_dhcpmon_process(mock_swsscommon_dbconnector_init, new_dhcp_interf
patch.object(time, "sleep"), \
patch("dhcp_server.dhcprelayd.dhcprelayd.terminate_proc", return_value=None) as mock_terminate, \
patch.object(psutil.Process, "__init__", return_value=None), \
patch.object(psutil.Process, "status", return_value=proc_status):
patch.object(psutil.Process, "status", return_value=proc_status), \
patch.object(ConfigDbEventChecker, "enable"):
dhcp_db_connector = DhcpDbConnector()
dhcprelayd = DhcpRelayd(dhcp_db_connector)
dhcprelayd = DhcpRelayd(dhcp_db_connector, None)
dhcprelayd._start_dhcpmon_process(new_dhcp_interfaces, False)
if len(new_dhcp_interfaces) == 0 or kill_res == NOT_KILLED:
mock_popen.assert_not_called()
@ -100,9 +102,10 @@ def test_kill_exist_relay_releated_process(mock_swsscommon_dbconnector_init, new
process_iter_ret = []
for running_proc in running_procs:
process_iter_ret.append(MockProc(running_proc))
with patch.object(psutil, "process_iter", return_value=process_iter_ret):
with patch.object(psutil, "process_iter", return_value=process_iter_ret), \
patch.object(ConfigDbEventChecker, "enable"):
dhcp_db_connector = DhcpDbConnector()
dhcprelayd = DhcpRelayd(dhcp_db_connector)
dhcprelayd = DhcpRelayd(dhcp_db_connector, None)
res = dhcprelayd._kill_exist_relay_releated_process(new_dhcp_interfaces, process_name, force_kill)
if force_kill and process_name in running_procs:
assert res == KILLED_OLD
@ -118,9 +121,10 @@ def test_kill_exist_relay_releated_process(mock_swsscommon_dbconnector_init, new
def test_get_dhcp_server_ip(mock_swsscommon_dbconnector_init, mock_swsscommon_table_init, get_res):
with patch.object(swsscommon.Table, "hget", return_value=get_res), \
patch.object(time, "sleep") as mock_sleep, \
patch.object(sys, "exit") as mock_exit:
patch.object(sys, "exit") as mock_exit, \
patch.object(ConfigDbEventChecker, "enable"):
dhcp_db_connector = DhcpDbConnector()
dhcprelayd = DhcpRelayd(dhcp_db_connector)
dhcprelayd = DhcpRelayd(dhcp_db_connector, None)
ret = dhcprelayd._get_dhcp_server_ip()
if get_res[0] == 1:
assert ret == get_res[1]

View File

@ -5,28 +5,48 @@ import sys
import time
from common_utils import MockProc
from dhcp_server.common.utils import DhcpDbConnector
from dhcp_server.common.dhcp_db_monitor import DhcpServdDbMonitor
from dhcp_server.dhcpservd.dhcp_cfggen import DhcpServCfgGenerator
from dhcp_server.dhcpservd.dhcpservd import DhcpServd
from swsscommon import swsscommon
from unittest.mock import patch, call, MagicMock
from unittest.mock import patch, call, MagicMock, PropertyMock
AF_INET = 2
AF_INET6 = 10
PORT_MODE_CHECKER = ["DhcpServerTableCfgChangeEventChecker", "DhcpPortTableEventChecker", "DhcpRangeTableEventChecker",
"DhcpOptionTableEventChecker", "VlanTableEventChecker", "VlanIntfTableEventChecker",
"VlanMemberTableEventChecker"]
def test_dump_dhcp4_config(mock_swsscommon_dbconnector_init):
with patch("dhcp_server.dhcpservd.dhcp_cfggen.DhcpServCfgGenerator.generate", return_value="dummy_config") as mock_generate, \
patch("dhcp_server.dhcpservd.dhcpservd.DhcpServd._notify_kea_dhcp4_proc", MagicMock()) as mock_notify_kea_dhcp4_proc:
@pytest.mark.parametrize("enabled_checker", [None, set(PORT_MODE_CHECKER)])
def test_dump_dhcp4_config(mock_swsscommon_dbconnector_init, enabled_checker):
new_enabled_checker = set(["VlanTableEventChecker"])
with patch("dhcp_server.dhcpservd.dhcp_cfggen.DhcpServCfgGenerator.generate",
return_value=("dummy_config", set(), set(), set(), new_enabled_checker)) as mock_generate, \
patch("dhcp_server.dhcpservd.dhcpservd.DhcpServd._notify_kea_dhcp4_proc",
MagicMock()) as mock_notify_kea_dhcp4_proc, \
patch.object(DhcpServd, "dhcp_servd_monitor", return_value=DhcpServdDbMonitor,
new_callable=PropertyMock), \
patch.object(DhcpServdDbMonitor, "disable_checkers") as mock_unsubscribe, \
patch.object(DhcpServdDbMonitor, "enable_checkers") as mock_subscribe, \
patch.object(DhcpServd, "enabled_checker", return_value=enabled_checker, new_callable=PropertyMock):
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 = DhcpServd(dhcp_cfg_generator, dhcp_db_connector, None,
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()
if enabled_checker is None:
mock_subscribe.assert_not_called()
mock_unsubscribe.assert_not_called()
else:
mock_unsubscribe.assert_called_once_with(enabled_checker - new_enabled_checker)
mock_subscribe.assert_called_once_with(new_enabled_checker - enabled_checker)
@pytest.mark.parametrize("process_list", [["proc1", "proc2", "kea-dhcp4"], ["proc1", "proc2"]])
@ -37,7 +57,7 @@ def test_notify_kea_dhcp4_proc(process_list, mock_swsscommon_dbconnector_init, m
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 = DhcpServd(dhcp_cfg_generator, dhcp_db_connector, None)
dhcpservd._notify_kea_dhcp4_proc()
if "kea-dhcp4" in process_list:
mock_send_signal.assert_has_calls([
@ -62,7 +82,7 @@ def test_update_dhcp_server_ip(mock_swsscommon_dbconnector_init, mock_parse_port
patch.object(sys, "exit") as mock_exit:
dhcp_db_connector = DhcpDbConnector()
dhcp_cfg_generator = DhcpServCfgGenerator(dhcp_db_connector)
dhcpservd = DhcpServd(dhcp_cfg_generator, dhcp_db_connector)
dhcpservd = DhcpServd(dhcp_cfg_generator, dhcp_db_connector, None)
dhcpservd._update_dhcp_server_ip()
if mock_intf:
mock_hset.assert_has_calls([
@ -76,10 +96,11 @@ def test_update_dhcp_server_ip(mock_swsscommon_dbconnector_init, mock_parse_port
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, \
patch.object(DhcpServd, "_update_dhcp_server_ip") as mock_update_dhcp_server_ip:
patch.object(DhcpServd, "_update_dhcp_server_ip") as mock_update_dhcp_server_ip, \
patch.object(DhcpServdDbMonitor, "enable_checkers"):
dhcp_db_connector = DhcpDbConnector()
dhcp_cfg_generator = DhcpServCfgGenerator(dhcp_db_connector)
dhcpservd = DhcpServd(dhcp_cfg_generator, dhcp_db_connector)
dhcpservd = DhcpServd(dhcp_cfg_generator, dhcp_db_connector, MagicMock())
dhcpservd.start()
mock_dump.assert_called_once_with()
mock_update_dhcp_server_ip.assert_called_once_with()

View File

@ -26,6 +26,37 @@ interval_test_data = {
"expected_res": [["192.168.0.10", "192.168.0.10"]]
}
}
validate_str_type_data = [
# type, value, expected_res
["string", 123, False],
["string", "123", True],
["binary", "01020304ef", True],
# False because we only support octet-based binary
["binary", "01020304e", False],
["binary", "0102ab0304ef", True],
["binary", "we", False],
["boolean", "true", True],
["boolean", "false", True],
["boolean", "0", False],
["boolean", "1", False],
["boolean", True, False],
["boolean", "213", False],
["ipv4-address", "192.168.0.1", True],
["ipv4-address", "300.168.0.1", False],
["ipv4-address", 123, False],
["ipv4-address", "123", False],
["ipv4-address", "192.168.0.1/24", False],
["uint8", "e123", False],
["uint8", 123, False],
["uint8", "300", False],
["uint8", "128", True],
["uint16", "1000", True],
["uint16", "65536", False],
["uint32", "4294967296", False],
["uint32", "65536", True],
# False because we don't support uint64
["uint64", "65536", False]
]
def test_construct_without_sock(mock_swsscommon_dbconnector_init):
@ -47,18 +78,38 @@ def test_construct_sock(mock_swsscommon_dbconnector_init):
])
def test_get_config_db_table(mock_swsscommon_dbconnector_init, mock_swsscommon_table_init):
def test_get_db_table(mock_swsscommon_dbconnector_init, mock_swsscommon_table_init):
dhcp_db_connector = utils.DhcpDbConnector()
with patch.object(swsscommon.Table, "getKeys", return_value=["key1", "key2"]) as mock_get_keys, \
patch.object(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")
mock_get_keys.assert_called_once_with()
assert ret == {
"key1": {"list": ["1", "2"], "value": "3,4"},
"key2": {"list": ["1", "2"], "value": "3,4"}
}
ret = dhcp_db_connector.get_state_db_table("VLAN")
mock_swsscommon_table_init.assert_has_calls([
call(dhcp_db_connector.config_db, "VLAN"),
call(dhcp_db_connector.state_db, "VLAN")
])
mock_get_keys.assert_has_calls([
call(),
call()
])
assert ret == {
"key1": {"list": ["1", "2"], "value": "3,4"},
"key2": {"list": ["1", "2"], "value": "3,4"}
}
def test_get_entry(mock_swsscommon_dbconnector_init, mock_swsscommon_table_init):
tested_entry = {"key": "value"}
dhcp_db_connector = utils.DhcpDbConnector()
with patch.object(swsscommon.Table, "get", return_value=(None, tested_entry)) as mock_get:
res = utils.get_entry(swsscommon.Table(dhcp_db_connector.config_db, "VLAN"), "dummy_entry")
assert res == tested_entry
mock_get.assert_called_once_with("dummy_entry")
@pytest.mark.parametrize("test_type", interval_test_data.keys())
@ -80,3 +131,9 @@ def convert_ip_address_intervals(intervals):
for interval in intervals:
ret.append([ipaddress.ip_address(interval[0]), ipaddress.ip_address(interval[1])])
return ret
@pytest.mark.parametrize("test_data", validate_str_type_data)
def test_validate_ttr_type(test_data):
res = utils.validate_str_type(test_data[0], test_data[1])
assert res == test_data[2]