From 3112407a588f688e315ab8c8642f47e6054d334f Mon Sep 17 00:00:00 2001 From: Jianquan Ye Date: Mon, 23 Sep 2024 17:18:22 +1000 Subject: [PATCH] [Test gap] Test dhcp_relay with source port ip enabled (#14653) Description of PR Summary: Fixes #3624 Mitigate the test gap: Test dhcp relay with source port ip in relay enabled. Approach What is the motivation for this PR? Fixes #3624 Mitigate the test gap: Test dhcp relay with source port ip in relay enabled. How did you do it? Enhance dhcp_relay ptf test to verify the src_ip in relay packets Add a fixture which modify deployment_id to 8 and enable source port ip in relay Add a test case exactly same with test_dhcp_relay_default but include fixture enable_source_port_ip_in_relay. How did you verify/test it? Run on local dev vm, dhcp_relay/test_dhcp_relay.py::test_interface_binding PASSED [ 12%] dhcp_relay/test_dhcp_relay.py::test_dhcp_relay_default PASSED [ 25%] dhcp_relay/test_dhcp_relay.py::test_dhcp_relay_with_source_port_ip_in_relay_enabled PASSED [ 37%] dhcp_relay/test_dhcp_relay.py::test_dhcp_relay_after_link_flap PASSED [ 50%] dhcp_relay/test_dhcp_relay.py::test_dhcp_relay_start_with_uplinks_down PASSED [ 62%] dhcp_relay/test_dhcp_relay.py::test_dhcp_relay_unicast_mac PASSED [ 75%] dhcp_relay/test_dhcp_relay.py::test_dhcp_relay_random_sport PASSED [ 87%] dhcp_relay/test_dhcp_relay.py::test_dhcp_relay_counter SKIPPED (skip...) [100%] and PR test will test it again. co-authorized by: jianquanye@microsoft.com --- .../files/ptftests/py3/dhcp_relay_test.py | 16 +- tests/common/gcu_utils.py | 147 ++++++++++++++++++ tests/dhcp_relay/test_dhcp_relay.py | 139 +++++++++++++++++ 3 files changed, 298 insertions(+), 4 deletions(-) create mode 100644 tests/common/gcu_utils.py diff --git a/ansible/roles/test/files/ptftests/py3/dhcp_relay_test.py b/ansible/roles/test/files/ptftests/py3/dhcp_relay_test.py index 3a3a2617ee..9c4f8b804e 100644 --- a/ansible/roles/test/files/ptftests/py3/dhcp_relay_test.py +++ b/ansible/roles/test/files/ptftests/py3/dhcp_relay_test.py @@ -186,6 +186,7 @@ def setUp(self): self.dest_mac_address = self.test_params['dest_mac_address'] self.client_udp_src_port = self.test_params['client_udp_src_port'] + self.enable_source_port_ip_in_relay = self.test_params.get('enable_source_port_ip_in_relay', False) def tearDown(self): DataplaneBaseTest.tearDown(self) @@ -229,7 +230,12 @@ def create_dhcp_discover_relayed_packet(self): # be loopback. We could pull from minigraph and check here. ether = scapy.Ether(dst=self.BROADCAST_MAC, src=self.uplink_mac, type=0x0800) - ip = scapy.IP(src=self.DEFAULT_ROUTE_IP, + + source_ip = self.switch_loopback_ip + if self.enable_source_port_ip_in_relay: + source_ip = self.relay_iface_ip + + ip = scapy.IP(src=source_ip, dst=self.BROADCAST_IP, len=328, ttl=64) udp = scapy.UDP(sport=self.DHCP_SERVER_PORT, dport=self.DHCP_SERVER_PORT, len=308) @@ -420,7 +426,11 @@ def create_dhcp_request_relayed_packet(self): # be loopback. We could pull from minigraph and check here. ether = scapy.Ether(dst=self.BROADCAST_MAC, src=self.uplink_mac, type=0x0800) - ip = scapy.IP(src=self.DEFAULT_ROUTE_IP, + + source_ip = self.switch_loopback_ip + if self.enable_source_port_ip_in_relay: + source_ip = self.relay_iface_ip + ip = scapy.IP(src=source_ip, dst=self.BROADCAST_IP, len=336, ttl=64) udp = scapy.UDP(sport=self.DHCP_SERVER_PORT, dport=self.DHCP_SERVER_PORT, len=316) @@ -562,7 +572,6 @@ def verify_relayed_discover(self): masked_discover.set_do_not_care_scapy(scapy.IP, "ttl") masked_discover.set_do_not_care_scapy(scapy.IP, "proto") masked_discover.set_do_not_care_scapy(scapy.IP, "chksum") - masked_discover.set_do_not_care_scapy(scapy.IP, "src") masked_discover.set_do_not_care_scapy(scapy.IP, "dst") masked_discover.set_do_not_care_scapy(scapy.IP, "options") @@ -639,7 +648,6 @@ def verify_relayed_request(self): masked_request.set_do_not_care_scapy(scapy.IP, "ttl") masked_request.set_do_not_care_scapy(scapy.IP, "proto") masked_request.set_do_not_care_scapy(scapy.IP, "chksum") - masked_request.set_do_not_care_scapy(scapy.IP, "src") masked_request.set_do_not_care_scapy(scapy.IP, "dst") masked_request.set_do_not_care_scapy(scapy.IP, "options") diff --git a/tests/common/gcu_utils.py b/tests/common/gcu_utils.py new file mode 100644 index 0000000000..d4cd28bfe6 --- /dev/null +++ b/tests/common/gcu_utils.py @@ -0,0 +1,147 @@ +import json +import logging + +import pytest + +from tests.common import config_reload +from tests.common.helpers.assertions import pytest_assert + +logger = logging.getLogger(__name__) +DEFAULT_CHECKPOINT_NAME = "test" + + +def generate_tmpfile(duthost): + """Generate temp file + """ + return duthost.shell('mktemp')['stdout'] + + +def apply_patch(duthost, json_data, dest_file): + """Run apply-patch on target duthost + + Args: + duthost: Device Under Test (DUT) + json_data: Source json patch to apply + dest_file: Destination file on duthost + """ + duthost.copy(content=json.dumps(json_data, indent=4), dest=dest_file) + + cmds = 'config apply-patch {}'.format(dest_file) + + logger.info("Commands: {}".format(cmds)) + output = duthost.shell(cmds, module_ignore_errors=True) + + return output + + +def delete_tmpfile(duthost, tmpfile): + """Delete temp file + """ + duthost.file(path=tmpfile, state='absent') + + +def create_checkpoint(duthost, cp=DEFAULT_CHECKPOINT_NAME): + """Run checkpoint on target duthost + + Args: + duthost: Device Under Test (DUT) + cp: checkpoint filename + """ + cmds = 'config checkpoint {}'.format(cp) + + logger.info("Commands: {}".format(cmds)) + output = duthost.shell(cmds, module_ignore_errors=True) + + pytest_assert( + not output['rc'] + and "Checkpoint created successfully" in output['stdout'] + and verify_checkpoints_exist(duthost, cp), + "Failed to config a checkpoint file: {}".format(cp) + ) + + +def list_checkpoints(duthost): + """List checkpoint on target duthost + + Args: + duthost: Device Under Test (DUT) + """ + cmds = 'config list-checkpoints' + + logger.info("Commands: {}".format(cmds)) + output = duthost.shell(cmds, module_ignore_errors=True) + + pytest_assert( + not output['rc'], + "Failed to list all checkpoint file" + ) + + return output + + +def verify_checkpoints_exist(duthost, cp): + """Check if checkpoint file exist in duthost + """ + output = list_checkpoints(duthost) + return '"{}"'.format(cp) in output['stdout'] + + +def rollback(duthost, cp=DEFAULT_CHECKPOINT_NAME): + """Run rollback on target duthost + + Args: + duthost: Device Under Test (DUT) + cp: rollback filename + """ + cmds = 'config rollback {}'.format(cp) + + logger.info("Commands: {}".format(cmds)) + output = duthost.shell(cmds, module_ignore_errors=True) + + return output + + +def rollback_or_reload(duthost, cp=DEFAULT_CHECKPOINT_NAME): + """Run rollback on target duthost. config_reload if rollback failed. + + Args: + duthost: Device Under Test (DUT) + """ + output = rollback(duthost, cp) + + if output['rc'] or "Config rolled back successfully" not in output['stdout']: + config_reload(duthost) + pytest.fail("config rollback failed. Restored by config_reload") + + +def delete_checkpoint(duthost, cp=DEFAULT_CHECKPOINT_NAME): + """Run checkpoint on target duthost + + Args: + duthost: Device Under Test (DUT) + cp: checkpoint filename + """ + pytest_assert( + verify_checkpoints_exist(duthost, cp), + "Failed to find the checkpoint file: {}".format(cp) + ) + + cmds = 'config delete-checkpoint {}'.format(cp) + + logger.info("Commands: {}".format(cmds)) + output = duthost.shell(cmds, module_ignore_errors=True) + + pytest_assert( + not output['rc'] and "Checkpoint deleted successfully" in output['stdout'], + "Failed to delete a checkpoint file: {}".format(cp) + ) + + +def expect_op_success(duthost, output): + """Expected success from apply-patch output + """ + pytest_assert(not output['rc'], "Command is not running successfully") + pytest_assert( + "Patch applied successfully" in output['stdout'], + "Please check if json file is validate" + ) diff --git a/tests/dhcp_relay/test_dhcp_relay.py b/tests/dhcp_relay/test_dhcp_relay.py index 4150c719e8..90c77fef8b 100644 --- a/tests/dhcp_relay/test_dhcp_relay.py +++ b/tests/dhcp_relay/test_dhcp_relay.py @@ -7,6 +7,9 @@ from tests.common.fixtures.ptfhost_utils import copy_ptftests_directory # noqa F401 from tests.common.fixtures.ptfhost_utils import change_mac_addresses # noqa F401 from tests.common.dualtor.mux_simulator_control import toggle_all_simulator_ports_to_rand_selected_tor_m # noqa F401 +from tests.common.gcu_utils import generate_tmpfile, create_checkpoint, \ + apply_patch, expect_op_success, delete_tmpfile, \ + rollback_or_reload, delete_checkpoint from tests.ptf_runner import ptf_runner from tests.common.utilities import wait_until from tests.common.helpers.dut_utils import check_link_status @@ -52,6 +55,53 @@ def check_interface_status(duthost): return False +@pytest.fixture(scope="function") +def enable_source_port_ip_in_relay(duthosts, rand_one_dut_hostname, tbinfo): + duthost = duthosts[rand_one_dut_hostname] + + """ + Enable source port ip in relay function + -si parameter(Enable source port ip in relay function) will be added if deployment_id is '8', ref: + https://github.com/sonic-net/sonic-buildimage/blob/e0e0c0c1b3c58635bc25fde6a77ca3b0849dfde1/dockers/docker-dhcp-relay/dhcpv4-relay.agents.j2#L16 + """ + + json_patch = [ + { + "op": "replace", + "path": "/DEVICE_METADATA/localhost/deployment_id", + "value": "8" + } + ] + + tmpfile = generate_tmpfile(duthost) + logger.info("tmpfile {}".format(tmpfile)) + check_point = "dhcp_relay" + try: + create_checkpoint(duthost, check_point) + output = apply_patch(duthost, json_data=json_patch, dest_file=tmpfile) + expect_op_success(duthost, output) + duthost.restart_service("dhcp_relay") + + def dhcp_ready(enable_source_port_ip_in_relay): + dhcp_relay_running = duthost.is_service_fully_started("dhcp_relay") + dhcp_relay_process = duthost.shell("ps -ef |grep dhcrelay|grep -v grep", + module_ignore_errors=True)["stdout"] + if enable_source_port_ip_in_relay: + dhcp_relay_process_ready = "-si" in dhcp_relay_process and "dhcrelay" in dhcp_relay_process + else: + dhcp_relay_process_ready = "-si" not in dhcp_relay_process and "dhcrelay" in dhcp_relay_process + return dhcp_relay_running and dhcp_relay_process_ready + pytest_assert(wait_until(60, 2, 0, dhcp_ready, True), "Source port ip in relay is not enabled!") + yield + finally: + delete_tmpfile(duthost, tmpfile) + logger.info("Rolled back to original checkpoint") + rollback_or_reload(duthost, check_point) + delete_checkpoint(duthost, check_point) + duthost.restart_service("dhcp_relay") + pytest_assert(wait_until(60, 2, 0, dhcp_ready, False), "Source port ip in relay is not disabled!") + + def test_interface_binding(duthosts, rand_one_dut_hostname, dut_dhcp_relay_data): duthost = duthosts[rand_one_dut_hostname] skip_release(duthost, ["201811", "201911", "202106"]) @@ -184,6 +234,95 @@ def test_dhcp_relay_default(ptfhost, dut_dhcp_relay_data, validate_dut_routes_ex pytest_assert(wait_until(120, 5, 0, check_interface_status, duthost)) +def test_dhcp_relay_with_source_port_ip_in_relay_enabled(ptfhost, dut_dhcp_relay_data, + validate_dut_routes_exist, testing_config, + setup_standby_ports_on_rand_unselected_tor, # noqa F811 + rand_unselected_dut, toggle_all_simulator_ports_to_rand_selected_tor_m, # noqa F811 + enable_source_port_ip_in_relay): + """Test DHCP relay functionality on T0 topology. + For each DHCP relay agent running on the DuT, verify DHCP packets are relayed properly + """ + testing_mode, duthost = testing_config + + if testing_mode == DUAL_TOR_MODE: + skip_release(duthost, ["201811", "201911"]) + + skip_dhcpmon = any(vers in duthost.os_version for vers in ["201811", "201911", "202111"]) + + try: + for dhcp_relay in dut_dhcp_relay_data: + if not skip_dhcpmon: + dhcp_server_num = len(dhcp_relay['downlink_vlan_iface']['dhcp_server_addrs']) + if testing_mode == DUAL_TOR_MODE: + standby_duthost = rand_unselected_dut + start_dhcp_monitor_debug_counter(standby_duthost) + expected_standby_agg_counter_message = ( + r".*dhcp_relay#dhcpmon\[[0-9]+\]: " + r"\[\s*Agg-%s\s*-[\sA-Za-z0-9]+\s*rx/tx\] " + r"Discover: +0/ +0, Offer: +0/ +0, Request: +0/ +0, ACK: +0/ +0+" + ) % (dhcp_relay['downlink_vlan_iface']['name']) + loganalyzer_standby = LogAnalyzer(ansible_host=standby_duthost, marker_prefix="dhcpmon counter") + marker_standby = loganalyzer_standby.init() + loganalyzer_standby.expect_regex = [expected_standby_agg_counter_message] + start_dhcp_monitor_debug_counter(duthost) + if testing_mode == DUAL_TOR_MODE: + expected_agg_counter_message = ( + r".*dhcp_relay#dhcpmon\[[0-9]+\]: " + r"\[\s*Agg-%s\s*-[\sA-Za-z0-9]+\s*rx/tx\] " + r"Discover: +1/ +%d, Offer: +1/ +1, Request: +1/ +%d, ACK: +1/ +1+" + ) % (dhcp_relay['downlink_vlan_iface']['name'], dhcp_server_num, dhcp_server_num) + else: + expected_agg_counter_message = ( + r".*dhcp_relay#dhcpmon\[[0-9]+\]: " + r"\[\s*Agg-%s\s*-[\sA-Za-z0-9]+\s*rx/tx\] " + r"Discover: +1/ +%d, Offer: +1/ +1, Request: +2/ +%d, ACK: +1/ +1+" + ) % (dhcp_relay['downlink_vlan_iface']['name'], dhcp_server_num, dhcp_server_num * 2) + loganalyzer = LogAnalyzer(ansible_host=duthost, marker_prefix="dhcpmon counter") + marker = loganalyzer.init() + loganalyzer.expect_regex = [expected_agg_counter_message] + + # Run the DHCP relay test on the PTF host + ptf_runner(ptfhost, + "ptftests", + "dhcp_relay_test.DHCPTest", + platform_dir="ptftests", + params={"hostname": duthost.hostname, + "client_port_index": dhcp_relay['client_iface']['port_idx'], + # This port is introduced to test DHCP relay packet received + # on other client port + "other_client_port": repr(dhcp_relay['other_client_ports']), + "client_iface_alias": str(dhcp_relay['client_iface']['alias']), + "leaf_port_indices": repr(dhcp_relay['uplink_port_indices']), + "num_dhcp_servers": len(dhcp_relay['downlink_vlan_iface']['dhcp_server_addrs']), + "server_ip": dhcp_relay['downlink_vlan_iface']['dhcp_server_addrs'], + "relay_iface_ip": str(dhcp_relay['downlink_vlan_iface']['addr']), + "relay_iface_mac": str(dhcp_relay['downlink_vlan_iface']['mac']), + "relay_iface_netmask": str(dhcp_relay['downlink_vlan_iface']['mask']), + "dest_mac_address": BROADCAST_MAC, + "client_udp_src_port": DEFAULT_DHCP_CLIENT_PORT, + "switch_loopback_ip": dhcp_relay['switch_loopback_ip'], + "uplink_mac": str(dhcp_relay['uplink_mac']), + "testing_mode": testing_mode, + "enable_source_port_ip_in_relay": True}, + log_file="/tmp/dhcp_relay_test.DHCPTest.log", is_python3=True) + if not skip_dhcpmon: + time.sleep(36) # dhcpmon debug counter prints every 18 seconds + loganalyzer.analyze(marker) + if testing_mode == DUAL_TOR_MODE: + loganalyzer_standby.analyze(marker_standby) + except LogAnalyzerError as err: + logger.error("Unable to find expected log in syslog") + raise err + + if not skip_dhcpmon: + # Clean up - Restart DHCP relay service on DUT to recover original dhcpmon setting + restart_dhcp_service(duthost) + if testing_mode == DUAL_TOR_MODE: + restart_dhcp_service(standby_duthost) + pytest_assert(wait_until(120, 5, 0, check_interface_status, standby_duthost)) + pytest_assert(wait_until(120, 5, 0, check_interface_status, duthost)) + + def test_dhcp_relay_after_link_flap(ptfhost, dut_dhcp_relay_data, validate_dut_routes_exist, testing_config): """Test DHCP relay functionality on T0 topology after uplinks flap For each DHCP relay agent running on the DuT, with relay agent running, flap the uplinks,