[smart_switch][dhcp_server] Add smart_switch support in dhcpservd (#17576)

* [smart_switch][dhcp_server] Add related checker for smart_switch in dhcp_db_monitor
* [smart_switch][dhcp_server] Add smart_switch support in dhcpservd
This commit is contained in:
Yaqiang Zhu 2024-01-10 07:40:11 +08:00 committed by GitHub
parent e30782b0fe
commit b2ca36aa1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 110 additions and 32 deletions

View File

@ -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"

View File

@ -5,7 +5,7 @@ import os
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_RANGE = "DHCP_SERVER_IPV4_RANGE"
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 @@ class DhcpServCfgGenerator(object):
# 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 @@ class DhcpServCfgGenerator(object):
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 @@ class DhcpServCfgGenerator(object):
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 @@ class DhcpServCfgGenerator(object):
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

View File

@ -9,7 +9,7 @@ from .dhcp_lease import LeaseManager
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()

View File

@ -14,6 +14,7 @@ VLAN_MEMBER = "VLAN_MEMBER"
PORT_MODE_CHECKER = ["DhcpServerTableCfgChangeEventChecker", "DhcpPortTableEventChecker", "DhcpRangeTableEventChecker",
"DhcpOptionTableEventChecker", "VlanTableEventChecker", "VlanIntfTableEventChecker",
"VlanMemberTableEventChecker"]
SMART_SWITCH_CHECKER = ["DpusTableEventChecker", "MidPlaneTableEventChecker"]
class MockConfigDb(object):

View File

@ -2,10 +2,10 @@ import copy
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 @@ expected_parsed_port = {
"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"])

View File

@ -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

View File

@ -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