diff --git a/dockers/docker-dhcp-server/kea-dhcp4.conf.j2 b/dockers/docker-dhcp-server/kea-dhcp4.conf.j2 index 27fd1a1d1f9..d1e457764e0 100644 --- a/dockers/docker-dhcp-server/kea-dhcp4.conf.j2 +++ b/dockers/docker-dhcp-server/kea-dhcp4.conf.j2 @@ -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"] }}" diff --git a/src/sonic-dhcp-server/dhcp_server/common/dhcp_db_monitor.py b/src/sonic-dhcp-server/dhcp_server/common/dhcp_db_monitor.py index bbee63e1ec9..799acabbcc4 100644 --- a/src/sonic-dhcp-server/dhcp_server/common/dhcp_db_monitor.py +++ b/src/sonic-dhcp-server/dhcp_server/common/dhcp_db_monitor.py @@ -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): - self.db_connector = db_connector - self.sel = swsscommon.Select() - self.select_timeout = 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 subscribe_table(self): + def _get_parameter(self, db_snapshot): """ - Subcribe db table to monitor + Get paramter depends on subclass + Args: + db_snapshot: dict of db snapshot """ raise NotImplementedError @abstractmethod - def _do_check(self): + def _process_check(self, key, op, entry, parameter): """ - Check whether interested table content changed + 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_db_update(self, check_param): + 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 = sel + self.select_timeout = select_timeout + self.checker_dict = {} + for checker in checkers: + self.checker_dict[checker.get_class_name()] = checker + + def enable_checker(self, checker_names): + """ + Enable checkers + Args: + checker_names: set of tables checker to be enable + """ + for table in checker_names: + if table not in self.checker_dict: + syslog.syslog(syslog.LOG_ERR, "Cannot find checker for {} in checker_dict".format(table)) + continue + self.checker_dict[table].enable() + + 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 - 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 + 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: - enabled_dhcp_interfaces: DHCP interface that enabled dhcp_server - Returns: - Whether need to refresh + checker_names: set contains name of tables need to be disable """ - 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 + 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 _check_vlan_update(self, enabled_dhcp_interfaces): + def enable_checkers(self, checker_names): """ - Check vlan table + Enable checkers Args: - enabled_dhcp_interfaces: DHCP interface that enabled dhcp_server - Returns: - Whether need to refresh + checker_names: set contains name of tables need to be enable """ - 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 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 - need_refresh = True - return need_refresh + self.checker_dict[table].enable() - def _check_vlan_intf_update(self, enabled_dhcp_interfaces): + def check_db_update(self, db_snapshot): """ - Check vlan_interface table + Fetch db and check update Args: - enabled_dhcp_interfaces: DHCP interface that enabled dhcp_server + db_snapshot: dict contains db snapshot parameter Returns: - Whether need to refresh + 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 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: + for checker in self.checker_dict.values(): + if not checker.is_enabled(): continue - need_refresh = True + if need_refresh: + checker.clear_event() + else: + need_refresh |= checker.check_update_event(db_snapshot) return need_refresh diff --git a/src/sonic-dhcp-server/dhcp_server/common/utils.py b/src/sonic-dhcp-server/dhcp_server/common/utils.py index a07913d6c76..1b3268683c2 100644 --- a/src/sonic-dhcp-server/dhcp_server/common/utils.py +++ b/src/sonic-dhcp-server/dhcp_server/common/utils.py @@ -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(): diff --git a/src/sonic-dhcp-server/dhcp_server/dhcprelayd/dhcprelayd.py b/src/sonic-dhcp-server/dhcp_server/dhcprelayd/dhcprelayd.py index cf5a6de5e0b..fcf32fd7163 100644 --- a/src/sonic-dhcp-server/dhcp_server/dhcprelayd/dhcprelayd.py +++ b/src/sonic-dhcp-server/dhcp_server/dhcprelayd/dhcprelayd.py @@ -8,27 +8,25 @@ 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 @@ def __init__(self, db_connector, select_timeout=DEFAULT_SELECT_TIMEOUT): """ 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 @@ def wait(self): """ 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 @@ def _get_dhcp_server_ip(self): 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() diff --git a/src/sonic-dhcp-server/dhcp_server/dhcpservd/dhcp_cfggen.py b/src/sonic-dhcp-server/dhcp_server/dhcpservd/dhcp_cfggen.py index 0708f93c86a..ae2dd2e4670 100755 --- a/src/sonic-dhcp-server/dhcp_server/dhcpservd/dhcp_cfggen.py +++ b/src/sonic-dhcp-server/dhcp_server/dhcpservd/dhcp_cfggen.py @@ -5,7 +5,7 @@ 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_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 @@ def __init__(self, dhcp_db_connector, lease_path=DEFAULT_LEASE_PATH, port_map_pa 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 @@ def _parse_port_map_alias(self, port_map_path): 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 @@ def _construct_obj_for_template(self, dhcp_server_ipv4, port_ips, hostname): "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 @@ def _parse_port(self, port_ipv4, vlan_interfaces, vlan_members, ranges): 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 @@ def _parse_port(self, port_ipv4, vlan_interfaces, vlan_members, ranges): 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 @@ def _parse_port(self, port_ipv4, vlan_interfaces, vlan_members, ranges): 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]] = [] diff --git a/src/sonic-dhcp-server/dhcp_server/dhcpservd/dhcp_option.csv b/src/sonic-dhcp-server/dhcp_server/dhcpservd/dhcp_option.csv new file mode 100644 index 00000000000..c07ea6693b3 --- /dev/null +++ b/src/sonic-dhcp-server/dhcp_server/dhcpservd/dhcp_option.csv @@ -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 diff --git a/src/sonic-dhcp-server/dhcp_server/dhcpservd/dhcpservd.py b/src/sonic-dhcp-server/dhcp_server/dhcpservd/dhcpservd.py index 17266b66a86..997628276d5 100644 --- a/src/sonic-dhcp-server/dhcp_server/dhcpservd/dhcpservd.py +++ b/src/sonic-dhcp-server/dhcp_server/dhcpservd/dhcpservd.py @@ -7,6 +7,10 @@ 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 @@ 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 @@ def dump_dhcp4_config(self): """ 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 @@ def _update_dhcp_server_ip(self): 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() diff --git a/src/sonic-dhcp-server/setup.py b/src/sonic-dhcp-server/setup.py index 1dc57a70ff4..caf69e920d6 100644 --- a/src/sonic-dhcp-server/setup.py +++ b/src/sonic-dhcp-server/setup.py @@ -41,5 +41,8 @@ "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8" - ] + ], + package_data={ + "dhcp_server.dhcpservd": ["dhcp_option.csv"] + } ) diff --git a/src/sonic-dhcp-server/tests/common_utils.py b/src/sonic-dhcp-server/tests/common_utils.py index 35e09f7fec5..1b3323c7e12 100644 --- a/src/sonic-dhcp-server/tests/common_utils.py +++ b/src/sonic-dhcp-server/tests/common_utils.py @@ -3,7 +3,17 @@ 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 @@ def select(self, timeout): 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 @@ def status(self): 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 diff --git a/src/sonic-dhcp-server/tests/conftest.py b/src/sonic-dhcp-server/tests/conftest.py index 046c1f164c1..9661a0be43c 100644 --- a/src/sonic-dhcp-server/tests/conftest.py +++ b/src/sonic-dhcp-server/tests/conftest.py @@ -5,11 +5,13 @@ 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 - diff --git a/src/sonic-dhcp-server/tests/kea-dhcp4.conf.j2 b/src/sonic-dhcp-server/tests/kea-dhcp4.conf.j2 deleted file mode 100644 index 27fd1a1d1f9..00000000000 --- a/src/sonic-dhcp-server/tests/kea-dhcp4.conf.j2 +++ /dev/null @@ -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 %} - } -} diff --git a/src/sonic-dhcp-server/tests/test_data/dhcp_db_monitor_test_data.json b/src/sonic-dhcp-server/tests/test_data/dhcp_db_monitor_test_data.json new file mode 100644 index 00000000000..cbc132ecd70 --- /dev/null +++ b/src/sonic-dhcp-server/tests/test_data/dhcp_db_monitor_test_data.json @@ -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 + } + ] +} \ No newline at end of file diff --git a/src/sonic-dhcp-server/tests/test_data/kea-dhcp4.conf.j2 b/src/sonic-dhcp-server/tests/test_data/kea-dhcp4.conf.j2 index 27fd1a1d1f9..d1e457764e0 100644 --- a/src/sonic-dhcp-server/tests/test_data/kea-dhcp4.conf.j2 +++ b/src/sonic-dhcp-server/tests/test_data/kea-dhcp4.conf.j2 @@ -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"] }}" diff --git a/src/sonic-dhcp-server/tests/test_data/mock_config_db.json b/src/sonic-dhcp-server/tests/test_data/mock_config_db.json index 52e30d403b9..004ae6131a4 100644 --- a/src/sonic-dhcp-server/tests/test_data/mock_config_db.json +++ b/src/sonic-dhcp-server/tests/test_data/mock_config_db.json @@ -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": [ diff --git a/src/sonic-dhcp-server/tests/test_dhcp_cfggen.py b/src/sonic-dhcp-server/tests/test_dhcp_cfggen.py index 410b809489d..b3ec371a96b 100644 --- a/src/sonic-dhcp-server/tests/test_dhcp_cfggen.py +++ b/src/sonic-dhcp-server/tests/test_dhcp_cfggen.py @@ -2,12 +2,20 @@ 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 @@ } } 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 == {} diff --git a/src/sonic-dhcp-server/tests/test_dhcp_db_monitor.py b/src/sonic-dhcp-server/tests/test_dhcp_db_monitor.py index 2f2eed66a3d..d6830b274e7 100644 --- a/src/sonic-dhcp-server/tests/test_dhcp_db_monitor.py +++ b/src/sonic-dhcp-server/tests/test_dhcp_db_monitor.py @@ -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("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_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_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("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_update(set(["Vlan1000"])) - assert check_res == vlan_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() -@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(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() - 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 = 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 -def mock_subscriber_state_table(db, table_name): - return table_name +@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 diff --git a/src/sonic-dhcp-server/tests/test_dhcprelayd.py b/src/sonic-dhcp-server/tests/test_dhcprelayd.py index d7f4f5f85c8..7681f5d3a39 100644 --- a/src/sonic-dhcp-server/tests/test_dhcprelayd.py +++ b/src/sonic-dhcp-server/tests/test_dhcprelayd.py @@ -5,45 +5,46 @@ 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] diff --git a/src/sonic-dhcp-server/tests/test_dhcpservd.py b/src/sonic-dhcp-server/tests/test_dhcpservd.py index 558d8cb879c..0eb74ca3011 100644 --- a/src/sonic-dhcp-server/tests/test_dhcpservd.py +++ b/src/sonic-dhcp-server/tests/test_dhcpservd.py @@ -5,28 +5,48 @@ 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() diff --git a/src/sonic-dhcp-server/tests/test_utils.py b/src/sonic-dhcp-server/tests/test_utils.py index 6acc39115be..a8f4feb36a4 100644 --- a/src/sonic-dhcp-server/tests/test_utils.py +++ b/src/sonic-dhcp-server/tests/test_utils.py @@ -26,6 +26,37 @@ "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,20 +78,40 @@ 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()) def test_merge_intervals(test_type): intervals = convert_ip_address_intervals(interval_test_data[test_type]["intervals"]) @@ -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]