diff --git a/src/sonic-dhcp-utilities/dhcp_utilities/common/utils.py b/src/sonic-dhcp-utilities/dhcp_utilities/common/utils.py index c92936bfc1bd..f6975c452cdd 100644 --- a/src/sonic-dhcp-utilities/dhcp_utilities/common/utils.py +++ b/src/sonic-dhcp-utilities/dhcp_utilities/common/utils.py @@ -163,3 +163,14 @@ def get_target_process_cmds(process_name): if proc.name() == process_name: res.append(proc.cmdline()) return res + + +def is_smart_switch(device_metadata): + """ + Check in device metadata whether subtype is smartswitch + Args: + device_metadata: DEVICE_METADATA table + Returns: + If subtype is "SmartSwitch", return True. Else, return False + """ + return device_metadata.get("localhost", {}).get("subtype", "") == "SmartSwitch" diff --git a/src/sonic-dhcp-utilities/dhcp_utilities/dhcpservd/dhcp_cfggen.py b/src/sonic-dhcp-utilities/dhcp_utilities/dhcpservd/dhcp_cfggen.py index 136ff01c8875..dc69281c3ba9 100755 --- a/src/sonic-dhcp-utilities/dhcp_utilities/dhcpservd/dhcp_cfggen.py +++ b/src/sonic-dhcp-utilities/dhcp_utilities/dhcpservd/dhcp_cfggen.py @@ -5,7 +5,7 @@ import syslog from jinja2 import Environment, FileSystemLoader -from dhcp_utilities.common.utils import merge_intervals, validate_str_type +from dhcp_utilities.common.utils import merge_intervals, validate_str_type, is_smart_switch PORT_MAP_PATH = "/tmp/port-name-alias-map.txt" UNICODE_TYPE = str @@ -15,9 +15,12 @@ DHCP_SERVER_IPV4_PORT = "DHCP_SERVER_IPV4_PORT" VLAN_INTERFACE = "VLAN_INTERFACE" VLAN_MEMBER = "VLAN_MEMBER" +DPUS = "DPUS" +MID_PLANE_BRIDGE = "MID_PLANE_BRIDGE" PORT_MODE_CHECKER = ["DhcpServerTableCfgChangeEventChecker", "DhcpPortTableEventChecker", "DhcpRangeTableEventChecker", "DhcpOptionTableEventChecker", "VlanTableEventChecker", "VlanIntfTableEventChecker", "VlanMemberTableEventChecker"] +SMART_SWITCH_CHECKER = ["DpusTableEventChecker", "MidPlaneTableEventChecker"] LEASE_UPDATE_SCRIPT_PATH = "/etc/kea/lease_update.sh" DEFAULT_LEASE_TIME = 900 DEFAULT_LEASE_PATH = "/tmp/kea-lease.csv" @@ -58,21 +61,62 @@ def generate(self): # Get host name device_metadata = self.db_connector.get_config_db_table("DEVICE_METADATA") hostname = self._parse_hostname(device_metadata) + smart_switch = is_smart_switch(device_metadata) # Get ip information of vlan vlan_interface = self.db_connector.get_config_db_table(VLAN_INTERFACE) vlan_member_table = self.db_connector.get_config_db_table(VLAN_MEMBER) vlan_interfaces, vlan_members = self._parse_vlan(vlan_interface, vlan_member_table) + + # Parse dpu + dpus_table = self.db_connector.get_config_db_table(DPUS) + mid_plane_table = self.db_connector.get_config_db_table(MID_PLANE_BRIDGE) + mid_plane, dpus = self._parse_dpu(dpus_table, mid_plane_table) if smart_switch else {}, {} + 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) # Parse port table - port_ips, used_ranges = self._parse_port(port_ipv4, vlan_interfaces, vlan_members, ranges) + dhcp_interfaces = vlan_interfaces + if smart_switch and "bridge" in mid_plane and "ip_prefix" in mid_plane: + mid_plane_name = mid_plane["bridge"] + dhcp_interfaces[mid_plane_name] = [{ + "network": ipaddress.ip_network(mid_plane["ip_prefix"], strict=False), + "ip": mid_plane["ip_prefix"] + }] + dpus = ["{}|{}".format(mid_plane_name, dpu) for dpu in dpus] + dhcp_members = vlan_members | set(dpus) + port_ips, used_ranges = self._parse_port(port_ipv4, dhcp_interfaces, dhcp_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) + + if smart_switch: + subscribe_table |= set(SMART_SWITCH_CHECKER) + return self._render_config(render_obj), used_ranges, enabled_dhcp_interfaces, used_options, subscribe_table + def _parse_dpu(self, dpus_table, mid_plane_table): + """ + Parse dpu related tables + Args: + dpus_table: DPU table dict + mid_plane_table: mid_plane table dict + Returns: + Parsed obj, sample: + mid_plane = { + "bridge": "bridge_midplane", + "address": "169.254.200.254/24" + } + dpus = { + "dpu0" + } + """ + mid_plane = mid_plane_table.get("GLOBAL", {}) + dpus = set([dpu_value["midplane_interface"] for dpu_value in dpus_table.values() + if "midplane_interface" in dpu_value]) + return mid_plane, dpus + def _parse_customized_options(self, customized_options_ipv4): customized_options = {} for option_name, config in customized_options_ipv4.items(): @@ -107,7 +151,7 @@ def _render_config(self, render_obj): def _parse_vlan(self, vlan_interface, vlan_member): vlan_interfaces = self._get_vlan_ipv4_interface(vlan_interface.keys()) - vlan_members = vlan_member.keys() + vlan_members = set(vlan_member.keys()) return vlan_interfaces, vlan_members def _parse_hostname(self, device_metadata): @@ -306,19 +350,19 @@ def _match_range_network(self, dhcp_interface, dhcp_interface_name, port, range, port_ips[dhcp_interface_name][dhcp_interface_ip_str][port].append([range[0], range[1]]) break - def _parse_port(self, port_ipv4, vlan_interfaces, vlan_members, ranges): + def _parse_port(self, port_ipv4, dhcp_interfaces, dhcp_members, ranges): """ Parse content in DHCP_SERVER_IPV4_PORT table to below format, which indicate ip ranges assign to interface. Args: port_ipv4: Table object. - vlan_interfaces: Vlan information, sample: + dhcp_interfaces: DHCP interfaces information, sample: { 'Vlan1000': [{ 'network': IPv4Network('192.168.0.0/24'), 'ip': '192.168.0.1/24' }] } - vlan_members: List of vlan members + dhcp_members: List of DHCP members ranges: Dict of ranges Returns: Dict of dhcp conf, sample: @@ -349,23 +393,21 @@ def _parse_port(self, port_ipv4, vlan_interfaces, vlan_members, ranges): continue splits = port_key.split("|") # Skip port not in correct vlan - if port_key not in vlan_members: + if port_key not in dhcp_members: syslog.syslog(syslog.LOG_WARNING, f"Port {splits[1]} is not in {splits[0]}") continue # Get dhcp interface name like Vlan1000 dhcp_interface_name = splits[0] - # Get dhcp member interface name like etp1 - if splits[1] not in self.port_alias_map: - syslog.syslog(syslog.LOG_WARNING, f"Cannot find {splits[1]} in port_alias_map") - continue - port = self.port_alias_map[splits[1]] - if dhcp_interface_name not in vlan_interfaces: + # Get dhcp member interface name like etp1, be consistent with dhcp_relay, if alias doesn't exist, + # use port name directly + port = self.port_alias_map[splits[1]] if splits[1] in self.port_alias_map else splits[1] + if dhcp_interface_name not in dhcp_interfaces: syslog.syslog(syslog.LOG_WARNING, f"Interface {dhcp_interface_name} doesn't have IPv4 address") continue if dhcp_interface_name not in port_ips: port_ips[dhcp_interface_name] = {} # Get ip information of Vlan - dhcp_interface = vlan_interfaces[dhcp_interface_name] + dhcp_interface = dhcp_interfaces[dhcp_interface_name] for dhcp_interface_ip in dhcp_interface: ip_ports[str(dhcp_interface_ip["network"])] = dhcp_interface_name diff --git a/src/sonic-dhcp-utilities/dhcp_utilities/dhcpservd/dhcpservd.py b/src/sonic-dhcp-utilities/dhcp_utilities/dhcpservd/dhcpservd.py index 8911a3341637..b95e95417398 100644 --- a/src/sonic-dhcp-utilities/dhcp_utilities/dhcpservd/dhcpservd.py +++ b/src/sonic-dhcp-utilities/dhcp_utilities/dhcpservd/dhcpservd.py @@ -9,7 +9,7 @@ from dhcp_utilities.common.utils import DhcpDbConnector from dhcp_utilities.common.dhcp_db_monitor import DhcpServdDbMonitor, DhcpServerTableCfgChangeEventChecker, \ DhcpOptionTableEventChecker, DhcpRangeTableEventChecker, DhcpPortTableEventChecker, VlanIntfTableEventChecker, \ - VlanMemberTableEventChecker, VlanTableEventChecker + VlanMemberTableEventChecker, VlanTableEventChecker, MidPlaneTableEventChecker, DpusTableEventChecker from swsscommon import swsscommon KEA_DHCP4_CONFIG = "/etc/kea/kea-dhcp4.conf" @@ -110,6 +110,8 @@ def main(): 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)) + checkers.append(DpusTableEventChecker(sel, dhcp_db_connector.config_db)) + checkers.append(MidPlaneTableEventChecker(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() diff --git a/src/sonic-dhcp-utilities/tests/common_utils.py b/src/sonic-dhcp-utilities/tests/common_utils.py index bf88117293b8..6b7d0acd047c 100644 --- a/src/sonic-dhcp-utilities/tests/common_utils.py +++ b/src/sonic-dhcp-utilities/tests/common_utils.py @@ -14,6 +14,7 @@ PORT_MODE_CHECKER = ["DhcpServerTableCfgChangeEventChecker", "DhcpPortTableEventChecker", "DhcpRangeTableEventChecker", "DhcpOptionTableEventChecker", "VlanTableEventChecker", "VlanIntfTableEventChecker", "VlanMemberTableEventChecker"] +SMART_SWITCH_CHECKER = ["DpusTableEventChecker", "MidPlaneTableEventChecker"] class MockConfigDb(object): diff --git a/src/sonic-dhcp-utilities/tests/test_dhcp_cfggen.py b/src/sonic-dhcp-utilities/tests/test_dhcp_cfggen.py index dc387bc0c8aa..4d841df7b51e 100644 --- a/src/sonic-dhcp-utilities/tests/test_dhcp_cfggen.py +++ b/src/sonic-dhcp-utilities/tests/test_dhcp_cfggen.py @@ -2,10 +2,10 @@ import ipaddress import json import pytest -from common_utils import MockConfigDb, mock_get_config_db_table, PORT_MODE_CHECKER +from common_utils import MockConfigDb, mock_get_config_db_table, PORT_MODE_CHECKER, SMART_SWITCH_CHECKER from dhcp_utilities.common.utils import DhcpDbConnector from dhcp_utilities.dhcpservd.dhcp_cfggen import DhcpServCfgGenerator -from unittest.mock import patch +from unittest.mock import patch, MagicMock expected_dhcp_config = { "Dhcp4": { @@ -165,7 +165,8 @@ "Vlan1000": { "192.168.0.1/21": { "etp8": [["192.168.0.2", "192.168.0.5"], ["192.168.0.10", "192.168.0.10"]], - "etp7": [["192.168.0.7", "192.168.0.7"]] + "etp7": [["192.168.0.7", "192.168.0.7"]], + "Ethernet40": [["192.168.0.10", "192.168.0.10"]] } } } @@ -302,8 +303,8 @@ def test_parse_vlan(mock_swsscommon_dbconnector_init, mock_parse_port_map_alias, vlan_interfaces, vlan_members = dhcp_cfg_generator._parse_vlan(mock_config_db.config_db.get("VLAN_INTERFACE"), mock_config_db.config_db.get("VLAN_MEMBER")) assert vlan_interfaces == expected_vlan_ipv4_interface - expeceted_members = ["Vlan1000|Ethernet24", "Vlan1000|Ethernet28", "Vlan1000|Ethernet40", "Vlan3000|Ethernet44"] - assert list(vlan_members) == expeceted_members + assert vlan_members == set(["Vlan1000|Ethernet24", "Vlan1000|Ethernet28", "Vlan1000|Ethernet40", + "Vlan3000|Ethernet44"]) @pytest.mark.parametrize("test_config_db", ["mock_config_db.json", "mock_config_db_without_port_config.json"]) @@ -323,17 +324,22 @@ def test_parse_port(test_config_db, mock_swsscommon_dbconnector_init, mock_get_r 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): +@pytest.mark.parametrize("mid_plane", [{}, {"bridge": "mid_plane", "ip_prefix": "192.168.0.1/24"}]) +@pytest.mark.parametrize("is_smart_switch", [True, False]) +def test_generate(mock_swsscommon_dbconnector_init, mock_parse_port_map_alias, mock_get_render_template, mid_plane, + is_smart_switch): with patch.object(DhcpServCfgGenerator, "_parse_hostname"), \ - patch.object(DhcpServCfgGenerator, "_parse_vlan", return_value=(None, None)), \ + patch.object(DhcpServCfgGenerator, "_parse_vlan", return_value=({}, set(["Ethernet0"]))), \ 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, "_parse_dpu", side_effect=[mid_plane, set()]), \ 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): + patch.object(DhcpDbConnector, "get_config_db_table", side_effect=mock_get_config_db_table), \ + patch("dhcp_utilities.dhcpservd.dhcp_cfggen.is_smart_switch", return_value=is_smart_switch): dhcp_db_connector = DhcpDbConnector() dhcp_cfg_generator = DhcpServCfgGenerator(dhcp_db_connector) kea_dhcp4_config, used_ranges, enabled_dhcp_interfaces, used_options, subscribe_table = \ @@ -342,7 +348,11 @@ def test_generate(mock_swsscommon_dbconnector_init, mock_parse_port_map_alias, m assert used_ranges == set(["range1"]) assert enabled_dhcp_interfaces == set(["Vlan1000"]) assert used_options == set(["option1"]) - assert subscribe_table == set(["dummy"]) + expected_tables = set(["dummy"]) + if is_smart_switch: + expected_tables |= set(["DpusTableEventChecker", "MidPlaneTableEventChecker"]) + + assert subscribe_table == expected_tables def test_construct_obj_for_template(mock_swsscommon_dbconnector_init, mock_parse_port_map_alias, @@ -414,3 +424,13 @@ def test_parse_customized_options(mock_swsscommon_dbconnector_init, mock_get_ren } else: assert customized_options == {} + + +def test_parse_dpus(mock_swsscommon_dbconnector_init, mock_get_render_template, mock_parse_port_map_alias): + dhcp_db_connector = DhcpDbConnector() + dhcp_cfg_generator = DhcpServCfgGenerator(dhcp_db_connector) + dpus_table = {"dpu0": {"midplane_interface": "dpu0"}} + mid_plane_table = {"GLOBAL": {"bridge": "bridge_midplane", "ip_prefix": "169.254.200.254/24"}} + mid_plane, dpus = dhcp_cfg_generator._parse_dpu(dpus_table, mid_plane_table) + assert mid_plane == {"bridge": "bridge_midplane", "ip_prefix": "169.254.200.254/24"} + assert dpus == set(["dpu0"]) diff --git a/src/sonic-dhcp-utilities/tests/test_dhcp_db_monitor.py b/src/sonic-dhcp-utilities/tests/test_dhcp_db_monitor.py index 8a5a58c9c8ca..4352ee60f7a7 100644 --- a/src/sonic-dhcp-utilities/tests/test_dhcp_db_monitor.py +++ b/src/sonic-dhcp-utilities/tests/test_dhcp_db_monitor.py @@ -369,7 +369,7 @@ def test_feature_table_checker(mock_swsscommon_dbconnector_init, tested_data, te assert expected_res == check_res -@pytest.mark.parametrize("tested_db_snapshot", [{"enabled_dhcp_interfaces": {"bridge_midplane": ["dpu0"]}}, {}]) +@pytest.mark.parametrize("tested_db_snapshot", [{"enabled_dhcp_interfaces": {"bridge_midplane"}}, {}]) @pytest.mark.parametrize("tested_data", get_subscribe_table_tested_data("test_mid_plane_update")) def test_mid_plane_table_checker(mock_swsscommon_dbconnector_init, tested_data, tested_db_snapshot): with patch.object(ConfigDbEventChecker, "enable"), \ @@ -386,9 +386,8 @@ def test_mid_plane_table_checker(mock_swsscommon_dbconnector_init, tested_data, assert expected_res == check_res -@pytest.mark.parametrize("tested_db_snapshot", [{"enabled_dhcp_interfaces": {"bridge_midplane": ["dpu0"]}}, {}]) @pytest.mark.parametrize("tested_data", get_subscribe_table_tested_data("test_dpus_update")) -def test_dpus_table_checker(mock_swsscommon_dbconnector_init, tested_data, tested_db_snapshot): +def test_dpus_table_checker(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), \ @@ -396,8 +395,5 @@ def test_dpus_table_checker(mock_swsscommon_dbconnector_init, tested_data, teste sel = swsscommon.Select() db_event_checker = DpusTableEventChecker(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 + check_res = db_event_checker.check_update_event({}) + assert expected_res == check_res diff --git a/src/sonic-dhcp-utilities/tests/test_utils.py b/src/sonic-dhcp-utilities/tests/test_utils.py index 8017d23cb73b..cf69c2f93377 100644 --- a/src/sonic-dhcp-utilities/tests/test_utils.py +++ b/src/sonic-dhcp-utilities/tests/test_utils.py @@ -152,3 +152,9 @@ def test_get_target_process_cmds(): ] ] assert res == expected_res + + +@pytest.mark.parametrize("is_smart_switch", [True, False]) +def test_is_smart_switch(is_smart_switch): + device_metadata = {"localhost": {"subtype": "SmartSwitch"}} if is_smart_switch else {"localhost": {}} + assert utils.is_smart_switch(device_metadata) == is_smart_switch