Skip to content

Commit

Permalink
[smart_switch][dhcp_server] Add smart_switch support in dhcpservd (#1…
Browse files Browse the repository at this point in the history
…7576)

* [smart_switch][dhcp_server] Add related checker for smart_switch in dhcp_db_monitor
* [smart_switch][dhcp_server] Add smart_switch support in dhcpservd
  • Loading branch information
yaqiangz authored Jan 9, 2024
1 parent e30782b commit b2ca36a
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 32 deletions.
11 changes: 11 additions & 0 deletions src/sonic-dhcp-utilities/dhcp_utilities/common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
70 changes: 56 additions & 14 deletions src/sonic-dhcp-utilities/dhcp_utilities/dhcpservd/dhcp_cfggen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions src/sonic-dhcp-utilities/tests/common_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
PORT_MODE_CHECKER = ["DhcpServerTableCfgChangeEventChecker", "DhcpPortTableEventChecker", "DhcpRangeTableEventChecker",
"DhcpOptionTableEventChecker", "VlanTableEventChecker", "VlanIntfTableEventChecker",
"VlanMemberTableEventChecker"]
SMART_SWITCH_CHECKER = ["DpusTableEventChecker", "MidPlaneTableEventChecker"]


class MockConfigDb(object):
Expand Down
38 changes: 29 additions & 9 deletions src/sonic-dhcp-utilities/tests/test_dhcp_cfggen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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"]]
}
}
}
Expand Down Expand Up @@ -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"])
Expand All @@ -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 = \
Expand All @@ -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,
Expand Down Expand Up @@ -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"])
12 changes: 4 additions & 8 deletions src/sonic-dhcp-utilities/tests/test_dhcp_db_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"), \
Expand All @@ -386,18 +386,14 @@ 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), \
patch.object(sys, "exit"):
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
6 changes: 6 additions & 0 deletions src/sonic-dhcp-utilities/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit b2ca36a

Please sign in to comment.