From 465d498e5c1be68a6d574b315b4d47990570b457 Mon Sep 17 00:00:00 2001 From: Mihut Aronovici Date: Wed, 28 Jan 2026 23:26:45 -0500 Subject: [PATCH 01/19] add the ha test with PL config traffic --- tests/ha/configs/privatelink_config.py | 192 +++++++++ tests/ha/conftest.py | 402 ++++++++++++++++++ tests/ha/constants.py | 41 ++ tests/ha/dash_utils.py | 131 ++++++ tests/ha/gnmi_utils.py | 459 +++++++++++++++++++++ tests/ha/packets.py | 544 +++++++++++++++++++++++++ tests/ha/proto_utils.py | 350 ++++++++++++++++ tests/ha/test_dash_privatelink.py | 102 +++++ tests/ha/test_ha_dash_privatelink.py | 102 +++++ 9 files changed, 2323 insertions(+) create mode 100644 tests/ha/configs/privatelink_config.py create mode 100644 tests/ha/constants.py create mode 100644 tests/ha/dash_utils.py create mode 100644 tests/ha/gnmi_utils.py create mode 100644 tests/ha/packets.py create mode 100644 tests/ha/proto_utils.py create mode 100644 tests/ha/test_dash_privatelink.py create mode 100644 tests/ha/test_ha_dash_privatelink.py diff --git a/tests/ha/configs/privatelink_config.py b/tests/ha/configs/privatelink_config.py new file mode 100644 index 00000000000..4630e4db6e4 --- /dev/null +++ b/tests/ha/configs/privatelink_config.py @@ -0,0 +1,192 @@ +from dash_api.eni_pb2 import State, EniMode +from dash_api.route_type_pb2 import ActionType, EncapType, RoutingType +from dash_api.types_pb2 import IpVersion + +VNET = "vnet" +VNET_ENCAP = "vnet_encap" +VNET_DIRECT = "vnet_direct" +PRIVATELINK = "privatelink" +DECAP = "decap" + +APPLIANCE_VIP="3.2.1.0" +VM1_PA = "25.1.1.1" # VM host physical address +VM1_CA = "10.0.0.11" # VM customer address +VM_CA_SUBNET = "10.0.0.0/16" +PE_PA = "101.1.2.3" # private endpoint physical address +PE_CA = "10.2.0.100" # private endpoint customer address +PE_CA_SUBNET = "10.2.0.0/16" +PL_ENCODING_IP = "::d107:64:ff71:0:0" +PL_ENCODING_MASK = "::ffff:ffff:ffff:0:0" +PL_OVERLAY_SIP = "fd41:108:20:abc:abc::0" +PL_OVERLAY_SIP_MASK = "ffff:ffff:ffff:ffff:ffff:ffff::" +PL_OVERLAY_DIP = "2603:10e1:100:2::3401:203" +PL_OVERLAY_DIP_MASK = "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff" + +APPLIANCE_ID = "100" +LOCAL_REGION_ID = "100" +VM_VNI = 4321 +ENCAP_VNI = 100 +NSG_OUTBOUND_VNI = 100 +VNET1 = "Vnet1" +VNET2 = "Vnet2" +VNET1_VNI = "2001" +VNET1_GUID = "559c6ce8-26ab-4193-b946-ccc6e8f930b2" +VNET2_GUID = "559c6ce8-26ab-4193-b946-ccc6e8f930b3" +VM_MAC = "44:E3:9F:EF:C4:6E" +ENI_MAC = "F4:93:9F:EF:C4:7E" +ENI_MAC_STRING = ENI_MAC.replace(":", "") +# REMOTE MAC is corresponding to PE MAC +REMOTE_MAC = "43:BE:65:25:FA:67" +REMOTE_MAC_STRING = REMOTE_MAC.replace(":", "") +ENI_ID = "497f23d7-f0ac-4c99-a98f-59b470e8c7bd" +ROUTE_GROUP1 = "RouteGroup1" +ROUTE_GROUP2 = "RouteGroup2" +ROUTE_GROUP1_GUID = "48af6ce8-26cc-4293-bfa6-0126e8fcdeb2" +ROUTE_GROUP2_GUID = "58cf62e0-22cc-4693-baa6-012358fcdec9" +OUTBOUND_DIR_LOOKUP = "dst_mac" +ENI_ID2 = "497f23d7-f0ac-4c99-a98f-59b470e8c7bd" +ENI_TRUSTED_VNI = "800" +METER_POLICY_V4 = "MeterPolicyV4" +METER_RULE_V4_PREFIX1 = "48.10.5.0/24" +METER_RULE_V4_PREFIX2 = "92.6.0.0/16" + +APPLIANCE_CONFIG = { + f"DASH_APPLIANCE_TABLE:{APPLIANCE_ID}": { + "sip": APPLIANCE_VIP, + "vm_vni": VM_VNI, + "local_region_id": LOCAL_REGION_ID, + "trusted_vnis": [ENCAP_VNI, NSG_OUTBOUND_VNI], + } +} + + +VNET_CONFIG = { + f"DASH_VNET_TABLE:{VNET1}": { + "vni": VNET1_VNI, + "guid": VNET1_GUID + } +} + + +VNET2_CONFIG = { + f"DASH_VNET_TABLE:{VNET2}": { + "vni": VM_VNI, + "guid": VNET2_GUID + } +} + + +ENI_CONFIG = { + f"DASH_ENI_TABLE:{ENI_ID}": { + "vnet": VNET1, + "underlay_ip": VM1_PA, + "mac_address": ENI_MAC, + "eni_id": ENI_ID, + "admin_state": State.STATE_ENABLED, + "pl_underlay_sip": APPLIANCE_VIP, + "pl_sip_encoding": f"{PL_ENCODING_IP}/{PL_ENCODING_MASK}", + "v4_meter_policy_id": METER_POLICY_V4, + "trusted_vnis": VM_VNI + } +} + +PE_VNET_MAPPING_CONFIG = { + f"DASH_VNET_MAPPING_TABLE:{VNET1}:{PE_CA}": { + "routing_type": RoutingType.ROUTING_TYPE_PRIVATELINK, + "underlay_ip": PE_PA, + "overlay_sip_prefix": f"{PL_OVERLAY_SIP}/{PL_OVERLAY_SIP_MASK}", + "overlay_dip_prefix": f"{PL_OVERLAY_DIP}/{PL_OVERLAY_DIP_MASK}", + "metering_class_or": "1586", + } +} + + +INBOUND_VNI_ROUTE_RULE_CONFIG = { + f"DASH_ROUTE_RULE_TABLE:{ENI_ID}:{ENCAP_VNI}:{PE_PA}/32": { + "action_type": ActionType.ACTION_TYPE_DECAP, + "priority": 1 + } +} + + +PE_SUBNET_ROUTE_CONFIG = { + f"DASH_ROUTE_TABLE:{ROUTE_GROUP1}:{PE_CA_SUBNET}": { + "routing_type": RoutingType.ROUTING_TYPE_VNET, + "vnet": VNET1, + "metering_class_or": "2048", + "metering_class_and": "4095", + } +} + +VM_SUBNET_ROUTE_CONFIG = { + f"DASH_ROUTE_TABLE:{ROUTE_GROUP1}:{VM_CA_SUBNET}": { + "routing_type": RoutingType.ROUTING_TYPE_VNET, + "vnet": VNET1, + "metering_class_or": "2048", + "metering_class_and": "4095", + } +} + +ROUTING_TYPE_VNET_CONFIG = { + f"DASH_ROUTING_TYPE_TABLE:{VNET}": { + "items": [ + { + "action_name": "action1", + "action_type": ActionType.ACTION_TYPE_STATICENCAP, + "encap_type": EncapType.ENCAP_TYPE_VXLAN, + }, + ] + } +} + +ROUTING_TYPE_PL_CONFIG = { + f"DASH_ROUTING_TYPE_TABLE:{PRIVATELINK}": { + "items": [ + { + "action_name": "action1", + "action_type": ActionType.ACTION_TYPE_4_to_6 + }, + { + "action_name": "action2", + "action_type": ActionType.ACTION_TYPE_STATICENCAP, + "encap_type": EncapType.ENCAP_TYPE_NVGRE, + "vni": ENCAP_VNI + } + ] + } +} + +ROUTE_GROUP1_CONFIG = { + f"DASH_ROUTE_GROUP_TABLE:{ROUTE_GROUP1}": { + "guid": ROUTE_GROUP1_GUID, + "version": "rg_version" + } +} + +ENI_ROUTE_GROUP1_CONFIG = { + f"DASH_ENI_ROUTE_TABLE:{ENI_ID}": { + "group_id": ROUTE_GROUP1 + } +} + +METER_POLICY_V4_CONFIG = { + f"DASH_METER_POLICY_TABLE:{METER_POLICY_V4}": { + "ip_version": IpVersion.IP_VERSION_IPV4 + } +} + +METER_RULE1_V4_CONFIG = { + f"DASH_METER_RULE_TABLE:{METER_POLICY_V4}:1": { + "priority": "10", + "ip_prefix": f"{METER_RULE_V4_PREFIX1}", + "metering_class": 1, + } +} + +METER_RULE2_V4_CONFIG = { + f"DASH_METER_RULE_TABLE:{METER_POLICY_V4}:2": { + "priority": "10", + "ip_prefix": f"{METER_RULE_V4_PREFIX2}", + "metering_class": 2, + } +} diff --git a/tests/ha/conftest.py b/tests/ha/conftest.py index 66e5d328812..1ea2e8f0761 100644 --- a/tests/ha/conftest.py +++ b/tests/ha/conftest.py @@ -1,4 +1,7 @@ import pytest +import logging +import random +import json from pathlib import Path from collections import defaultdict from tests.common.helpers.constants import DEFAULT_NAMESPACE @@ -10,6 +13,36 @@ add_static_route_to_ptf, add_static_route_to_dut ) +from ipaddress import ip_interface +from constants import ENI, VM_VNI, VNET1_VNI, VNET2_VNI, REMOTE_CA_IP, LOCAL_CA_IP, REMOTE_ENI_MAC, \ + LOCAL_ENI_MAC, REMOTE_CA_PREFIX, LOOPBACK_IP, DUT_MAC, LOCAL_PA_IP, LOCAL_PTF_INTF, LOCAL_PTF_MAC, \ + REMOTE_PA_IP, REMOTE_PTF_INTF, REMOTE_PTF_MAC, REMOTE_PA_PREFIX, VNET1_NAME, VNET2_NAME, ROUTING_ACTION, \ + ROUTING_ACTION_TYPE, LOOKUP_OVERLAY_IP, LOCAL_DUT_INTF, REMOTE_DUT_INTF, \ + REMOTE_PTF_SEND_INTF, REMOTE_PTF_RECV_INTF, LOCAL_REGION_ID, VXLAN_UDP_BASE_SRC_PORT, VXLAN_UDP_SRC_PORT_MASK, \ + NPU_DATAPLANE_IP, NPU_DATAPLANE_MAC, NPU_DATAPLANE_PORT, DPU_DATAPLANE_IP, DPU_DATAPLANE_MAC, DPU_DATAPLANE_PORT +from dash_utils import render_template_to_host, apply_swssconfig_file +from gnmi_utils import generate_gnmi_cert, apply_gnmi_cert, recover_gnmi_cert, apply_gnmi_file +from tests.common.helpers.smartswitch_util import correlate_dpu_info_with_dpuhost, get_data_port_on_dpu, get_dpu_dataplane_port # noqa F401 +from tests.common import config_reload +import configs.privatelink_config as pl +from tests.common.helpers.assertions import pytest_require as pt_require + +logger = logging.getLogger(__name__) + +ENABLE_GNMI_API = True + +@pytest.fixture(scope="module") +def setup_ha_config(duthosts): + pass + +@pytest.fixture(scope="module") +def program_ha_set_and_initial_ha_scope(duthosts): + pass + + +@pytest.fixture(scope="module") +def ha_activate_role(duthosts): + pass @pytest.fixture(scope="module") @@ -122,3 +155,372 @@ def setup_namespaces_with_routes(ptfhost, duthosts, get_t2_info): if ns["namespace"] not in visited_namespaces: remove_namespace(ptfhost, ns["namespace"]) visited_namespaces.add(ns["namespace"]) + + + +def get_interface_ip(duthost, interface): + cmd = f"ip addr show {interface} | grep -w inet | awk '{{print $2}}'" + output = duthost.shell(cmd)["stdout"].strip() + return ip_interface(output) + + +def pytest_addoption(parser): + """ + Adds pytest options that are used by DASH tests + """ + + parser.addoption( + "--skip_config", + action="store_true", + help="Don't apply configurations on DUT" + ) + + parser.addoption( + "--config_only", + action="store_true", + help="Apply new configurations on DUT without running tests" + ) + + parser.addoption( + "--skip_dataplane_checking", + action="store_true", + help="Skip dataplane checking" + ) + + parser.addoption( + "--vxlan_udp_dport", + action="store", + default="random", + help="The vxlan udp dst port used in the test" + ) + + parser.addoption( + "--skip_cert_cleanup", + action="store_true", + help="Skip certificates cleanup after test" + ) + + parser.addoption( + "--dpu_index", + action="store", + default=0, + type=int, + help="The default dpu used for the test" + ) + + +@pytest.fixture(scope="module") +def config_only(request): + return request.config.getoption("--config_only") + + +@pytest.fixture(scope="module") +def skip_config(request): + return request.config.getoption("--skip_config") + + +@pytest.fixture(scope="module") +def skip_cleanup(request): + return request.config.getoption("--skip_cleanup") + + +@pytest.fixture(scope="module") +def skip_dataplane_checking(request): + return request.config.getoption("--skip_dataplane_checking") + + +@pytest.fixture(scope="module") +def skip_cert_cleanup(request): + return request.config.getoption("--skip_cert_cleanup") + + +@pytest.fixture(scope="module") +def config_facts(duthost): + return duthost.config_facts(host=duthost.hostname, source="running")['ansible_facts'] + + +@pytest.fixture(scope="module") +def minigraph_facts(duthosts, rand_one_dut_hostname, tbinfo): + """ + Fixture to get minigraph facts + + Args: + duthost: DUT host object + + Returns: + Dictionary containing minigraph information + """ + duthost = duthosts[rand_one_dut_hostname] + + return duthost.get_extended_minigraph_facts(tbinfo) + + +def get_intf_from_ip(local_ip, config_facts): + for intf, config in list(config_facts["INTERFACE"].items()): + for ip in config: + intf_ip = ip_interface(ip) + if str(intf_ip.ip) == local_ip: + return intf, intf_ip + + for intf, config in list(config_facts["PORTCHANNEL_INTERFACE"].items()): + for ip in config: + intf_ip = ip_interface(ip) + if str(intf_ip.ip) == local_ip: + return intf, intf_ip + + +@pytest.fixture(params=["no-underlay-route", "with-underlay-route"]) +def use_underlay_route(request): + return request.param == "with-underlay-route" + + +@pytest.fixture(scope="module") +def dash_pl_config(duthosts, dpuhosts, dpu_index, duts_minigraph_facts): + dash_info = [{ + LOCAL_CA_IP: "10.2.2.2", + } for _ in range(2)] + + for i in range(len(duthosts)): + config_facts = duthosts[i].get_running_config_facts() + minigraph_facts = duts_minigraph_facts[duthosts[i].hostname] + neigh_table = duthosts[i].switch_arptable()['ansible_facts']['arptable'] + dash_info[i][DUT_MAC] = config_facts["DEVICE_METADATA"]["localhost"]["mac"] + for neigh_ip, config in list(config_facts["BGP_NEIGHBOR"].items()): + if ip_interface(neigh_ip).version == 4: + if LOCAL_PTF_INTF not in dash_info[i] and config["name"].endswith("T0"): + intf, _ = get_intf_from_ip(config['local_addr'], config_facts) + dash_info[i][LOCAL_PTF_INTF] = minigraph_facts[0][1]["minigraph_ptf_indices"][intf] + dash_info[i][LOCAL_DUT_INTF] = intf + dash_info[i][LOCAL_PTF_MAC] = neigh_table["v4"][neigh_ip]["macaddress"] + if REMOTE_PTF_SEND_INTF not in dash_info[i] and config["name"].endswith("T2"): + intf, _ = get_intf_from_ip(config['local_addr'], config_facts) + intfs = list(config_facts["PORTCHANNEL_MEMBER"][intf].keys()) + dash_info[i][REMOTE_PTF_SEND_INTF] = minigraph_facts[0][1]["minigraph_ptf_indices"][intfs[0]] + dash_info[i][REMOTE_PTF_RECV_INTF] = [minigraph_facts[0][1]["minigraph_ptf_indices"][j] for j in intfs] + dash_info[i][REMOTE_DUT_INTF] = intf + dash_info[i][REMOTE_PTF_MAC] = neigh_table["v4"][neigh_ip]["macaddress"] + + if REMOTE_PTF_INTF in dash_info and LOCAL_PTF_INTF in dash_info[i]: + break + if len(dpuhosts) == 1: + dpuhost = dpuhosts[0] + else: + dpuhost = dpuhosts[i] + dash_info[i][DPU_DATAPLANE_PORT] = dpuhost.dpu_dataplane_port + dash_info[i][DPU_DATAPLANE_IP] = dpuhost.dpu_data_port_ip + dash_info[i][DPU_DATAPLANE_MAC] = dpuhost.dpu_dataplane_mac + + dash_info[i][NPU_DATAPLANE_PORT] = dpuhost.npu_dataplane_port + dash_info[i][NPU_DATAPLANE_IP] = dpuhost.npu_data_port_ip + dash_info[i][NPU_DATAPLANE_MAC] = dpuhost.npu_dataplane_mac + + breakpoint() + return dash_info + + +@pytest.fixture(scope="function") +def apply_config(localhost, duthost, ptfhost, skip_config, skip_cleanup): + configs = [] + op = "SET" + + def _apply_config(config_info): + if skip_config: + return + if config_info not in configs: + configs.append(config_info) + + config = "dash_basic_config" + template_name = "{}.j2".format(config) + dest_path = "/tmp/{}.json".format(config) + render_template_to_host(template_name, duthost, dest_path, config_info, op=op) + if ENABLE_GNMI_API: + apply_gnmi_file(localhost, duthost, ptfhost, dest_path) + else: + apply_swssconfig_file(duthost, dest_path) + + yield _apply_config + + op = "DEL" + if not skip_cleanup: + for config_info in reversed(configs): + _apply_config(config_info) + + +@pytest.fixture(scope="module", autouse=True) +def setup_gnmi_server(duthosts, localhost, ptfhost, skip_cert_cleanup): + if not ENABLE_GNMI_API: + yield + return + for duthost in duthosts: + generate_gnmi_cert(localhost, duthost) + apply_gnmi_cert(duthost, ptfhost) + yield + for duthost in duthosts: + recover_gnmi_cert(localhost, duthost, skip_cert_cleanup) + + +@pytest.fixture(scope="function") +def asic_db_checker(duthost): + def _check_asic_db(tables): + for table in tables: + output = duthost.shell("sonic-db-cli ASIC_DB keys 'ASIC_STATE:{}:*'".format(table)) + assert output["stdout"].strip() != "", "No entries found in ASIC_DB table {}".format(table) + yield _check_asic_db + + +@pytest.fixture(scope="function", params=['udp', 'tcp', 'echo_request', 'echo_reply']) +def inner_packet_type(request): + return request.param + + +def config_vxlan_udp_dport(duthost, port): + vxlan_port_config = [ + { + "SWITCH_TABLE:switch": {"vxlan_port": f"{port}"}, + "OP": "SET" + } + ] + config_path = "/tmp/vxlan_port_config.json" + duthost.copy(content=json.dumps(vxlan_port_config, indent=4), dest=config_path, verbose=False) + apply_swssconfig_file(duthost, config_path) + + +@pytest.fixture(scope="function") +def vxlan_udp_dport(request, duthost): + """ + Test the traffic with specified or randomly generated VxLAN UDP dst port. + Configuration is applied by swssconfig. + """ + UDP_PORT_RANGE = range(0, 65536) + WELL_KNOWN_UDP_PORT_RANGE = range(0, 1024) + vxlan_udp_dport = request.config.getoption("--vxlan_udp_dport") + if vxlan_udp_dport == "random": + port_candidate_list = ["default", 4789, 13330, 1024, 65535] + while True: + random_port = random.choice(UDP_PORT_RANGE) + if random_port not in WELL_KNOWN_UDP_PORT_RANGE and random_port not in port_candidate_list: + port_candidate_list.append(random_port) + break + vxlan_udp_dport = random.choice(port_candidate_list) + if vxlan_udp_dport != "default": + logger.info(f"Configure the VXLAN UDP dst port {vxlan_udp_dport} to DPU") + vxlan_udp_dport = int(vxlan_udp_dport) + config_vxlan_udp_dport(duthost, vxlan_udp_dport) + else: + logger.info("Use the default VXLAN UDP dst port 4789") + vxlan_udp_dport = 4789 + + yield vxlan_udp_dport + + logger.info("Restore the VXLAN UDP dst port to 4789") + config_vxlan_udp_dport(duthost, 4789) + + +@pytest.fixture(scope="function") +def set_vxlan_udp_sport_range(dpuhosts, dpu_index): + """ + Configure VXLAN UDP source port range in dpu configuration. + + """ + dpuhost = dpuhosts[dpu_index] + vxlan_sport_config = [ + { + "SWITCH_TABLE:switch": { + "vxlan_sport": VXLAN_UDP_BASE_SRC_PORT, + "vxlan_mask": VXLAN_UDP_SRC_PORT_MASK + }, + "OP": "SET" + } + ] + + logger.info(f"Setting VXLAN source port config: {vxlan_sport_config}") + config_path = "/tmp/vxlan_sport_config.json" + dpuhost.copy(content=json.dumps(vxlan_sport_config, indent=4), dest=config_path, verbose=False) + apply_swssconfig_file(dpuhost, config_path) + if 'pensando' in dpuhost.facts['asic_type']: + logger.warning("Applying Pensando DPU VXLAN sport workaround") + dpuhost.shell("pdsctl debug update device --vxlan-port 4789 --vxlan-src-ports 5120-5247") + yield + #if str(VXLAN_UDP_BASE_SRC_PORT) in dpuhost.shell("redis-cli -n 0 hget SWITCH_TABLE:switch vxlan_sport")['stdout']: + # config_reload(dpuhost, safe_reload=True, yang_validate=False) + + +@pytest.fixture(scope="module") +def dpu_index(request): + return request.config.getoption("--dpu_index") + + +@pytest.fixture(scope="module", params=[True, False], ids=["single-endpoint", "multi-endpoint"]) +def single_endpoint(request): + return request.param + + +@pytest.fixture +def dpu_setup(duthosts, dpuhosts, dpu_index, skip_config): + if skip_config: + + return + for i in range(len(duthosts)): + duthost = duthosts[i] + dpuhost = dpuhosts[i] + # explicitly add mgmt IP route so the default route doesn't disrupt SSH access + dpuhost.shell(f'ip route replace {duthost.mgmt_ip}/32 via 169.254.200.254') + intfs = dpuhost.shell("show ip int")["stdout"] + dpu_cmds = list() + if "Loopback0" not in intfs: + dpu_cmds.append("config loopback add Loopback0") + dpu_cmds.append(f"config int ip add Loopback0 {pl.APPLIANCE_VIP}/32") + + pt_require(dpuhost.npu_data_port_ip, "DPU data port IP is not set") + dpu_cmds.append(f"ip route replace default via {dpuhost.npu_data_port_ip}") + dpuhost.shell_cmds(cmds=dpu_cmds) + + +@pytest.fixture(scope="function") +def add_npu_static_routes( + duthosts, dash_pl_config, skip_config, skip_cleanup, dpu_index, dpuhosts +): + #dpuhost = dpuhosts[dpu_index] + if not skip_config: + for i in range(len(duthosts)): + duthost = duthosts[i] + dpuhost = dpuhosts[i] + + cmds = [] + vm_nexthop_ip = get_interface_ip(duthost, dash_pl_config[i][LOCAL_DUT_INTF]).ip + 1 + pe_nexthop_ip = get_interface_ip(duthost, dash_pl_config[i][REMOTE_DUT_INTF]).ip + 1 + + pt_require(vm_nexthop_ip, "VM nexthop interface does not have an IP address") + pt_require(pe_nexthop_ip, "PE nexthop interface does not have an IP address") + + cmds.append(f"ip route replace {pl.APPLIANCE_VIP}/32 via {dpuhost.dpu_data_port_ip}") + cmds.append(f"ip route replace {pl.VM1_PA}/32 via {vm_nexthop_ip}") + + cmds.append(f"ip route replace {pl.PE_PA}/32 via {pe_nexthop_ip}") + logger.info(f"Adding static routes: {cmds} on {duthost}") + duthost.shell_cmds(cmds=cmds) + + yield + + if not skip_config and not skip_cleanup: + for i in range(len(duthosts)): + duthost = duthosts[i] + dpuhost = dpuhosts[i] + + cmds = [] + vm_nexthop_ip = get_interface_ip(duthost, dash_pl_config[i][LOCAL_DUT_INTF]).ip + 1 + pe_nexthop_ip = get_interface_ip(duthost, dash_pl_config[i][REMOTE_DUT_INTF]).ip + 1 + + cmds.append(f"ip route del {pl.APPLIANCE_VIP}/32 via {dpuhost.dpu_data_port_ip}") + cmds.append(f"ip route del {pl.VM1_PA}/32 via {vm_nexthop_ip}") + cmds.append(f"ip route del {pl.PE_PA}/32 via {pe_nexthop_ip}") + logger.info(f"Removing static routes: {cmds} from {duthost}") + breakpoint() + duthost.shell_cmds(cmds=cmds) + + +@pytest.fixture(scope="function") +def setup_npu_dpu(dpu_setup, add_npu_static_routes): + yield + + diff --git a/tests/ha/constants.py b/tests/ha/constants.py new file mode 100644 index 00000000000..c4605cb8cfa --- /dev/null +++ b/tests/ha/constants.py @@ -0,0 +1,41 @@ +from os import path +_TEMPLATE_DIR = "dash/templates" +TEMPLATE_DIR = path.abspath(_TEMPLATE_DIR) +# Constants used for generating dash configs +ENI = "eni" +LOOPBACK_IP = "loopback_ip" +LOCAL_REGION_ID = "local_region_id" +VM_VNI = "vm_vni" +VNET1_VNI = "vnet1_vni" +VNET1_NAME = "vnet1_name" +VNET2_VNI = "vnet2_vni" +VNET2_NAME = "vnet2_name" +REMOTE_CA_IP = "remote_ca_ip" +LOCAL_CA_IP = "local_ca_ip" +REMOTE_PA_IP = "remote_pa_ip" +LOCAL_PA_IP = "local_pa_ip" +REMOTE_ENI_MAC = "remote_eni_mac" +LOCAL_ENI_MAC = "local_eni_mac" +DUT_MAC = "dut_mac" +REMOTE_PTF_MAC = "remote_ptf_mac" +LOCAL_PTF_MAC = "local_ptf_mac" +REMOTE_CA_PREFIX = "remote_ca_prefix" +REMOTE_PA_PREFIX = "remote_pa_prefix" +LOCAL_PTF_INTF = "local_ptf_intf" +REMOTE_PTF_INTF = "remote_ptf_intf" +LOCAL_DUT_INTF = "local_dut_intf" +REMOTE_DUT_INTF = "remote_dut_intf" +REMOTE_PTF_SEND_INTF = "remote_ptf_send_intf" +REMOTE_PTF_RECV_INTF = "remote_ptf_recv_intf" +ROUTING_ACTION = "routing_action" +ROUTING_ACTION_TYPE = "routing_action_type" +LOOKUP_OVERLAY_IP = "lookup_overlay_ip" +NPU_DATAPLANE_PORT = "npu_dataplane_port" +NPU_DATAPLANE_IP = "npu_dataplane_ip" +NPU_DATAPLANE_MAC = "npu_dataplane_mac" +DPU_DATAPLANE_PORT = "dpu_dataplane_port" +DPU_DATAPLANE_IP = "dpu_dataplane_ip" +DPU_DATAPLANE_MAC = "dpu_dataplane_mac" +# For VxLAN source UDP port range +VXLAN_UDP_BASE_SRC_PORT = 5120 +VXLAN_UDP_SRC_PORT_MASK = 7 # number of least significant bits diff --git a/tests/ha/dash_utils.py b/tests/ha/dash_utils.py new file mode 100644 index 00000000000..e1dd07f5856 --- /dev/null +++ b/tests/ha/dash_utils.py @@ -0,0 +1,131 @@ +import logging +from os import path +from time import sleep + +import ptf.packet as scapy +import ptf.testutils as testutils +import pytest + +from jinja2 import Template + +from constants import TEMPLATE_DIR + +logger = logging.getLogger(__name__) + + +def safe_open_template(template_path): + """ + Safely loads Jinja2 template from given path + + Note: + All Jinja2 templates should be accessed with this method to ensure proper garbage disposal + + Args: + template_path: String containing the location of the template file to be opened + + Returns: + A Jinja2 Template object read from the provided file + """ + + with open(template_path) as template_file: + return Template(template_file.read()) + + +def combine_dicts(*args): + """ + Combines multiple Python dictionaries into a single dictionary + + Used primarily to pass arguments contained in multiple dictionaries to the `render()` method for Jinja2 templates + + Args: + *args: The dictionaries to be combined + + Returns: + A single Python dictionary containing the key/value pairs of all the input dictionaries + """ + + combined_args = {} + + for arg in args: + combined_args.update(arg) + + return combined_args + + +def render_template_to_host(template_name, host, dest_file, *template_args, **template_kwargs): + """ + Renders a template with the given arguments and copies it to the host + + Args: + template_name: A template inside the "templates" folder (without the preceding "templates/") + host: The host device to copy the rendered template to (either a PTF or DUT host object) + dest_file: The location on the host to copy the rendered template to + *template_args: Any arguments to be passed to j2 during rendering + **template_kwargs: Any keyword arguments to be passed to j2 during rendering + """ + + combined_args = combine_dicts(*template_args) + + rendered = safe_open_template(path.join(TEMPLATE_DIR, template_name)).render(combined_args, **template_kwargs) + + host.copy(content=rendered, dest=dest_file) + + +def render_template(template_name, *template_args, **template_kwargs): + """ + Renders a template with the given arguments and copies it to the host + + Args: + template_name: A template inside the "templates" folder (without the preceding "templates/") + *template_args: Any arguments to be passed to j2 during rendering + **template_kwargs: Any keyword arguments to be passed to j2 during rendering + """ + + combined_args = combine_dicts(*template_args) + + return safe_open_template(path.join(TEMPLATE_DIR, template_name)).render(combined_args, **template_kwargs) + + +def apply_swssconfig_file(duthost, file_path): + """ + Copies config file from the DUT host to the SWSS docker and applies them with swssconfig + + Args: + duthost: DUT host object + file: Path to config file on the host + """ + logger.info("Applying config files on DUT") + file_name = path.basename(file_path) + + duthost.shell("docker cp {} swss:/{}".format(file_path, file_name)) + duthost.shell("docker exec swss sh -c \"swssconfig /{}\"".format(file_name)) + sleep(5) + + +def verify_tunnel_packets(ptfadapter, ports, exp_dpu_to_vm_pkt, tunnel_endpoint_counts): + timeout = 1 + if isinstance(ports, list): + target_ports = ports + else: + target_ports = [ports] + + result = testutils.dp_poll(ptfadapter, timeout=timeout, exp_pkt=exp_dpu_to_vm_pkt) + if isinstance(result, ptfadapter.dataplane.PollSuccess): + pkt_repr = scapy.Ether(result.packet) + if result.port in target_ports: + if pkt_repr["IP"].dst in tunnel_endpoint_counts: + tunnel_endpoint_counts[pkt_repr["IP"].dst] += 1 + logging.debug( + f"Packet sent to tunnel endpoint {pkt_repr['IP'].dst} matches:\ + \n{result.format()} \nExpected:\n{exp_dpu_to_vm_pkt}" + ) + return + else: + pytest.fail( + f"Received packet has unexpected dst IP {pkt_repr['IP'].dst}, \ + expected one of {tunnel_endpoint_counts.keys()} \ + \n{result.format()} \nExpected:\n{exp_dpu_to_vm_pkt}" + ) + else: + pytest.fail(f"Got expected packet on unexpected port {result.port}: {pkt_repr}") + pytest.fail(f"DP poll failed:\n{result.format()}") diff --git a/tests/ha/gnmi_utils.py b/tests/ha/gnmi_utils.py new file mode 100644 index 00000000000..c894c2152e5 --- /dev/null +++ b/tests/ha/gnmi_utils.py @@ -0,0 +1,459 @@ +import json +import logging +import math +import time +import uuid +from functools import lru_cache + +import proto_utils +import pytest + +logger = logging.getLogger(__name__) + + +@lru_cache(maxsize=None) +class GNMIEnvironment(object): + def __init__(self, my_duthost): + self.work_dir = "/tmp/_my_gnmi_dir/" + self.gnmi_cert_path = "/etc/sonic/telemetry/" + self.gnmi_ca_cert = "gnmiCA.pem" + self.gnmi_ca_key = "gnmiCA.key" + self.gnmi_server_cert = "gnmiserver.crt" + self.gnmi_server_key = "gnmiserver.key" + self.gnmi_client_cert = "gnmiclient.crt" + self.gnmi_client_key = "gnmiclient.key" + self.gnmi_server_start_wait_time = 30 + # self.enable_zmq = my_duthost.shell("netstat -na | grep -w 8100", module_ignore_errors=True)['rc'] == 0 + self.enable_zmq = True + cmd = "docker images | grep -w sonic-gnmi" + if my_duthost.shell(cmd, module_ignore_errors=True)['rc'] == 0: + cmd = "docker ps | grep -w gnmi" + if my_duthost.shell(cmd, module_ignore_errors=True)['rc'] == 0: + self.gnmi_config_table = "GNMI" + self.gnmi_container = "gnmi" + self.gnmi_program = "gnmi-native" + self.gnmi_port = 50052 + return + else: + pytest.fail("GNMI is not running") + cmd = "docker images | grep -w sonic-telemetry" + if my_duthost.shell(cmd, module_ignore_errors=True)['rc'] == 0: + cmd = "docker ps | grep -w telemetry" + if my_duthost.shell(cmd, module_ignore_errors=True)['rc'] == 0: + self.gnmi_config_table = "TELEMETRY" + self.gnmi_container = "telemetry" + self.gnmi_program = "telemetry" + self.gnmi_port = 50051 + return + else: + pytest.fail("Telemetry is not running") + pytest.fail("Can't find telemetry and gnmi image") + + +def create_ext_conf(ip, filename): + """ + Generate configuration for openssl + + Args: + ip: server ip address + filename: configuration file name + + Returns: + """ + text = ''' +[ req_ext ] +subjectAltName = @alt_names +[alt_names] +DNS.1 = hostname.com +IP = %s +''' % ip + with open(filename, 'w') as file: + file.write(text) + return + + +def generate_gnmi_cert(localhost, my_duthost): + """ + Args: + localhost: fixture for localhost + my_duthost: fixture for my_duthost + + Returns: + """ + env = GNMIEnvironment(my_duthost) + localhost.shell("mkdir "+env.work_dir, module_ignore_errors=True) + local_command = "openssl genrsa -out %s 2048" % (env.work_dir+env.gnmi_ca_key) + localhost.shell(local_command) + + # Create Root cert + local_command = "openssl req \ + -x509 \ + -new \ + -nodes \ + -key %s \ + -sha256 \ + -days 1825 \ + -subj '/CN=test.gnmi.sonic' \ + -out %s" % (env.work_dir+env.gnmi_ca_key, env.work_dir+env.gnmi_ca_cert) + localhost.shell(local_command) + + # Create server key + local_command = "openssl genrsa -out %s 2048" % (env.work_dir+env.gnmi_server_key) + localhost.shell(local_command) + + # Create server CSR + local_command = "openssl req \ + -new \ + -key %s \ + -subj '/CN=test.server.gnmi.sonic' \ + -out %s" % ( + env.work_dir+env.gnmi_server_key, + env.work_dir+"gnmiserver.csr") + localhost.shell(local_command) + + # Sign server certificate + create_ext_conf(my_duthost.mgmt_ip, env.work_dir+"extfile.cnf") + local_command = "openssl x509 \ + -req \ + -in %s \ + -CA %s \ + -CAkey %s \ + -CAcreateserial \ + -out %s \ + -days 825 \ + -sha256 \ + -extensions req_ext -extfile %s" % ( + env.work_dir+"gnmiserver.csr", + env.work_dir+env.gnmi_ca_cert, + env.work_dir+env.gnmi_ca_key, + env.work_dir+env.gnmi_server_cert, + env.work_dir+"extfile.cnf") + localhost.shell(local_command) + + # Create client key + local_command = "openssl genrsa -out %s 2048" % (env.work_dir+env.gnmi_client_key) + localhost.shell(local_command) + + # Create client CSR + local_command = "openssl req \ + -new \ + -key %s \ + -subj '/CN=test.client.gnmi.sonic' \ + -out %s" % ( + env.work_dir+env.gnmi_client_key, + env.work_dir+"gnmiclient.csr") + localhost.shell(local_command) + + # Sign client certificate + local_command = "openssl x509 \ + -req \ + -in %s \ + -CA %s \ + -CAkey %s \ + -CAcreateserial \ + -out %s \ + -days 825 \ + -sha256" % ( + env.work_dir+"gnmiclient.csr", + env.work_dir+env.gnmi_ca_cert, + env.work_dir+env.gnmi_ca_key, + env.work_dir+env.gnmi_client_cert) + localhost.shell(local_command) + + +def apply_gnmi_cert(my_duthost, ptfhost): + """ + Upload new certificate to DUT, and restart gnmi server with new certificate + + Args: + my_duthost: fixture for my_duthost + ptfhost: fixture to ptfhost + + Returns: + """ + env = GNMIEnvironment(my_duthost) + # Copy CA certificate and server certificate over to the DUT + my_duthost.copy(src=env.work_dir+env.gnmi_ca_cert, dest=env.gnmi_cert_path) + my_duthost.copy(src=env.work_dir+env.gnmi_server_cert, dest=env.gnmi_cert_path) + my_duthost.copy(src=env.work_dir+env.gnmi_server_key, dest=env.gnmi_cert_path) + # Copy CA certificate and client certificate over to the PTF + + my_dest = '/root/' + my_duthost.hostname + ptfhost.shell("mkdir -p {0}".format(my_dest)) + ptfhost.copy(src=env.work_dir+env.gnmi_ca_cert, dest=my_dest) + ptfhost.copy(src=env.work_dir+env.gnmi_client_cert, dest=my_dest) + ptfhost.copy(src=env.work_dir+env.gnmi_client_key, dest=my_dest) + port = env.gnmi_port + assert int(port) > 0, "Invalid GNMI port" + dut_command = "docker exec %s supervisorctl stop %s" % (env.gnmi_container, env.gnmi_program) + my_duthost.shell(dut_command) + dut_command = "docker exec %s pkill telemetry" % (env.gnmi_container) + my_duthost.shell(dut_command, module_ignore_errors=True) + dut_command = "docker exec %s bash -c " % env.gnmi_container + dut_command += "\"/usr/bin/nohup /usr/sbin/telemetry -logtostderr --port %s " % port + dut_command += "--server_crt %s%s " % (env.gnmi_cert_path, env.gnmi_server_cert) + dut_command += "--server_key %s%s " % (env.gnmi_cert_path, env.gnmi_server_key) + dut_command += "--ca_crt %s%s " % (env.gnmi_cert_path, env.gnmi_ca_cert) + if env.enable_zmq: + dut_command += " -zmq_address=tcp://127.0.0.1:8100 " + dut_command += "-gnmi_native_write=true -v=10 >/root/gnmi.log 2>&1 &\"" + my_duthost.shell(dut_command) + time.sleep(env.gnmi_server_start_wait_time) + + +def recover_gnmi_cert(localhost, my_duthost, skip_cert_cleanup): + """ + Restart gnmi server to use default certificate + + Args: + localhost: fixture for localhost + my_duthost: fixture for my_duthost + + Returns: + """ + env = GNMIEnvironment(my_duthost) + if not skip_cert_cleanup: + localhost.shell("rm -rf "+env.work_dir, module_ignore_errors=True) + dut_command = "docker exec %s supervisorctl status %s" % (env.gnmi_container, env.gnmi_program) + output = my_duthost.command(dut_command, module_ignore_errors=True)['stdout'].strip() + if 'RUNNING' in output: + return + dut_command = "docker exec %s pkill telemetry" % (env.gnmi_container) + my_duthost.shell(dut_command, module_ignore_errors=True) + dut_command = "docker exec %s supervisorctl start %s" % (env.gnmi_container, env.gnmi_program) + my_duthost.shell(dut_command) + time.sleep(env.gnmi_server_start_wait_time) + + +def gnmi_set(my_duthost, ptfhost, delete_list, update_list, replace_list): + """ + Send GNMI set request with GNMI client + + Args: + my_duthost: fixture for my_duthost + ptfhost: fixture for ptfhost + delete_list: list for delete operations + update_list: list for update operations + replace_list: list for replace operations + + Returns: + """ + env = GNMIEnvironment(my_duthost) + ip = my_duthost.mgmt_ip + port = env.gnmi_port + cmd = '/root/env-python3/bin/python /root/gnxi/gnmi_cli_py/py_gnmicli.py ' + cmd += '--timeout 30 ' + cmd += '-t %s -p %u ' % (ip, port) + cmd += '-xo sonic-db ' + cmd += '-rcert /root/%s/%s ' % (my_duthost.hostname,env.gnmi_ca_cert) + cmd += '-pkey /root/%s/%s ' % (my_duthost.hostname,env.gnmi_client_key) + cmd += '-cchain /root/%s/%s ' % (my_duthost.hostname,env.gnmi_client_cert) + if len(update_list) > 0: + cmd += '-m set-update ' + elif len(delete_list) > 0: + cmd += '-m set-delete ' + elif len(replace_list) > 0: + cmd += '-m set-replace ' + else: + logger.info("PTF GNMI no items to operate on") + return + xpath = '' + xvalue = '' + for path in delete_list: + path = path.replace('sonic-db:', '') + xpath += ' ' + path + xvalue += ' ""' + for update in update_list: + update = update.replace('sonic-db:', '') + result = update.rsplit(':', 1) + xpath += ' ' + result[0] + xvalue += ' ' + result[1] + for replace in replace_list: + replace = replace.replace('sonic-db:', '') + result = replace.rsplit(':', 1) + xpath += ' ' + result[0] + if '#' in result[1]: + xvalue += ' ""' + else: + xvalue += ' ' + result[1] + cmd += '--xpath ' + xpath + cmd += ' ' + cmd += '--value ' + xvalue + logger.info(f"PTF GNMI command: {cmd}") + output = ptfhost.shell(cmd, module_ignore_errors=True) + error = "GRPC error\n" + if error in output['stdout']: + result = output['stdout'].split(error, 1) + raise Exception("GRPC error:" + result[1]) + return + + +def gnmi_get(my_duthost, ptfhost, path_list): + """ + Send GNMI get request with GNMI client + + Args: + my_duthost: fixture for my_duthost + ptfhost: fixture for ptfhost + path_list: list for get path + + Returns: + msg_list: list for get result + """ + env = GNMIEnvironment(my_duthost) + ip = my_duthost.mgmt_ip + port = env.gnmi_port + cmd = '/root/env-python3/bin/python /root/gnxi/gnmi_cli_py/py_gnmicli.py ' + cmd += '--timeout 30 ' + cmd += '-t %s -p %u ' % (ip, port) + cmd += '-xo sonic-db ' + cmd += '-rcert /root/%s/%s ' % (my_duthost.hostname, env.gnmi_ca_cert) + cmd += '-pkey /root/%s/%s ' % (my_duthost.hostname, env.gnmi_client_key) + cmd += '-cchain /root/%s/%s ' % (my_duthost.hostname, env.gnmi_client_cert) + cmd += '--encoding 4 ' + cmd += '-m get ' + cmd += '--xpath ' + for path in path_list: + path = path.replace('sonic-db:', '') + cmd += " " + path + output = ptfhost.shell(cmd, module_ignore_errors=True) + msg = output['stdout'].replace('\\', '') + error = "GRPC error\n" + if error in msg: + result = msg.split(error, 1) + raise Exception("GRPC error:" + result[1]) + mark = 'The GetResponse is below\n' + '-'*25 + '\n' + if mark in msg: + result = msg.split(mark, 1) + msg_list = result[1].split('-'*25)[0:-1] + return [msg.strip("\n") for msg in msg_list] + else: + raise Exception("error:" + msg) + + +def apply_messages( + localhost, + my_duthost, + ptfhost, + messages, + dpu_index, + set_db=True, + wait_after_apply=5, + max_updates_in_single_cmd=1024, +): + env = GNMIEnvironment(my_duthost) + update_list = [] + delete_list = [] + for i, (key, config_dict) in enumerate(messages.items()): + message = proto_utils.parse_dash_proto(key, config_dict) + keys = key.split(":", 1) + gnmi_key = keys[0] + "[key=" + keys[1] + "]" + filename = f"update{i}" + + if set_db: + if proto_utils.ENABLE_PROTO: + path = f"/APPL_DB/dpu{dpu_index}/{gnmi_key}:$/root/{filename}" + else: + path = f"/APPL_DB/dpu{dpu_index}/{gnmi_key}:@/root/{filename}" + with open(env.work_dir + filename, "wb") as file: + file.write(message.SerializeToString()) + update_list.append(path) + else: + path = f"/APPL_DB/dpu{dpu_index}/{gnmi_key}" + delete_list.append(path) + + write_gnmi_files(localhost, my_duthost, ptfhost, env, delete_list, update_list, max_updates_in_single_cmd) + time.sleep(wait_after_apply) + + +def apply_gnmi_file(localhost, my_duthost, ptfhost, dest_path=None, config_json=None, + wait_after_apply=5, max_updates_in_single_cmd=1024, host='localhost'): + """ + Apply dash configuration with gnmi client + + Args: + my_duthost: fixture for my_duthost + ptfhost: fixture for ptfhost + dest_path: configuration file path + config_json: configuration in json + wait_after_apply: the seconds to wait after gNMI file applied + max_updates_in_single_cmd: threshold to separate the updates into multiple gnmi calls for linux command + length is limited + Returns: + """ + env = GNMIEnvironment(my_duthost) + if dest_path: + logger.info("Applying config files on DUT") + dut_command = "cat %s" % dest_path + ret = my_duthost.shell(dut_command) + assert ret["rc"] == 0, "Failed to read config file" + text = ret["stdout"] + res = json.loads(text) + elif config_json: + res = json.loads(config_json) + delete_list = [] + update_list = [] + update_cnt = 0 + for operation in res: + if operation["OP"] == "SET": + for k, v in operation.items(): + if k == "OP": + continue + logger.info("Config Json %s" % k) + update_cnt += 1 + filename = "update%u" % update_cnt + if proto_utils.ENABLE_PROTO: + message = proto_utils.parse_dash_proto(k, v) + with open(env.work_dir+filename, "wb") as file: + file.write(message.SerializeToString()) + else: + text = json.dumps(v) + with open(env.work_dir+filename, "w") as file: + file.write(text) + keys = k.split(":", 1) + k = keys[0] + "[key=" + keys[1] + "]" + if proto_utils.ENABLE_PROTO: + path = "/APPL_DB/%s/%s:$/root/%s" % (host, k, filename) + else: + path = "/APPL_DB/%s/%s:@/root/%s" % (host, k, filename) + update_list.append(path) + elif operation["OP"] == "DEL": + for k, v in operation.items(): + if k == "OP": + continue + keys = k.split(":", 1) + k = keys[0] + "[key=" + keys[1] + "]" + path = "/APPL_DB/%s/%s" % (host, k) + delete_list.append(path) + else: + logger.info("Invalid operation %s" % operation["OP"]) + write_gnmi_files(localhost, my_duthost, ptfhost, env, delete_list, update_list, max_updates_in_single_cmd) + time.sleep(wait_after_apply) + + +def write_gnmi_files(localhost, my_duthost, ptfhost, env, delete_list, update_list, max_updates_in_single_cmd): + localhost.shell(f'tar -zcvf /tmp/updates.tar.gz -C {env.work_dir} .') + ptfhost.copy(src='/tmp/updates.tar.gz', dest='~') + ptfhost.shell('tar -xf updates.tar.gz') + + def _devide_list(operation_list): + list_group = [] + for i in range(math.ceil(len(operation_list) / max_updates_in_single_cmd)): + start_index = max_updates_in_single_cmd * i + end_index = max_updates_in_single_cmd * (i + 1) + list_group.append(operation_list[start_index:end_index]) + return list_group + + if delete_list: + delete_list_group = _devide_list(delete_list) + for delete_list in delete_list_group: + gnmi_set(my_duthost, ptfhost, delete_list, [], []) + if update_list: + update_list_group = _devide_list(update_list) + for update_list in update_list_group: + gnmi_set(my_duthost, ptfhost, [], update_list, []) + + localhost.shell('rm -f /tmp/updates.tar.gz') + ptfhost.shell('rm -f updates.tar.gz') + localhost.shell(f'rm -f {env.work_dir}update*') + ptfhost.shell('rm -f update*') diff --git a/tests/ha/packets.py b/tests/ha/packets.py new file mode 100644 index 00000000000..dd55d16ff1e --- /dev/null +++ b/tests/ha/packets.py @@ -0,0 +1,544 @@ +import logging +import random +import sys +import time +from ipaddress import ip_address + +import ptf.packet as scapy +import ptf.testutils as testutils +import scapy.utils as scapy_utils +from configs import privatelink_config as pl +from constants import * # noqa: F403 +from ptf.dataplane import match_exp_pkt +from ptf.mask import Mask, MaskException +from six import StringIO + +from tests.common.helpers.assertions import pytest_assert + +logger = logging.getLogger(__name__) + + +def generate_inner_packet(packet_type, ipv6=False): + if packet_type == "udp": + if ipv6: + return testutils.simple_udpv6_packet + else: + return testutils.simple_udp_packet + elif packet_type == "tcp": + if ipv6: + return testutils.simple_tcpv6_packet + else: + return testutils.simple_tcp_packet + elif packet_type == "echo_request" or packet_type == "echo_reply": + if ipv6: + return testutils.simple_icmpv6_packet + else: + return testutils.simple_icmp_packet + + return None + + +def set_icmp_sub_type(packet, packet_type): + if packet_type == "echo_request": + packet[scapy.ICMP].type = 8 + elif packet_type == "echo_reply": + packet[scapy.ICMP].type = 0 + + +def get_bits(ip): + addr = ip_address(ip) + return int(addr) + + +def get_pl_overlay_sip(orig_sip, ol_sip, ol_mask, pl_sip_encoding, pl_sip_mask): + pkt_sip = get_bits(orig_sip) + ol_sip_ip = get_bits(ol_sip) + ol_sip_mask = get_bits(ol_mask) + pl_encoding_ip = get_bits(pl_sip_encoding) + pl_encoding_mask = get_bits(pl_sip_mask) + + overlay_sip = (((pkt_sip & ~ol_sip_mask) | ol_sip_ip) & ~pl_encoding_mask) | pl_encoding_ip + return str(ip_address(overlay_sip)) + + +def get_pl_overlay_dip(orig_dip, ol_dip, ol_mask): + pkt_dip = get_bits(orig_dip) + ol_dip_ip = get_bits(ol_dip) + ol_dip_mask = get_bits(ol_mask) + + overlay_dip = (pkt_dip & ~ol_dip_mask) | ol_dip_ip + return str(ip_address(overlay_dip)) + + +def rand_udp_port_packets(config, floating_nic=True, outbound_vni=None): + """ + Randomly generate the inner (overlay) UDP source and destination ports. + Useful to ensure an even distribution of packets across multiple ECMP endpoints. + """ + sport = random.randint(49152, 65535) + dport = random.randint(49152, 65535) + vm_to_dpu_pkt, exp_dpu_to_pe_pkt = outbound_pl_packets( + config, "vxlan", floating_nic, inner_sport=sport, inner_dport=dport, vni=outbound_vni + ) + pe_to_dpu_pkt, exp_dpu_to_vm_pkt = inbound_pl_packets(config, floating_nic, inner_sport=dport, inner_dport=sport) + return vm_to_dpu_pkt, exp_dpu_to_pe_pkt, pe_to_dpu_pkt, exp_dpu_to_vm_pkt + + +def set_do_not_care_layer(mask, layer, field_name, n=1): + """ + Zeroes out the mask for 'field' in the nth occurrence of the specified layer. + """ + header_offset = mask.size - len(mask.exp_pkt.getlayer(layer, n)) + + try: + fields_desc = [ + field + for field in layer.fields_desc + if field.name in mask.exp_pkt[layer].__class__(bytes(mask.exp_pkt[layer])).fields.keys() + ] # build & parse packet to be sure all fields are correctly filled + except Exception: # noqa + raise MaskException("Can not build or decode Packet") + + if field_name not in [x.name for x in fields_desc]: + raise MaskException("Field %s does not exist in frame" % field_name) + + field_offset = 0 + bitwidth = 0 + for f in fields_desc: + try: + bits = f.size + except Exception: # noqa + bits = 8 * f.sz + if f.name == field_name: + bitwidth = bits + break + else: + field_offset += bits + + mask.set_do_not_care(header_offset * 8 + field_offset, bitwidth) + + +def inbound_pl_packets( + config, floating_nic=False, inner_packet_type="udp", vxlan_udp_dport=4789, inner_sport=4567, inner_dport=6789, + vxlan_udp_base_src_port=VXLAN_UDP_BASE_SRC_PORT, vxlan_udp_src_port_mask=VXLAN_UDP_SRC_PORT_MASK +): + inner_sip = get_pl_overlay_dip( # not a typo, inner DIP/SIP are reversed for inbound direction + pl.PE_CA, pl.PL_OVERLAY_DIP, pl.PL_OVERLAY_DIP_MASK + ) + + inner_dip = get_pl_overlay_sip( + pl.VM1_CA, pl.PL_OVERLAY_SIP, pl.PL_OVERLAY_SIP_MASK, pl.PL_ENCODING_IP, pl.PL_ENCODING_MASK + ) + + l4_protocol_key = get_scapy_l4_protocol_key(inner_packet_type) + + inner_packet = generate_inner_packet(inner_packet_type, ipv6=True)( + eth_src=pl.REMOTE_MAC, + eth_dst=pl.ENI_MAC, + ipv6_src=inner_sip, + ipv6_dst=inner_dip, + ) + inner_packet[l4_protocol_key].sport = inner_sport + inner_packet[l4_protocol_key].dport = inner_dport + + gre_packet = testutils.simple_gre_packet( + eth_dst=config[DUT_MAC], + ip_src=pl.PE_PA, + ip_dst=pl.APPLIANCE_VIP, + gre_key_present=True, + gre_key=int(pl.ENCAP_VNI) << 8, + inner_frame=inner_packet, + ) + + exp_inner_packet = generate_inner_packet(inner_packet_type)( + eth_src=pl.ENI_MAC if floating_nic else pl.REMOTE_MAC, + eth_dst=pl.VM_MAC if floating_nic else pl.ENI_MAC, + ip_src=pl.PE_CA, + ip_dst=pl.VM1_CA, + ip_id=0, + ) + + l4_protocol_key = get_scapy_l4_protocol_key(inner_packet_type) + exp_inner_packet[l4_protocol_key] = inner_packet[l4_protocol_key] + + exp_vxlan_packet = testutils.simple_vxlan_packet( + eth_src=config[DUT_MAC], + eth_dst=config[LOCAL_PTF_MAC], + ip_src=pl.APPLIANCE_VIP, + ip_dst=pl.VM1_PA, + ip_ttl=254, + ip_id=0, + udp_dport=vxlan_udp_dport, + udp_sport=vxlan_udp_base_src_port, + vxlan_vni=pl.ENCAP_VNI if floating_nic else int(pl.VNET1_VNI), + inner_frame=exp_inner_packet, + ) + + masked_exp_packet = Mask(exp_vxlan_packet) + masked_exp_packet.set_do_not_care_packet(scapy.Ether, "src") + masked_exp_packet.set_do_not_care_packet(scapy.Ether, "dst") + masked_exp_packet.set_do_not_care_packet(scapy.UDP, "chksum") + masked_exp_packet.set_do_not_care(8 * (34 + 2) - vxlan_udp_src_port_mask, vxlan_udp_src_port_mask) + masked_exp_packet.set_do_not_care_packet(scapy.IP, "ttl") + masked_exp_packet.set_do_not_care_packet(scapy.IP, "chksum") + if floating_nic: + # As destination IP is not fixed in case of return path ECMP, + # we need to mask the checksum and destination IP + masked_exp_packet.set_do_not_care_packet(scapy.IP, "dst") + masked_exp_packet.set_do_not_care(400, 48) # Inner dst MAC + + return gre_packet, masked_exp_packet + + +def plnsg_packets(config): + vm_to_dpu_pkt, exp_dpu_to_pe_pkt = outbound_pl_packets(config, "vxlan") + inner_pkt = exp_dpu_to_pe_pkt.exp_pkt + inner_pkt[scapy.Ether].src = config[DPU_DATAPLANE_MAC] + inner_pkt[scapy.Ether].dst = config[NPU_DATAPLANE_MAC] + exp_outer_pkt = testutils.simple_vxlan_packet( + eth_src=config[DUT_MAC], + eth_dst=config[REMOTE_PTF_MAC], + ip_src=pl.APPLIANCE_VIP, + ip_id=0, + udp_dport=4789, + udp_sport=1234, + with_udp_chksum=False, + vxlan_vni=pl.NSG_OUTBOUND_VNI, + inner_frame=inner_pkt, + ) + masked_outer_pkt = Mask(exp_outer_pkt) + masked_outer_pkt.set_do_not_care_packet(scapy.UDP, "sport") + masked_outer_pkt.set_do_not_care_packet(scapy.IP, "chksum") + masked_outer_pkt.set_do_not_care_packet(scapy.IP, "dst") + masked_outer_pkt.set_do_not_care_packet(scapy.IP, "ttl") + set_do_not_care_layer(masked_outer_pkt, scapy.IP, "ttl", 2) + set_do_not_care_layer(masked_outer_pkt, scapy.IP, "chksum", 2) + return vm_to_dpu_pkt, masked_outer_pkt + + +def outbound_pl_packets( + config, + outer_encap, + floating_nic=False, + inner_packet_type="udp", + vxlan_udp_dport=4789, + vxlan_udp_sport=random.randint( + VXLAN_UDP_BASE_SRC_PORT, + VXLAN_UDP_BASE_SRC_PORT + 2**VXLAN_UDP_SRC_PORT_MASK - 1), + inner_sport=6789, + inner_dport=4567, + vni=None +): + outer_vni = int(vni if vni else pl.VM_VNI) + + l4_protocol_key = get_scapy_l4_protocol_key(inner_packet_type) + + inner_packet = generate_inner_packet(inner_packet_type)( + eth_src=pl.VM_MAC if floating_nic else pl.ENI_MAC, + eth_dst=pl.ENI_MAC if floating_nic else pl.REMOTE_MAC, + ip_src=pl.VM1_CA, + ip_dst=pl.PE_CA, + ) + inner_packet[l4_protocol_key].sport = inner_sport + inner_packet[l4_protocol_key].dport = inner_dport + + if outer_encap == "vxlan": + outer_packet = testutils.simple_vxlan_packet( + eth_src=config[LOCAL_PTF_MAC], + eth_dst=config[DUT_MAC], + ip_src=pl.VM1_PA, + ip_dst=pl.APPLIANCE_VIP, + udp_dport=vxlan_udp_dport, + udp_sport=vxlan_udp_sport, + with_udp_chksum=False, + vxlan_vni=outer_vni if floating_nic else int(pl.VNET1_VNI), + inner_frame=inner_packet, + ) + elif outer_encap == "gre": + outer_packet = testutils.simple_gre_packet( + eth_src=config[LOCAL_PTF_MAC], + eth_dst=config[DUT_MAC], + ip_src=pl.VM1_PA, + ip_dst=pl.APPLIANCE_VIP, + gre_key_present=True, + gre_key=(outer_vni << 8) if floating_nic else (int(pl.VNET1_VNI) << 8), + inner_frame=inner_packet, + ) + else: + logger.error(f"Unsupported encap type: {outer_encap}") + return None + + exp_overlay_sip = get_pl_overlay_sip( + inner_packet[scapy.IP].src, pl.PL_OVERLAY_SIP, pl.PL_OVERLAY_SIP_MASK, pl.PL_ENCODING_IP, pl.PL_ENCODING_MASK + ) + + exp_overlay_dip = get_pl_overlay_dip(inner_packet[scapy.IP].dst, pl.PL_OVERLAY_DIP, pl.PL_OVERLAY_DIP_MASK) + + logger.debug(f"Expecting overlay SIP: {exp_overlay_sip}") + logger.debug(f"Expecting overlay DIP: {exp_overlay_dip}") + + if inner_packet_type == 'tcp': + exp_inner_packet = scapy.Ether() / scapy.IPv6() / scapy.TCP() + else: + exp_inner_packet = scapy.Ether() / scapy.IPv6() / scapy.UDP() + exp_inner_packet[scapy.Ether].src = pl.ENI_MAC + exp_inner_packet[scapy.Ether].dst = pl.REMOTE_MAC + exp_inner_packet[scapy.IPv6].src = exp_overlay_sip + exp_inner_packet[scapy.IPv6].dst = exp_overlay_dip + + exp_inner_packet[l4_protocol_key] = inner_packet[l4_protocol_key] + + exp_encap_packet = testutils.simple_gre_packet( + eth_dst=config[REMOTE_PTF_MAC], + eth_src=config[DUT_MAC], + ip_src=pl.APPLIANCE_VIP, + ip_dst=pl.PE_PA, + gre_key_present=True, + gre_key=pl.ENCAP_VNI << 8, + inner_frame=exp_inner_packet, + ip_id=0, + ) + + masked_exp_packet = Mask(exp_encap_packet) + masked_exp_packet.set_do_not_care_packet(scapy.Ether, "src") + masked_exp_packet.set_do_not_care_packet(scapy.Ether, "dst") + masked_exp_packet.set_do_not_care_packet(scapy.IP, "chksum") + masked_exp_packet.set_do_not_care_packet(scapy.IP, "ttl") # behavior differs between Cisco and Nvidia platforms + masked_exp_packet.set_do_not_care(336, 48) # Inner Ether dst + + return outer_packet, masked_exp_packet + + +def inbound_vnet_packets(dash_config_info, inner_extra_conf={}, inner_packet_type="udp", vxlan_udp_dport=4789): + inner_packet = generate_inner_packet(inner_packet_type)( + eth_src=dash_config_info[REMOTE_ENI_MAC], + eth_dst=dash_config_info[LOCAL_ENI_MAC], + ip_src=dash_config_info[REMOTE_CA_IP], + ip_dst=dash_config_info[LOCAL_CA_IP], + **inner_extra_conf, + ) + set_icmp_sub_type(inner_packet, inner_packet_type) + pa_match_vxlan_packet = testutils.simple_vxlan_packet( + eth_src=dash_config_info[REMOTE_PTF_MAC], + eth_dst=dash_config_info[DUT_MAC], + ip_src=dash_config_info[REMOTE_PA_IP], + ip_dst=dash_config_info[LOOPBACK_IP], + udp_dport=vxlan_udp_dport, + vxlan_vni=dash_config_info[VNET2_VNI], + ip_ttl=64, + inner_frame=inner_packet, + ) + expected_packet = testutils.simple_vxlan_packet( + eth_src=dash_config_info[DUT_MAC], + eth_dst=dash_config_info[LOCAL_PTF_MAC], + ip_src=dash_config_info[LOOPBACK_IP], + ip_dst=dash_config_info[LOCAL_PA_IP], + udp_dport=vxlan_udp_dport, + vxlan_vni=dash_config_info[VM_VNI], + ip_ttl=255, + ip_id=0, + inner_frame=inner_packet, + ) + + pa_mismatch_vxlan_packet = pa_match_vxlan_packet.copy() + remote_pa_ip = ip_address(dash_config_info[REMOTE_PA_IP]) + pa_mismatch_vxlan_packet["IP"].src = str(remote_pa_ip + 1) + + masked_exp_packet = Mask(expected_packet) + masked_exp_packet.set_do_not_care_scapy(scapy.IP, "id") + masked_exp_packet.set_do_not_care_scapy(scapy.IP, "chksum") + masked_exp_packet.set_do_not_care_scapy(scapy.UDP, "sport") + masked_exp_packet.set_do_not_care_scapy(scapy.UDP, "chksum") + + return inner_packet, pa_match_vxlan_packet, pa_mismatch_vxlan_packet, masked_exp_packet + + +def outbound_vnet_packets(dash_config_info, inner_extra_conf={}, inner_packet_type="udp", vxlan_udp_dport=4789): + proto = None + if "proto" in inner_extra_conf: + proto = int(inner_extra_conf["proto"]) + del inner_extra_conf["proto"] + + inner_packet = generate_inner_packet(inner_packet_type)( + eth_src=dash_config_info[LOCAL_ENI_MAC], + eth_dst=dash_config_info[REMOTE_ENI_MAC], + ip_src=dash_config_info[LOCAL_CA_IP], + ip_dst=dash_config_info[REMOTE_CA_IP], + **inner_extra_conf, + ) + set_icmp_sub_type(inner_packet, inner_packet_type) + + if proto: + inner_packet[scapy.IP].proto = proto + + vxlan_packet = testutils.simple_vxlan_packet( + eth_src=dash_config_info[LOCAL_PTF_MAC], + eth_dst=dash_config_info[DUT_MAC], + ip_src=dash_config_info[LOCAL_PA_IP], + ip_dst=dash_config_info[LOOPBACK_IP], + udp_dport=vxlan_udp_dport, + with_udp_chksum=False, + vxlan_vni=dash_config_info[VM_VNI], + ip_ttl=64, + inner_frame=inner_packet, + ) + expected_packet = testutils.simple_vxlan_packet( + eth_src=dash_config_info[DUT_MAC], + eth_dst=dash_config_info[REMOTE_PTF_MAC], + ip_src=dash_config_info[LOOPBACK_IP], + ip_dst=dash_config_info[REMOTE_PA_IP], + udp_dport=vxlan_udp_dport, + vxlan_vni=dash_config_info[VNET2_VNI], + # TODO: Change TTL to 63 after SAI bug is fixed + ip_ttl=0xFF, + ip_id=0, + inner_frame=inner_packet, + ) + + masked_exp_packet = Mask(expected_packet) + masked_exp_packet.set_do_not_care_scapy(scapy.IP, "id") + masked_exp_packet.set_do_not_care_scapy(scapy.IP, "chksum") + masked_exp_packet.set_do_not_care_scapy(scapy.UDP, "sport") + masked_exp_packet.set_do_not_care_scapy(scapy.UDP, "chksum") + return inner_packet, vxlan_packet, masked_exp_packet + + +def get_packets_on_specified_ports(ptfadapter, ports, filter_pkt_lens, device_number=0, duration=3, timeout=0.2): + """ + Get the packets on the specified ports and device for the specified duration + """ + logging.info("Get pkts on device %d, port %r", device_number, ports) + + received_pkts_res = {} + start_time = time.time() + while (time.time() - start_time) < duration: + result = testutils.dp_poll(ptfadapter, device_number=device_number, timeout=timeout) + logger.info(result) + if isinstance(result, ptfadapter.dataplane.PollSuccess) and result.port in ports: + if len(result.packet) in filter_pkt_lens: + if result.port in received_pkts_res: + received_pkts_res[result.port].append(result) + else: + received_pkts_res[result.port] = [result] + return received_pkts_res + + +def verify_each_packet_on_each_port(exp_pkts, received_pkts_res, ports): + """ + Verify each packet can be received on the corresponding port + """ + logger.info(f"Checking pkts on ports :{ports}") + for port, exp_pkt in zip(ports, exp_pkts): + if port in received_pkts_res: + find_matched_ptk = False + for pkt_res in received_pkts_res[port]: + if match_exp_pkt(exp_pkt, pkt_res.packet): + find_matched_ptk = True + logger.info(f"find the matched packet on port {port}") + break + if not find_matched_ptk: + logger.error( + print_expect_packet_and_received_packet_hex_information([exp_pkt], received_pkts_res[port]) + ) + pytest_assert(False, f"Not find the matched pkt on port {port}") + else: + pytest_assert(False, f"port {port} doesn't receive any packet") + return True + + +def verify_packets_not_received(unexp_pkts, received_pkts_res): + """ + Verify packets are not received + """ + for unexp_pkt in unexp_pkts: + for _, received_pkts in received_pkts_res.items(): + for pkt_res in received_pkts: + if match_exp_pkt(unexp_pkt, pkt_res.packet): + logger.error(print_expect_packet_and_received_packet_hex_information([unexp_pkt], received_pkts)) + pytest_assert(False, f" unexpected packet are received on port {pkt_res.port}") + return True + + +def print_expect_packet_and_received_packet_hex_information(exp_pkts, received_pkts_res): + try: + stdout_save = sys.stdout + # The scapy packet dissection methods print directly to stdout, + # so we have to redirect stdout to a string. + sys.stdout = StringIO() + + for exp_pkt in exp_pkts: + print("Expected pkt:") + scapy_utils.hexdump(exp_pkt.exp_pkt) + print("Expected pkt mask:") + scapy_utils.hexdump(exp_pkt.mask) + + print("==============================") + for pkt_res in received_pkts_res: + print("Receive pkt:") + scapy_utils.hexdump(pkt_res.packet) + + return sys.stdout.getvalue() + finally: + sys.stdout.close() + sys.stdout = stdout_save # Restore the original stdout. + + +def verify_tcp_packet_drop_rst_packet_sent( + ptfadapter, exp_rst_pkts, drop_tcp_pkts, ports, filter_pkt_lens, device_number=0, duration=10, timeout=0.2 +): + received_pkts_res = get_packets_on_specified_ports( + ptfadapter, ports, filter_pkt_lens, device_number, duration, timeout + ) + verify_packets_not_received(drop_tcp_pkts, received_pkts_res) + verify_each_packet_on_each_port(exp_rst_pkts, received_pkts_res, ports) + + +def outbound_smartswitch_vnet_packets( + dash_config_info, inner_extra_conf={}, inner_packet_type="udp", vxlan_udp_dport=4789 +): + + inner_packet = generate_inner_packet(inner_packet_type)( + eth_src=dash_config_info[LOCAL_ENI_MAC], + eth_dst=dash_config_info[REMOTE_ENI_MAC], + ip_src=dash_config_info[LOCAL_CA_IP], + ip_dst=dash_config_info[REMOTE_CA_IP], + ) + set_icmp_sub_type(inner_packet, inner_packet_type) + + vxlan_packet = testutils.simple_vxlan_packet( + eth_src=dash_config_info[LOCAL_PTF_MAC], + eth_dst=dash_config_info[DUT_MAC], + ip_src=dash_config_info[LOCAL_PA_IP], + ip_dst=dash_config_info[LOOPBACK_IP], + udp_dport=vxlan_udp_dport, + udp_sport=VXLAN_UDP_BASE_SRC_PORT, + with_udp_chksum=False, + vxlan_vni=dash_config_info[VNET1_VNI], + ip_ttl=64, + inner_frame=inner_packet, + ) + expected_packet = testutils.simple_vxlan_packet( + eth_src=dash_config_info[DUT_MAC], + eth_dst=dash_config_info[REMOTE_PTF_MAC], + ip_src=dash_config_info[LOOPBACK_IP], + ip_dst=dash_config_info[REMOTE_PA_IP], + udp_dport=vxlan_udp_dport, + vxlan_vni=dash_config_info[VNET1_VNI], + ip_ttl=63, + ip_id=0, + inner_frame=inner_packet, + ) + + masked_exp_packet = Mask(expected_packet) + masked_exp_packet.set_do_not_care_scapy(scapy.IP, "id") + masked_exp_packet.set_do_not_care_scapy(scapy.IP, "chksum") + masked_exp_packet.set_do_not_care_scapy(scapy.UDP, "sport") + masked_exp_packet.set_do_not_care_scapy(scapy.UDP, "chksum") + return inner_packet, vxlan_packet, masked_exp_packet + + +def get_scapy_l4_protocol_key(inner_packet_type): + scapy_tcp = scapy.TCP + scapy_udp = scapy.UDP + l4_protocol_key = scapy_udp if inner_packet_type == 'udp' else scapy_tcp + return l4_protocol_key diff --git a/tests/ha/proto_utils.py b/tests/ha/proto_utils.py new file mode 100644 index 00000000000..718bbea2233 --- /dev/null +++ b/tests/ha/proto_utils.py @@ -0,0 +1,350 @@ +import base64 +import re +import socket +import uuid +import importlib +from ipaddress import ip_address + +from dash_api.appliance_pb2 import Appliance +from dash_api.eni_pb2 import Eni, State # noqa: F401 +from dash_api.eni_route_pb2 import EniRoute +from dash_api.route_group_pb2 import RouteGroup +from dash_api.route_pb2 import Route +from dash_api.route_type_pb2 import ActionType, RouteType, RouteTypeItem, EncapType, RoutingType # noqa: F401 +from dash_api.vnet_mapping_pb2 import VnetMapping +from dash_api.vnet_pb2 import Vnet +from dash_api.meter_policy_pb2 import MeterPolicy +from dash_api.meter_rule_pb2 import MeterRule +from dash_api.tunnel_pb2 import Tunnel +from dash_api.route_rule_pb2 import RouteRule +import dash_api.types_pb2 as types_pb2 + +from google.protobuf.descriptor import FieldDescriptor +from google.protobuf.json_format import ParseDict + +ENABLE_PROTO = True + +PB_INT_TYPES = set([ + FieldDescriptor.TYPE_INT32, + FieldDescriptor.TYPE_INT64, + FieldDescriptor.TYPE_UINT32, + FieldDescriptor.TYPE_UINT64, + FieldDescriptor.TYPE_FIXED64, + FieldDescriptor.TYPE_FIXED32, + FieldDescriptor.TYPE_SFIXED32, + FieldDescriptor.TYPE_SFIXED64, + FieldDescriptor.TYPE_SINT32, + FieldDescriptor.TYPE_SINT64 +]) + +PB_CLASS_MAP = { + "APPLIANCE": Appliance, + "VNET": Vnet, + "ENI": Eni, + "VNET_MAPPING": VnetMapping, + "ROUTE": Route, + "ROUTING_TYPE": RouteType, + "ROUTE_GROUP": RouteGroup, + "ENI_ROUTE": EniRoute, + "METER_POLICY": MeterPolicy, + "METER_RULE": MeterRule, + "TUNNEL": Tunnel, + "ROUTE_RULE": RouteRule +} + +PB_TYPE_MAP = { + "HA_SCOPE": types_pb2.HaScope, + "HA_OWNER": types_pb2.HaOwner, + "IP_VERSION": types_pb2.IpVersion, + "IP_ADDRESS": types_pb2.IpAddress, + "IP_PREFIX": types_pb2.IpPrefix, + "VALUE_OR_RANGE": types_pb2.ValueOrRange, + "RANGE": types_pb2.Range, +} + + +def parse_ip_address(ip_str): + ip_addr = ip_address(ip_str) + if ip_addr.version == 4: + encoded_val = socket.htonl(int(ip_addr)) + else: + encoded_val = base64.b64encode(ip_addr.packed) + + return {f"ipv{ip_addr.version}": encoded_val} + + +def parse_byte_field(orig_val): + return base64.b64encode(bytes.fromhex(orig_val.replace(":", ""))) + + +def parse_guid(guid_str): + return {"value": parse_byte_field(uuid.UUID(guid_str).hex)} + + +def parse_value_or_range(orig): + if isinstance(orig, list): + if len(orig) == 1: + val = int(orig[0]) + return {"value": val} + elif len(orig) == 2: + min = int(orig[0]) + max = int(orig[1]) + return {"range": {"min": min, "max": max}} + else: + val = int(orig) + return {"value": val} + + +def parse_dash_proto(key: str, proto_dict: dict): + """ + Custom parser for DASH configs to allow writing configs + in a more human-readable format + """ + table_name = re.search(r"DASH_(\w+)_TABLE", key).group(1) + message = PB_CLASS_MAP[table_name]() + field_map = message.DESCRIPTOR.fields_by_name + + if table_name == "ROUTING_TYPE": + pb = routing_type_from_json(proto_dict) + return pb + + new_dict = {} + for key, value in proto_dict.items(): + if field_map[key].type == field_map[key].TYPE_MESSAGE: + + if field_map[key].message_type.name == "IpAddress": + if field_map[key].label == FieldDescriptor.LABEL_REPEATED: + new_dict[key] = [parse_ip_address(val) for val in value] + else: + new_dict[key] = parse_ip_address(value) + elif field_map[key].message_type.name == "IpPrefix": + new_dict[key] = parse_ip_prefix(value) + elif field_map[key].message_type.name == "Guid": + new_dict[key] = parse_guid(value) + elif field_map[key].message_type.name == "ValueOrRange": + new_dict[key] = parse_value_or_range(value) + + elif field_map[key].type == field_map[key].TYPE_BYTES: + new_dict[key] = parse_byte_field(value) + + elif field_map[key].type == field_map[key].TYPE_ENUM: + if isinstance(value, int): + new_dict[key] = value + else: + new_dict[key] = get_enum_type_from_str(field_map[key].enum_type.name, value) + + elif field_map[key].type in PB_INT_TYPES: + new_dict[key] = int(value) + + if key not in new_dict: + new_dict[key] = value + + return ParseDict(new_dict, message) + + +def get_enum_type_from_str(enum_type_str, enum_name_str): + # Special-case: enum value does not match standard naming + if enum_name_str == "4_to_6": + return ActionType.ACTION_TYPE_4_TO_6 + + # Convert enum type name → ENUM_PREFIX + # Example: HaScope → HA_SCOPE + my_enum_type_parts = re.findall(r'[A-Z][^A-Z]*', enum_type_str) + enum_prefix = '_'.join(my_enum_type_parts).upper() + + enum_class = PB_TYPE_MAP.get(enum_prefix) + if not enum_class: + raise Exception(f"Cannot find enum type {enum_prefix} in PB_TYPE_MAP") + + # Normalize enum value + # Example: "dpu" → HA_SCOPE_DPU + enum_value = enum_name_str.upper() + if not enum_value.startswith(enum_prefix): + enum_value = f"{enum_prefix}_{enum_value}" + + return enum_class.Value(enum_value) + + +def routing_type_from_json(json_obj): + pb = RouteType() + route_type_items = json_obj['items'] + for item in route_type_items: + pbi = RouteTypeItem() + pbi.action_name = item["action_name"] + if isinstance(item.get("action_type"), int): + pbi.action_type = item.get("action_type") + else: + pbi.action_type = get_enum_type_from_str('ActionType', item.get("action_type")) + if item.get("encap_type") is not None: + if isinstance(item.get("encap_type"), int): + pbi.encap_type = item.get("encap_type") + else: + pbi.encap_type = get_enum_type_from_str('EncapType', item.get("encap_type")) + if item.get("vni") is not None: + pbi.vni = int(item["vni"]) + pb.items.append(pbi) + return pb + + +def get_message_from_table_name(table_name): + table_name_lis = table_name.lower().split("_") + table_name_lis2 = [item.capitalize() for item in table_name_lis] + message_name = ''.join(table_name_lis2) + module_name = f'dash_api.{table_name.lower()}_pb2' + + # Import the module dynamically + module = importlib.import_module(module_name) + + # Get the class object + message_class = getattr(module, message_name) + + return message_class() + + +def prefix_to_ipv4(prefix_length): + if int(prefix_length) > 32: + return "" + mask = 2**32 - 2**(32-int(prefix_length)) + s = str(hex(mask)) + s = s[2:] + hex_groups = [s[i:i+2] for i in range(0, len(s), 2)] + decimal_groups = [] + for hex_string in hex_groups: + decimal_groups.append(str(int(hex_string, 16))) + ipv4_address_str = '.'.join(decimal_groups) + return ipv4_address_str + + +def prefix_to_ipv6(prefix_length): + if int(prefix_length) > 128: + return "" + mask = 2**128 - 2**(128-int(prefix_length)) + s = str(hex(mask)) + s = s[2:] + hex_groups = [s[i:i+4] for i in range(0, len(s), 4)] + ipv6_address_str = ':'.join(hex_groups) + return ipv6_address_str + + +def parse_ip_prefix(ip_prefix_str): + ip_addr_str, mask = ip_prefix_str.split("/") + if mask.isdigit(): + ip_addr = ip_address(ip_addr_str) + if ip_addr.version == 4: + mask_str = prefix_to_ipv4(mask) + else: + mask_str = prefix_to_ipv6(mask) + else: + mask_str = mask + return {"ip": parse_ip_address(ip_addr_str), "mask": parse_ip_address(mask_str)} + + +def json_to_proto(key: str, proto_dict: dict): + """ + Custom parser for DASH configs to allow writing configs + in a more human-readable format. + Converts JSON → protobuf bytes. + """ + + table_name = re.search(r"DASH_(\w+)_TABLE", key).group(1) + + # Special handler for ROUTING_TYPE + if table_name == "ROUTING_TYPE": + pb = routing_type_from_json(proto_dict) + return pb.SerializeToString() + + # Load the PB message type + message = get_message_from_table_name(table_name) + field_map = message.DESCRIPTOR.fields_by_name + new_dict = {} + + for field_name, value in proto_dict.items(): + field = field_map[field_name] + + # ============================================================ + # MESSAGE FIELDS (IpAddress, IpPrefix, Guid, etc.) + # ============================================================ + if field.type == field.TYPE_MESSAGE: + + # REPEATED message + if field.label == field.LABEL_REPEATED: + new_list = [] + for item in value: + if field.message_type.name == "IpAddress": + new_list.append(parse_ip_address(item)) + elif field.message_type.name == "IpPrefix": + new_list.append(parse_ip_prefix(item)) + elif field.message_type.name == "Guid": + new_list.append(parse_guid(item)) + else: + new_list.append(item) + new_dict[field_name] = new_list + continue + + # SINGLE message field + if field.message_type.name == "IpAddress": + new_dict[field_name] = parse_ip_address(value) + elif field.message_type.name == "IpPrefix": + new_dict[field_name] = parse_ip_prefix(value) + elif field.message_type.name == "Guid": + new_dict[field_name] = parse_guid(value) + else: + new_dict[field_name] = value + + continue + + # ============================================================ + # ENUM FIELDS (HaScope, HaRole, HaOwner, etc.) + # ============================================================ + elif field.type == field.TYPE_ENUM: + enum_type = field.enum_type.name # Example: "HaScope" + + # Convert CamelCase → HA_SCOPE + parts = re.findall(r'[A-Z][a-z]*', enum_type) + enum_prefix = '_'.join(parts).upper() # HaScope → HA_SCOPE + + # Normalize user-supplied value (e.g. "dpu" or "HA_SCOPE_DPU") + value_upper = value.upper() + if value_upper.startswith(enum_prefix): + enum_name = value_upper + else: + enum_name = f"{enum_prefix}_{value_upper}" + + # Lookup enum class from unified PB_TYPE_MAP + enum_class = PB_TYPE_MAP.get(enum_prefix) + if enum_class is None: + raise KeyError(f"Enum type '{enum_type}' ({enum_prefix}) not found in PB_TYPE_MAP") + + # Convert string to enum integer + new_dict[field_name] = enum_class.Value(enum_name) + continue + + # ============================================================ + # BOOL + # ============================================================ + if field.type == field.TYPE_BOOL: + new_dict[field_name] = bool(value) + continue + + # ============================================================ + # BYTES + # ============================================================ + if field.type == field.TYPE_BYTES: + new_dict[field_name] = parse_byte_field(value) + continue + + # ============================================================ + # INT + # ============================================================ + if field.type in PB_INT_TYPES: + new_dict[field_name] = int(value) + continue + + # ============================================================ + # STRING / DEFAULT + # ============================================================ + new_dict[field_name] = value + + # Build protobuf + pb = ParseDict(new_dict, message) + return pb.SerializeToString() diff --git a/tests/ha/test_dash_privatelink.py b/tests/ha/test_dash_privatelink.py new file mode 100644 index 00000000000..7e449f31497 --- /dev/null +++ b/tests/ha/test_dash_privatelink.py @@ -0,0 +1,102 @@ +import logging + +import configs.privatelink_config as pl +import ptf.testutils as testutils +import pytest +import random +import ptf.packet as scapy +from constants import LOCAL_PTF_INTF, REMOTE_PTF_RECV_INTF, REMOTE_PTF_SEND_INTF +from gnmi_utils import apply_messages +from packets import outbound_pl_packets, inbound_pl_packets +from tests.common.config_reload import config_reload + +logger = logging.getLogger(__name__) + +pytestmark = [ + pytest.mark.topology('t1-smartswitch-ha'), + pytest.mark.skip_check_dut_health +] + + +""" +Test prerequisites: +- Assign IPs to DPU-NPU dataplane interfaces +""" + + +@pytest.fixture(autouse=True, scope="function") +def common_setup_teardown( + localhost, + duthosts, + ptfhost, + dpu_index, + skip_config, + dpuhosts, + set_vxlan_udp_sport_range, + setup_npu_dpu # noqa: F811 +): + if skip_config: + return + breakpoint() + for i in range(len(duthosts)): + duthost = duthosts[i] + dpuhost = dpuhosts[i] + base_config_messages = { + **pl.APPLIANCE_CONFIG, + **pl.ROUTING_TYPE_PL_CONFIG, + **pl.VNET_CONFIG, + **pl.ROUTE_GROUP1_CONFIG, + **pl.METER_POLICY_V4_CONFIG + } + logger.info(f"configure on {duthost.hostname} dpu {dpuhost.dpu_index} {base_config_messages}") + + breakpoint() + apply_messages(localhost, duthost, ptfhost, base_config_messages, dpuhost.dpu_index) + + route_and_mapping_messages = { + **pl.PE_VNET_MAPPING_CONFIG, + **pl.PE_SUBNET_ROUTE_CONFIG, + **pl.VM_SUBNET_ROUTE_CONFIG + } + logger.info(route_and_mapping_messages) + apply_messages(localhost, duthost, ptfhost, route_and_mapping_messages, dpuhost.dpu_index) + + meter_rule_messages = { + **pl.METER_RULE1_V4_CONFIG, + **pl.METER_RULE2_V4_CONFIG, + } + logger.info(meter_rule_messages) + apply_messages(localhost, duthost, ptfhost, meter_rule_messages, dpuhost.dpu_index) + + logger.info(pl.ENI_CONFIG) + apply_messages(localhost, duthost, ptfhost, pl.ENI_CONFIG, dpuhost.dpu_index) + + logger.info(pl.ENI_ROUTE_GROUP1_CONFIG) + apply_messages(localhost, duthost, ptfhost, pl.ENI_ROUTE_GROUP1_CONFIG, dpuhost.dpu_index) + + yield + + config_reload(dpuhost, safe_reload=True, yang_validate=False) + # apply_messages(localhost, duthost, ptfhost, pl.ENI_ROUTE_GROUP1_CONFIG, dpuhost.dpu_index, False) + # apply_messages(localhost, duthost, ptfhost, pl.ENI_CONFIG, dpuhost.dpu_index, False) + # apply_messages(localhost, duthost, ptfhost, meter_rule_messages, dpuhost.dpu_index, False) + # apply_messages(localhost, duthost, ptfhost, route_and_mapping_messages, dpuhost.dpu_index, False) + # apply_messages(localhost, duthost, ptfhost, base_config_messages, dpuhost.dpu_index, False) + + +@pytest.mark.parametrize("encap_proto", ["vxlan", "gre"]) +def test_privatelink_basic_transform( + ptfadapter, + dash_pl_config, + encap_proto +): + vm_to_dpu_pkt, exp_dpu_to_pe_pkt = outbound_pl_packets(dash_pl_config[0], encap_proto) + pe_to_dpu_pkt, exp_dpu_to_vm_pkt = inbound_pl_packets(dash_pl_config[0]) + + breakpoint() + ptfadapter.dataplane.flush() + testutils.send(ptfadapter, dash_pl_config[0][LOCAL_PTF_INTF], vm_to_dpu_pkt, 1) + testutils.verify_packet_any_port(ptfadapter, exp_dpu_to_pe_pkt, dash_pl_config[0][REMOTE_PTF_RECV_INTF]) + testutils.send(ptfadapter, dash_pl_config[0][REMOTE_PTF_SEND_INTF], pe_to_dpu_pkt, 1) + testutils.verify_packet(ptfadapter, exp_dpu_to_vm_pkt, dash_pl_config[0][LOCAL_PTF_INTF]) + diff --git a/tests/ha/test_ha_dash_privatelink.py b/tests/ha/test_ha_dash_privatelink.py new file mode 100644 index 00000000000..6b96746cabc --- /dev/null +++ b/tests/ha/test_ha_dash_privatelink.py @@ -0,0 +1,102 @@ +import logging + +import configs.privatelink_config as pl +import ptf.testutils as testutils +import pytest +import random +import ptf.packet as scapy +from constants import LOCAL_PTF_INTF, REMOTE_PTF_RECV_INTF, REMOTE_PTF_SEND_INTF +from gnmi_utils import apply_messages +from packets import outbound_pl_packets, inbound_pl_packets +from tests.common.config_reload import config_reload + +logger = logging.getLogger(__name__) + +pytestmark = [ + pytest.mark.topology('t1-smartswitch-ha'), + pytest.mark.skip_check_dut_health +] + +""" +Test prerequisites: +- Assign IPs to DPU-NPU dataplane interfaces +""" + + +@pytest.fixture(autouse=True, scope="function") +def common_setup_teardown( + localhost, + duthosts, + ptfhost, + dpu_index, + skip_config, + dpuhosts, + setup_ha_config, + program_ha_set_and_initial_ha_scope, + set_vxlan_udp_sport_range, + setup_npu_dpu # noqa: F811 +): + if skip_config: + return + + for i in range(len(duthosts)): + duthost = duthosts[i] + dpuhost = dpuhosts[i] + base_config_messages = { + **pl.APPLIANCE_CONFIG, + **pl.ROUTING_TYPE_PL_CONFIG, + **pl.VNET_CONFIG, + **pl.ROUTE_GROUP1_CONFIG, + **pl.METER_POLICY_V4_CONFIG + } + logger.info(f"configure on {duthost.hostname} dpu {dpuhost.dpu_index} {base_config_messages}") + + apply_messages(localhost, duthost, ptfhost, base_config_messages, dpuhost.dpu_index) + + route_and_mapping_messages = { + **pl.PE_VNET_MAPPING_CONFIG, + **pl.PE_SUBNET_ROUTE_CONFIG, + **pl.VM_SUBNET_ROUTE_CONFIG + } + logger.info(route_and_mapping_messages) + apply_messages(localhost, duthost, ptfhost, route_and_mapping_messages, dpuhost.dpu_index) + + meter_rule_messages = { + **pl.METER_RULE1_V4_CONFIG, + **pl.METER_RULE2_V4_CONFIG, + } + logger.info(meter_rule_messages) + apply_messages(localhost, duthost, ptfhost, meter_rule_messages, dpuhost.dpu_index) + + logger.info(pl.ENI_CONFIG) + apply_messages(localhost, duthost, ptfhost, pl.ENI_CONFIG, dpuhost.dpu_index) + + logger.info(pl.ENI_ROUTE_GROUP1_CONFIG) + apply_messages(localhost, duthost, ptfhost, pl.ENI_ROUTE_GROUP1_CONFIG, dpuhost.dpu_index) + + yield + + config_reload(dpuhost, safe_reload=True, yang_validate=False) + # apply_messages(localhost, duthost, ptfhost, pl.ENI_ROUTE_GROUP1_CONFIG, dpuhost.dpu_index, False) + # apply_messages(localhost, duthost, ptfhost, pl.ENI_CONFIG, dpuhost.dpu_index, False) + # apply_messages(localhost, duthost, ptfhost, meter_rule_messages, dpuhost.dpu_index, False) + # apply_messages(localhost, duthost, ptfhost, route_and_mapping_messages, dpuhost.dpu_index, False) + # apply_messages(localhost, duthost, ptfhost, base_config_messages, dpuhost.dpu_index, False) + + +@pytest.mark.parametrize("encap_proto", ["vxlan", "gre"]) +def test_privatelink_basic_transform( + ptfadapter, + ha_activate_role, + dash_pl_config, + encap_proto +): + vm_to_dpu_pkt, exp_dpu_to_pe_pkt = outbound_pl_packets(dash_pl_config[0], encap_proto) + pe_to_dpu_pkt, exp_dpu_to_vm_pkt = inbound_pl_packets(dash_pl_config[0]) + + ptfadapter.dataplane.flush() + testutils.send(ptfadapter, dash_pl_config[0][LOCAL_PTF_INTF], vm_to_dpu_pkt, 1) + testutils.verify_packet_any_port(ptfadapter, exp_dpu_to_pe_pkt, dash_pl_config[0][REMOTE_PTF_RECV_INTF]) + testutils.send(ptfadapter, dash_pl_config[0][REMOTE_PTF_SEND_INTF], pe_to_dpu_pkt, 1) + testutils.verify_packet(ptfadapter, exp_dpu_to_vm_pkt, dash_pl_config[0][LOCAL_PTF_INTF]) + From 0a2ceddf759bdad6ed6a853067c02e4a32e57ca7 Mon Sep 17 00:00:00 2001 From: Mihut Aronovici Date: Wed, 4 Feb 2026 15:12:25 -0800 Subject: [PATCH 02/19] fix review items, flake8 issues --- tests/ha/configs/privatelink_config.py | 4 +- tests/ha/conftest.py | 39 ++- tests/ha/gnmi_utils.py | 9 +- tests/ha/packets.py | 321 +------------------------ tests/ha/test_dash_privatelink.py | 6 - tests/ha/test_ha_dash_privatelink.py | 3 - 6 files changed, 27 insertions(+), 355 deletions(-) diff --git a/tests/ha/configs/privatelink_config.py b/tests/ha/configs/privatelink_config.py index 4630e4db6e4..9e9793f0d37 100644 --- a/tests/ha/configs/privatelink_config.py +++ b/tests/ha/configs/privatelink_config.py @@ -1,4 +1,4 @@ -from dash_api.eni_pb2 import State, EniMode +from dash_api.eni_pb2 import State from dash_api.route_type_pb2 import ActionType, EncapType, RoutingType from dash_api.types_pb2 import IpVersion @@ -8,7 +8,7 @@ PRIVATELINK = "privatelink" DECAP = "decap" -APPLIANCE_VIP="3.2.1.0" +APPLIANCE_VIP = "3.2.1.0" VM1_PA = "25.1.1.1" # VM host physical address VM1_CA = "10.0.0.11" # VM customer address VM_CA_SUBNET = "10.0.0.0/16" diff --git a/tests/ha/conftest.py b/tests/ha/conftest.py index 1ea2e8f0761..a26a7c8eaca 100644 --- a/tests/ha/conftest.py +++ b/tests/ha/conftest.py @@ -14,11 +14,11 @@ add_static_route_to_dut ) from ipaddress import ip_interface -from constants import ENI, VM_VNI, VNET1_VNI, VNET2_VNI, REMOTE_CA_IP, LOCAL_CA_IP, REMOTE_ENI_MAC, \ - LOCAL_ENI_MAC, REMOTE_CA_PREFIX, LOOPBACK_IP, DUT_MAC, LOCAL_PA_IP, LOCAL_PTF_INTF, LOCAL_PTF_MAC, \ - REMOTE_PA_IP, REMOTE_PTF_INTF, REMOTE_PTF_MAC, REMOTE_PA_PREFIX, VNET1_NAME, VNET2_NAME, ROUTING_ACTION, \ - ROUTING_ACTION_TYPE, LOOKUP_OVERLAY_IP, LOCAL_DUT_INTF, REMOTE_DUT_INTF, \ - REMOTE_PTF_SEND_INTF, REMOTE_PTF_RECV_INTF, LOCAL_REGION_ID, VXLAN_UDP_BASE_SRC_PORT, VXLAN_UDP_SRC_PORT_MASK, \ +from constants import LOCAL_CA_IP, \ + DUT_MAC, LOCAL_PTF_INTF, LOCAL_PTF_MAC, \ + REMOTE_PTF_INTF, REMOTE_PTF_MAC, \ + LOCAL_DUT_INTF, REMOTE_DUT_INTF, \ + REMOTE_PTF_SEND_INTF, REMOTE_PTF_RECV_INTF, VXLAN_UDP_BASE_SRC_PORT, VXLAN_UDP_SRC_PORT_MASK, \ NPU_DATAPLANE_IP, NPU_DATAPLANE_MAC, NPU_DATAPLANE_PORT, DPU_DATAPLANE_IP, DPU_DATAPLANE_MAC, DPU_DATAPLANE_PORT from dash_utils import render_template_to_host, apply_swssconfig_file from gnmi_utils import generate_gnmi_cert, apply_gnmi_cert, recover_gnmi_cert, apply_gnmi_file @@ -31,17 +31,19 @@ ENABLE_GNMI_API = True + @pytest.fixture(scope="module") def setup_ha_config(duthosts): pass + @pytest.fixture(scope="module") -def program_ha_set_and_initial_ha_scope(duthosts): +def setup_dash_ha_from_json(duthosts): pass @pytest.fixture(scope="module") -def ha_activate_role(duthosts): +def activate_dash_ha_from_json(duthosts): pass @@ -157,7 +159,6 @@ def setup_namespaces_with_routes(ptfhost, duthosts, get_t2_info): visited_namespaces.add(ns["namespace"]) - def get_interface_ip(duthost, interface): cmd = f"ip addr show {interface} | grep -w inet | awk '{{print $2}}'" output = duthost.shell(cmd)["stdout"].strip() @@ -296,7 +297,8 @@ def dash_pl_config(duthosts, dpuhosts, dpu_index, duts_minigraph_facts): intf, _ = get_intf_from_ip(config['local_addr'], config_facts) intfs = list(config_facts["PORTCHANNEL_MEMBER"][intf].keys()) dash_info[i][REMOTE_PTF_SEND_INTF] = minigraph_facts[0][1]["minigraph_ptf_indices"][intfs[0]] - dash_info[i][REMOTE_PTF_RECV_INTF] = [minigraph_facts[0][1]["minigraph_ptf_indices"][j] for j in intfs] + dash_info[i][REMOTE_PTF_RECV_INTF] = \ + [minigraph_facts[0][1]["minigraph_ptf_indices"][j] for j in intfs] dash_info[i][REMOTE_DUT_INTF] = intf dash_info[i][REMOTE_PTF_MAC] = neigh_table["v4"][neigh_ip]["macaddress"] @@ -314,7 +316,6 @@ def dash_pl_config(duthosts, dpuhosts, dpu_index, duts_minigraph_facts): dash_info[i][NPU_DATAPLANE_IP] = dpuhost.npu_data_port_ip dash_info[i][NPU_DATAPLANE_MAC] = dpuhost.npu_dataplane_mac - breakpoint() return dash_info @@ -441,8 +442,8 @@ def set_vxlan_udp_sport_range(dpuhosts, dpu_index): logger.warning("Applying Pensando DPU VXLAN sport workaround") dpuhost.shell("pdsctl debug update device --vxlan-port 4789 --vxlan-src-ports 5120-5247") yield - #if str(VXLAN_UDP_BASE_SRC_PORT) in dpuhost.shell("redis-cli -n 0 hget SWITCH_TABLE:switch vxlan_sport")['stdout']: - # config_reload(dpuhost, safe_reload=True, yang_validate=False) + if str(VXLAN_UDP_BASE_SRC_PORT) in dpuhost.shell("redis-cli -n 0 hget SWITCH_TABLE:switch vxlan_sport")['stdout']: + config_reload(dpuhost, safe_reload=True, yang_validate=False) @pytest.fixture(scope="module") @@ -458,9 +459,13 @@ def single_endpoint(request): @pytest.fixture def dpu_setup(duthosts, dpuhosts, dpu_index, skip_config): if skip_config: - return + + """ + Prior to this, HA configuration will set the route from DPU to NPU + """ for i in range(len(duthosts)): + # we run the DUT and DPU index in parallel because they are forming the HA pair duthost = duthosts[i] dpuhost = dpuhosts[i] # explicitly add mgmt IP route so the default route doesn't disrupt SSH access @@ -471,16 +476,11 @@ def dpu_setup(duthosts, dpuhosts, dpu_index, skip_config): dpu_cmds.append("config loopback add Loopback0") dpu_cmds.append(f"config int ip add Loopback0 {pl.APPLIANCE_VIP}/32") - pt_require(dpuhost.npu_data_port_ip, "DPU data port IP is not set") - dpu_cmds.append(f"ip route replace default via {dpuhost.npu_data_port_ip}") - dpuhost.shell_cmds(cmds=dpu_cmds) - @pytest.fixture(scope="function") def add_npu_static_routes( duthosts, dash_pl_config, skip_config, skip_cleanup, dpu_index, dpuhosts ): - #dpuhost = dpuhosts[dpu_index] if not skip_config: for i in range(len(duthosts)): duthost = duthosts[i] @@ -515,12 +515,9 @@ def add_npu_static_routes( cmds.append(f"ip route del {pl.VM1_PA}/32 via {vm_nexthop_ip}") cmds.append(f"ip route del {pl.PE_PA}/32 via {pe_nexthop_ip}") logger.info(f"Removing static routes: {cmds} from {duthost}") - breakpoint() duthost.shell_cmds(cmds=cmds) @pytest.fixture(scope="function") def setup_npu_dpu(dpu_setup, add_npu_static_routes): yield - - diff --git a/tests/ha/gnmi_utils.py b/tests/ha/gnmi_utils.py index c894c2152e5..76ebef49bc1 100644 --- a/tests/ha/gnmi_utils.py +++ b/tests/ha/gnmi_utils.py @@ -2,7 +2,6 @@ import logging import math import time -import uuid from functools import lru_cache import proto_utils @@ -177,7 +176,7 @@ def apply_gnmi_cert(my_duthost, ptfhost): my_duthost.copy(src=env.work_dir+env.gnmi_server_cert, dest=env.gnmi_cert_path) my_duthost.copy(src=env.work_dir+env.gnmi_server_key, dest=env.gnmi_cert_path) # Copy CA certificate and client certificate over to the PTF - + my_dest = '/root/' + my_duthost.hostname ptfhost.shell("mkdir -p {0}".format(my_dest)) ptfhost.copy(src=env.work_dir+env.gnmi_ca_cert, dest=my_dest) @@ -245,9 +244,9 @@ def gnmi_set(my_duthost, ptfhost, delete_list, update_list, replace_list): cmd += '--timeout 30 ' cmd += '-t %s -p %u ' % (ip, port) cmd += '-xo sonic-db ' - cmd += '-rcert /root/%s/%s ' % (my_duthost.hostname,env.gnmi_ca_cert) - cmd += '-pkey /root/%s/%s ' % (my_duthost.hostname,env.gnmi_client_key) - cmd += '-cchain /root/%s/%s ' % (my_duthost.hostname,env.gnmi_client_cert) + cmd += '-rcert /root/%s/%s ' % (my_duthost.hostname, env.gnmi_ca_cert) + cmd += '-pkey /root/%s/%s ' % (my_duthost.hostname, env.gnmi_client_key) + cmd += '-cchain /root/%s/%s ' % (my_duthost.hostname, env.gnmi_client_cert) if len(update_list) > 0: cmd += '-m set-update ' elif len(delete_list) > 0: diff --git a/tests/ha/packets.py b/tests/ha/packets.py index dd55d16ff1e..4ac782feb58 100644 --- a/tests/ha/packets.py +++ b/tests/ha/packets.py @@ -1,19 +1,13 @@ import logging import random -import sys -import time from ipaddress import ip_address import ptf.packet as scapy import ptf.testutils as testutils -import scapy.utils as scapy_utils from configs import privatelink_config as pl -from constants import * # noqa: F403 -from ptf.dataplane import match_exp_pkt -from ptf.mask import Mask, MaskException -from six import StringIO - -from tests.common.helpers.assertions import pytest_assert +from constants import VXLAN_UDP_BASE_SRC_PORT, VXLAN_UDP_SRC_PORT_MASK, \ + DUT_MAC, LOCAL_PTF_MAC, REMOTE_PTF_MAC +from ptf.mask import Mask logger = logging.getLogger(__name__) @@ -38,13 +32,6 @@ def generate_inner_packet(packet_type, ipv6=False): return None -def set_icmp_sub_type(packet, packet_type): - if packet_type == "echo_request": - packet[scapy.ICMP].type = 8 - elif packet_type == "echo_reply": - packet[scapy.ICMP].type = 0 - - def get_bits(ip): addr = ip_address(ip) return int(addr) @@ -70,54 +57,6 @@ def get_pl_overlay_dip(orig_dip, ol_dip, ol_mask): return str(ip_address(overlay_dip)) -def rand_udp_port_packets(config, floating_nic=True, outbound_vni=None): - """ - Randomly generate the inner (overlay) UDP source and destination ports. - Useful to ensure an even distribution of packets across multiple ECMP endpoints. - """ - sport = random.randint(49152, 65535) - dport = random.randint(49152, 65535) - vm_to_dpu_pkt, exp_dpu_to_pe_pkt = outbound_pl_packets( - config, "vxlan", floating_nic, inner_sport=sport, inner_dport=dport, vni=outbound_vni - ) - pe_to_dpu_pkt, exp_dpu_to_vm_pkt = inbound_pl_packets(config, floating_nic, inner_sport=dport, inner_dport=sport) - return vm_to_dpu_pkt, exp_dpu_to_pe_pkt, pe_to_dpu_pkt, exp_dpu_to_vm_pkt - - -def set_do_not_care_layer(mask, layer, field_name, n=1): - """ - Zeroes out the mask for 'field' in the nth occurrence of the specified layer. - """ - header_offset = mask.size - len(mask.exp_pkt.getlayer(layer, n)) - - try: - fields_desc = [ - field - for field in layer.fields_desc - if field.name in mask.exp_pkt[layer].__class__(bytes(mask.exp_pkt[layer])).fields.keys() - ] # build & parse packet to be sure all fields are correctly filled - except Exception: # noqa - raise MaskException("Can not build or decode Packet") - - if field_name not in [x.name for x in fields_desc]: - raise MaskException("Field %s does not exist in frame" % field_name) - - field_offset = 0 - bitwidth = 0 - for f in fields_desc: - try: - bits = f.size - except Exception: # noqa - bits = 8 * f.sz - if f.name == field_name: - bitwidth = bits - break - else: - field_offset += bits - - mask.set_do_not_care(header_offset * 8 + field_offset, bitwidth) - - def inbound_pl_packets( config, floating_nic=False, inner_packet_type="udp", vxlan_udp_dport=4789, inner_sport=4567, inner_dport=6789, vxlan_udp_base_src_port=VXLAN_UDP_BASE_SRC_PORT, vxlan_udp_src_port_mask=VXLAN_UDP_SRC_PORT_MASK @@ -190,32 +129,6 @@ def inbound_pl_packets( return gre_packet, masked_exp_packet -def plnsg_packets(config): - vm_to_dpu_pkt, exp_dpu_to_pe_pkt = outbound_pl_packets(config, "vxlan") - inner_pkt = exp_dpu_to_pe_pkt.exp_pkt - inner_pkt[scapy.Ether].src = config[DPU_DATAPLANE_MAC] - inner_pkt[scapy.Ether].dst = config[NPU_DATAPLANE_MAC] - exp_outer_pkt = testutils.simple_vxlan_packet( - eth_src=config[DUT_MAC], - eth_dst=config[REMOTE_PTF_MAC], - ip_src=pl.APPLIANCE_VIP, - ip_id=0, - udp_dport=4789, - udp_sport=1234, - with_udp_chksum=False, - vxlan_vni=pl.NSG_OUTBOUND_VNI, - inner_frame=inner_pkt, - ) - masked_outer_pkt = Mask(exp_outer_pkt) - masked_outer_pkt.set_do_not_care_packet(scapy.UDP, "sport") - masked_outer_pkt.set_do_not_care_packet(scapy.IP, "chksum") - masked_outer_pkt.set_do_not_care_packet(scapy.IP, "dst") - masked_outer_pkt.set_do_not_care_packet(scapy.IP, "ttl") - set_do_not_care_layer(masked_outer_pkt, scapy.IP, "ttl", 2) - set_do_not_care_layer(masked_outer_pkt, scapy.IP, "chksum", 2) - return vm_to_dpu_pkt, masked_outer_pkt - - def outbound_pl_packets( config, outer_encap, @@ -309,234 +222,6 @@ def outbound_pl_packets( return outer_packet, masked_exp_packet -def inbound_vnet_packets(dash_config_info, inner_extra_conf={}, inner_packet_type="udp", vxlan_udp_dport=4789): - inner_packet = generate_inner_packet(inner_packet_type)( - eth_src=dash_config_info[REMOTE_ENI_MAC], - eth_dst=dash_config_info[LOCAL_ENI_MAC], - ip_src=dash_config_info[REMOTE_CA_IP], - ip_dst=dash_config_info[LOCAL_CA_IP], - **inner_extra_conf, - ) - set_icmp_sub_type(inner_packet, inner_packet_type) - pa_match_vxlan_packet = testutils.simple_vxlan_packet( - eth_src=dash_config_info[REMOTE_PTF_MAC], - eth_dst=dash_config_info[DUT_MAC], - ip_src=dash_config_info[REMOTE_PA_IP], - ip_dst=dash_config_info[LOOPBACK_IP], - udp_dport=vxlan_udp_dport, - vxlan_vni=dash_config_info[VNET2_VNI], - ip_ttl=64, - inner_frame=inner_packet, - ) - expected_packet = testutils.simple_vxlan_packet( - eth_src=dash_config_info[DUT_MAC], - eth_dst=dash_config_info[LOCAL_PTF_MAC], - ip_src=dash_config_info[LOOPBACK_IP], - ip_dst=dash_config_info[LOCAL_PA_IP], - udp_dport=vxlan_udp_dport, - vxlan_vni=dash_config_info[VM_VNI], - ip_ttl=255, - ip_id=0, - inner_frame=inner_packet, - ) - - pa_mismatch_vxlan_packet = pa_match_vxlan_packet.copy() - remote_pa_ip = ip_address(dash_config_info[REMOTE_PA_IP]) - pa_mismatch_vxlan_packet["IP"].src = str(remote_pa_ip + 1) - - masked_exp_packet = Mask(expected_packet) - masked_exp_packet.set_do_not_care_scapy(scapy.IP, "id") - masked_exp_packet.set_do_not_care_scapy(scapy.IP, "chksum") - masked_exp_packet.set_do_not_care_scapy(scapy.UDP, "sport") - masked_exp_packet.set_do_not_care_scapy(scapy.UDP, "chksum") - - return inner_packet, pa_match_vxlan_packet, pa_mismatch_vxlan_packet, masked_exp_packet - - -def outbound_vnet_packets(dash_config_info, inner_extra_conf={}, inner_packet_type="udp", vxlan_udp_dport=4789): - proto = None - if "proto" in inner_extra_conf: - proto = int(inner_extra_conf["proto"]) - del inner_extra_conf["proto"] - - inner_packet = generate_inner_packet(inner_packet_type)( - eth_src=dash_config_info[LOCAL_ENI_MAC], - eth_dst=dash_config_info[REMOTE_ENI_MAC], - ip_src=dash_config_info[LOCAL_CA_IP], - ip_dst=dash_config_info[REMOTE_CA_IP], - **inner_extra_conf, - ) - set_icmp_sub_type(inner_packet, inner_packet_type) - - if proto: - inner_packet[scapy.IP].proto = proto - - vxlan_packet = testutils.simple_vxlan_packet( - eth_src=dash_config_info[LOCAL_PTF_MAC], - eth_dst=dash_config_info[DUT_MAC], - ip_src=dash_config_info[LOCAL_PA_IP], - ip_dst=dash_config_info[LOOPBACK_IP], - udp_dport=vxlan_udp_dport, - with_udp_chksum=False, - vxlan_vni=dash_config_info[VM_VNI], - ip_ttl=64, - inner_frame=inner_packet, - ) - expected_packet = testutils.simple_vxlan_packet( - eth_src=dash_config_info[DUT_MAC], - eth_dst=dash_config_info[REMOTE_PTF_MAC], - ip_src=dash_config_info[LOOPBACK_IP], - ip_dst=dash_config_info[REMOTE_PA_IP], - udp_dport=vxlan_udp_dport, - vxlan_vni=dash_config_info[VNET2_VNI], - # TODO: Change TTL to 63 after SAI bug is fixed - ip_ttl=0xFF, - ip_id=0, - inner_frame=inner_packet, - ) - - masked_exp_packet = Mask(expected_packet) - masked_exp_packet.set_do_not_care_scapy(scapy.IP, "id") - masked_exp_packet.set_do_not_care_scapy(scapy.IP, "chksum") - masked_exp_packet.set_do_not_care_scapy(scapy.UDP, "sport") - masked_exp_packet.set_do_not_care_scapy(scapy.UDP, "chksum") - return inner_packet, vxlan_packet, masked_exp_packet - - -def get_packets_on_specified_ports(ptfadapter, ports, filter_pkt_lens, device_number=0, duration=3, timeout=0.2): - """ - Get the packets on the specified ports and device for the specified duration - """ - logging.info("Get pkts on device %d, port %r", device_number, ports) - - received_pkts_res = {} - start_time = time.time() - while (time.time() - start_time) < duration: - result = testutils.dp_poll(ptfadapter, device_number=device_number, timeout=timeout) - logger.info(result) - if isinstance(result, ptfadapter.dataplane.PollSuccess) and result.port in ports: - if len(result.packet) in filter_pkt_lens: - if result.port in received_pkts_res: - received_pkts_res[result.port].append(result) - else: - received_pkts_res[result.port] = [result] - return received_pkts_res - - -def verify_each_packet_on_each_port(exp_pkts, received_pkts_res, ports): - """ - Verify each packet can be received on the corresponding port - """ - logger.info(f"Checking pkts on ports :{ports}") - for port, exp_pkt in zip(ports, exp_pkts): - if port in received_pkts_res: - find_matched_ptk = False - for pkt_res in received_pkts_res[port]: - if match_exp_pkt(exp_pkt, pkt_res.packet): - find_matched_ptk = True - logger.info(f"find the matched packet on port {port}") - break - if not find_matched_ptk: - logger.error( - print_expect_packet_and_received_packet_hex_information([exp_pkt], received_pkts_res[port]) - ) - pytest_assert(False, f"Not find the matched pkt on port {port}") - else: - pytest_assert(False, f"port {port} doesn't receive any packet") - return True - - -def verify_packets_not_received(unexp_pkts, received_pkts_res): - """ - Verify packets are not received - """ - for unexp_pkt in unexp_pkts: - for _, received_pkts in received_pkts_res.items(): - for pkt_res in received_pkts: - if match_exp_pkt(unexp_pkt, pkt_res.packet): - logger.error(print_expect_packet_and_received_packet_hex_information([unexp_pkt], received_pkts)) - pytest_assert(False, f" unexpected packet are received on port {pkt_res.port}") - return True - - -def print_expect_packet_and_received_packet_hex_information(exp_pkts, received_pkts_res): - try: - stdout_save = sys.stdout - # The scapy packet dissection methods print directly to stdout, - # so we have to redirect stdout to a string. - sys.stdout = StringIO() - - for exp_pkt in exp_pkts: - print("Expected pkt:") - scapy_utils.hexdump(exp_pkt.exp_pkt) - print("Expected pkt mask:") - scapy_utils.hexdump(exp_pkt.mask) - - print("==============================") - for pkt_res in received_pkts_res: - print("Receive pkt:") - scapy_utils.hexdump(pkt_res.packet) - - return sys.stdout.getvalue() - finally: - sys.stdout.close() - sys.stdout = stdout_save # Restore the original stdout. - - -def verify_tcp_packet_drop_rst_packet_sent( - ptfadapter, exp_rst_pkts, drop_tcp_pkts, ports, filter_pkt_lens, device_number=0, duration=10, timeout=0.2 -): - received_pkts_res = get_packets_on_specified_ports( - ptfadapter, ports, filter_pkt_lens, device_number, duration, timeout - ) - verify_packets_not_received(drop_tcp_pkts, received_pkts_res) - verify_each_packet_on_each_port(exp_rst_pkts, received_pkts_res, ports) - - -def outbound_smartswitch_vnet_packets( - dash_config_info, inner_extra_conf={}, inner_packet_type="udp", vxlan_udp_dport=4789 -): - - inner_packet = generate_inner_packet(inner_packet_type)( - eth_src=dash_config_info[LOCAL_ENI_MAC], - eth_dst=dash_config_info[REMOTE_ENI_MAC], - ip_src=dash_config_info[LOCAL_CA_IP], - ip_dst=dash_config_info[REMOTE_CA_IP], - ) - set_icmp_sub_type(inner_packet, inner_packet_type) - - vxlan_packet = testutils.simple_vxlan_packet( - eth_src=dash_config_info[LOCAL_PTF_MAC], - eth_dst=dash_config_info[DUT_MAC], - ip_src=dash_config_info[LOCAL_PA_IP], - ip_dst=dash_config_info[LOOPBACK_IP], - udp_dport=vxlan_udp_dport, - udp_sport=VXLAN_UDP_BASE_SRC_PORT, - with_udp_chksum=False, - vxlan_vni=dash_config_info[VNET1_VNI], - ip_ttl=64, - inner_frame=inner_packet, - ) - expected_packet = testutils.simple_vxlan_packet( - eth_src=dash_config_info[DUT_MAC], - eth_dst=dash_config_info[REMOTE_PTF_MAC], - ip_src=dash_config_info[LOOPBACK_IP], - ip_dst=dash_config_info[REMOTE_PA_IP], - udp_dport=vxlan_udp_dport, - vxlan_vni=dash_config_info[VNET1_VNI], - ip_ttl=63, - ip_id=0, - inner_frame=inner_packet, - ) - - masked_exp_packet = Mask(expected_packet) - masked_exp_packet.set_do_not_care_scapy(scapy.IP, "id") - masked_exp_packet.set_do_not_care_scapy(scapy.IP, "chksum") - masked_exp_packet.set_do_not_care_scapy(scapy.UDP, "sport") - masked_exp_packet.set_do_not_care_scapy(scapy.UDP, "chksum") - return inner_packet, vxlan_packet, masked_exp_packet - - def get_scapy_l4_protocol_key(inner_packet_type): scapy_tcp = scapy.TCP scapy_udp = scapy.UDP diff --git a/tests/ha/test_dash_privatelink.py b/tests/ha/test_dash_privatelink.py index 7e449f31497..37ec5c46fdc 100644 --- a/tests/ha/test_dash_privatelink.py +++ b/tests/ha/test_dash_privatelink.py @@ -3,8 +3,6 @@ import configs.privatelink_config as pl import ptf.testutils as testutils import pytest -import random -import ptf.packet as scapy from constants import LOCAL_PTF_INTF, REMOTE_PTF_RECV_INTF, REMOTE_PTF_SEND_INTF from gnmi_utils import apply_messages from packets import outbound_pl_packets, inbound_pl_packets @@ -37,7 +35,6 @@ def common_setup_teardown( ): if skip_config: return - breakpoint() for i in range(len(duthosts)): duthost = duthosts[i] dpuhost = dpuhosts[i] @@ -50,7 +47,6 @@ def common_setup_teardown( } logger.info(f"configure on {duthost.hostname} dpu {dpuhost.dpu_index} {base_config_messages}") - breakpoint() apply_messages(localhost, duthost, ptfhost, base_config_messages, dpuhost.dpu_index) route_and_mapping_messages = { @@ -93,10 +89,8 @@ def test_privatelink_basic_transform( vm_to_dpu_pkt, exp_dpu_to_pe_pkt = outbound_pl_packets(dash_pl_config[0], encap_proto) pe_to_dpu_pkt, exp_dpu_to_vm_pkt = inbound_pl_packets(dash_pl_config[0]) - breakpoint() ptfadapter.dataplane.flush() testutils.send(ptfadapter, dash_pl_config[0][LOCAL_PTF_INTF], vm_to_dpu_pkt, 1) testutils.verify_packet_any_port(ptfadapter, exp_dpu_to_pe_pkt, dash_pl_config[0][REMOTE_PTF_RECV_INTF]) testutils.send(ptfadapter, dash_pl_config[0][REMOTE_PTF_SEND_INTF], pe_to_dpu_pkt, 1) testutils.verify_packet(ptfadapter, exp_dpu_to_vm_pkt, dash_pl_config[0][LOCAL_PTF_INTF]) - diff --git a/tests/ha/test_ha_dash_privatelink.py b/tests/ha/test_ha_dash_privatelink.py index 6b96746cabc..5840c06dfaa 100644 --- a/tests/ha/test_ha_dash_privatelink.py +++ b/tests/ha/test_ha_dash_privatelink.py @@ -3,8 +3,6 @@ import configs.privatelink_config as pl import ptf.testutils as testutils import pytest -import random -import ptf.packet as scapy from constants import LOCAL_PTF_INTF, REMOTE_PTF_RECV_INTF, REMOTE_PTF_SEND_INTF from gnmi_utils import apply_messages from packets import outbound_pl_packets, inbound_pl_packets @@ -99,4 +97,3 @@ def test_privatelink_basic_transform( testutils.verify_packet_any_port(ptfadapter, exp_dpu_to_pe_pkt, dash_pl_config[0][REMOTE_PTF_RECV_INTF]) testutils.send(ptfadapter, dash_pl_config[0][REMOTE_PTF_SEND_INTF], pe_to_dpu_pkt, 1) testutils.verify_packet(ptfadapter, exp_dpu_to_vm_pkt, dash_pl_config[0][LOCAL_PTF_INTF]) - From efe12f8591894c2f3d6eac16861033e12094832a Mon Sep 17 00:00:00 2001 From: Mihut Aronovici Date: Tue, 10 Feb 2026 07:30:10 -0800 Subject: [PATCH 03/19] fix the PR review comments --- tests/ha/conftest.py | 2 +- tests/ha/dash_utils.py | 131 --------------------------- tests/ha/gnmi_utils.py | 12 +-- tests/ha/test_dash_privatelink.py | 5 + tests/ha/test_ha_dash_privatelink.py | 6 ++ 5 files changed, 18 insertions(+), 138 deletions(-) delete mode 100644 tests/ha/dash_utils.py diff --git a/tests/ha/conftest.py b/tests/ha/conftest.py index a26a7c8eaca..1714903bdac 100644 --- a/tests/ha/conftest.py +++ b/tests/ha/conftest.py @@ -20,7 +20,7 @@ LOCAL_DUT_INTF, REMOTE_DUT_INTF, \ REMOTE_PTF_SEND_INTF, REMOTE_PTF_RECV_INTF, VXLAN_UDP_BASE_SRC_PORT, VXLAN_UDP_SRC_PORT_MASK, \ NPU_DATAPLANE_IP, NPU_DATAPLANE_MAC, NPU_DATAPLANE_PORT, DPU_DATAPLANE_IP, DPU_DATAPLANE_MAC, DPU_DATAPLANE_PORT -from dash_utils import render_template_to_host, apply_swssconfig_file +from tests.dash.dash_utils import render_template_to_host, apply_swssconfig_file from gnmi_utils import generate_gnmi_cert, apply_gnmi_cert, recover_gnmi_cert, apply_gnmi_file from tests.common.helpers.smartswitch_util import correlate_dpu_info_with_dpuhost, get_data_port_on_dpu, get_dpu_dataplane_port # noqa F401 from tests.common import config_reload diff --git a/tests/ha/dash_utils.py b/tests/ha/dash_utils.py deleted file mode 100644 index e1dd07f5856..00000000000 --- a/tests/ha/dash_utils.py +++ /dev/null @@ -1,131 +0,0 @@ -import logging -from os import path -from time import sleep - -import ptf.packet as scapy -import ptf.testutils as testutils -import pytest - -from jinja2 import Template - -from constants import TEMPLATE_DIR - -logger = logging.getLogger(__name__) - - -def safe_open_template(template_path): - """ - Safely loads Jinja2 template from given path - - Note: - All Jinja2 templates should be accessed with this method to ensure proper garbage disposal - - Args: - template_path: String containing the location of the template file to be opened - - Returns: - A Jinja2 Template object read from the provided file - """ - - with open(template_path) as template_file: - return Template(template_file.read()) - - -def combine_dicts(*args): - """ - Combines multiple Python dictionaries into a single dictionary - - Used primarily to pass arguments contained in multiple dictionaries to the `render()` method for Jinja2 templates - - Args: - *args: The dictionaries to be combined - - Returns: - A single Python dictionary containing the key/value pairs of all the input dictionaries - """ - - combined_args = {} - - for arg in args: - combined_args.update(arg) - - return combined_args - - -def render_template_to_host(template_name, host, dest_file, *template_args, **template_kwargs): - """ - Renders a template with the given arguments and copies it to the host - - Args: - template_name: A template inside the "templates" folder (without the preceding "templates/") - host: The host device to copy the rendered template to (either a PTF or DUT host object) - dest_file: The location on the host to copy the rendered template to - *template_args: Any arguments to be passed to j2 during rendering - **template_kwargs: Any keyword arguments to be passed to j2 during rendering - """ - - combined_args = combine_dicts(*template_args) - - rendered = safe_open_template(path.join(TEMPLATE_DIR, template_name)).render(combined_args, **template_kwargs) - - host.copy(content=rendered, dest=dest_file) - - -def render_template(template_name, *template_args, **template_kwargs): - """ - Renders a template with the given arguments and copies it to the host - - Args: - template_name: A template inside the "templates" folder (without the preceding "templates/") - *template_args: Any arguments to be passed to j2 during rendering - **template_kwargs: Any keyword arguments to be passed to j2 during rendering - """ - - combined_args = combine_dicts(*template_args) - - return safe_open_template(path.join(TEMPLATE_DIR, template_name)).render(combined_args, **template_kwargs) - - -def apply_swssconfig_file(duthost, file_path): - """ - Copies config file from the DUT host to the SWSS docker and applies them with swssconfig - - Args: - duthost: DUT host object - file: Path to config file on the host - """ - logger.info("Applying config files on DUT") - file_name = path.basename(file_path) - - duthost.shell("docker cp {} swss:/{}".format(file_path, file_name)) - duthost.shell("docker exec swss sh -c \"swssconfig /{}\"".format(file_name)) - sleep(5) - - -def verify_tunnel_packets(ptfadapter, ports, exp_dpu_to_vm_pkt, tunnel_endpoint_counts): - timeout = 1 - if isinstance(ports, list): - target_ports = ports - else: - target_ports = [ports] - - result = testutils.dp_poll(ptfadapter, timeout=timeout, exp_pkt=exp_dpu_to_vm_pkt) - if isinstance(result, ptfadapter.dataplane.PollSuccess): - pkt_repr = scapy.Ether(result.packet) - if result.port in target_ports: - if pkt_repr["IP"].dst in tunnel_endpoint_counts: - tunnel_endpoint_counts[pkt_repr["IP"].dst] += 1 - logging.debug( - f"Packet sent to tunnel endpoint {pkt_repr['IP'].dst} matches:\ - \n{result.format()} \nExpected:\n{exp_dpu_to_vm_pkt}" - ) - return - else: - pytest.fail( - f"Received packet has unexpected dst IP {pkt_repr['IP'].dst}, \ - expected one of {tunnel_endpoint_counts.keys()} \ - \n{result.format()} \nExpected:\n{exp_dpu_to_vm_pkt}" - ) - else: - pytest.fail(f"Got expected packet on unexpected port {result.port}: {pkt_repr}") - pytest.fail(f"DP poll failed:\n{result.format()}") diff --git a/tests/ha/gnmi_utils.py b/tests/ha/gnmi_utils.py index 76ebef49bc1..bf884c36380 100644 --- a/tests/ha/gnmi_utils.py +++ b/tests/ha/gnmi_utils.py @@ -351,14 +351,14 @@ def apply_messages( if set_db: if proto_utils.ENABLE_PROTO: - path = f"/APPL_DB/dpu{dpu_index}/{gnmi_key}:$/root/{filename}" + path = f"/DPU_APPL_DB/dpu{dpu_index}/{gnmi_key}:$/root/{filename}" else: - path = f"/APPL_DB/dpu{dpu_index}/{gnmi_key}:@/root/{filename}" + path = f"/DPU_APPL_DB/dpu{dpu_index}/{gnmi_key}:@/root/{filename}" with open(env.work_dir + filename, "wb") as file: file.write(message.SerializeToString()) update_list.append(path) else: - path = f"/APPL_DB/dpu{dpu_index}/{gnmi_key}" + path = f"/DPU_APPL_DB/dpu{dpu_index}/{gnmi_key}" delete_list.append(path) write_gnmi_files(localhost, my_duthost, ptfhost, env, delete_list, update_list, max_updates_in_single_cmd) @@ -412,9 +412,9 @@ def apply_gnmi_file(localhost, my_duthost, ptfhost, dest_path=None, config_json= keys = k.split(":", 1) k = keys[0] + "[key=" + keys[1] + "]" if proto_utils.ENABLE_PROTO: - path = "/APPL_DB/%s/%s:$/root/%s" % (host, k, filename) + path = "/DPU_APPL_DB/%s/%s:$/root/%s" % (host, k, filename) else: - path = "/APPL_DB/%s/%s:@/root/%s" % (host, k, filename) + path = "/DPU_APPL_DB/%s/%s:@/root/%s" % (host, k, filename) update_list.append(path) elif operation["OP"] == "DEL": for k, v in operation.items(): @@ -422,7 +422,7 @@ def apply_gnmi_file(localhost, my_duthost, ptfhost, dest_path=None, config_json= continue keys = k.split(":", 1) k = keys[0] + "[key=" + keys[1] + "]" - path = "/APPL_DB/%s/%s" % (host, k) + path = "/DPU_APPL_DB/%s/%s" % (host, k) delete_list.append(path) else: logger.info("Invalid operation %s" % operation["OP"]) diff --git a/tests/ha/test_dash_privatelink.py b/tests/ha/test_dash_privatelink.py index 37ec5c46fdc..a3a2744b5db 100644 --- a/tests/ha/test_dash_privatelink.py +++ b/tests/ha/test_dash_privatelink.py @@ -54,6 +54,11 @@ def common_setup_teardown( **pl.PE_SUBNET_ROUTE_CONFIG, **pl.VM_SUBNET_ROUTE_CONFIG } + if 'bluefield' in dpuhost.facts['asic_type']: + route_and_mapping_messages.update({ + **pl.INBOUND_VNI_ROUTE_RULE_CONFIG + }) + logger.info(route_and_mapping_messages) apply_messages(localhost, duthost, ptfhost, route_and_mapping_messages, dpuhost.dpu_index) diff --git a/tests/ha/test_ha_dash_privatelink.py b/tests/ha/test_ha_dash_privatelink.py index 5840c06dfaa..d52612eda1f 100644 --- a/tests/ha/test_ha_dash_privatelink.py +++ b/tests/ha/test_ha_dash_privatelink.py @@ -56,6 +56,12 @@ def common_setup_teardown( **pl.PE_SUBNET_ROUTE_CONFIG, **pl.VM_SUBNET_ROUTE_CONFIG } + + if 'bluefield' in dpuhost.facts['asic_type']: + route_and_mapping_messages.update({ + **pl.INBOUND_VNI_ROUTE_RULE_CONFIG + }) + logger.info(route_and_mapping_messages) apply_messages(localhost, duthost, ptfhost, route_and_mapping_messages, dpuhost.dpu_index) From 192a8c37d8d5ab6fa13bebba534b61547cd168b3 Mon Sep 17 00:00:00 2001 From: Mihut Aronovici Date: Tue, 10 Feb 2026 08:33:41 -0800 Subject: [PATCH 04/19] removed the cross dependency error --- tests/{dash => common}/dash_utils.py | 0 tests/dash/conftest.py | 2 +- tests/dash/test_dash_disable_enable_eni.py | 2 +- tests/dash/test_dash_smartswitch_vnet.py | 2 +- tests/dash/test_eni_based_forwarding.py | 2 +- tests/dash/test_fnic.py | 2 +- tests/ha/conftest.py | 2 +- 7 files changed, 6 insertions(+), 6 deletions(-) rename tests/{dash => common}/dash_utils.py (100%) diff --git a/tests/dash/dash_utils.py b/tests/common/dash_utils.py similarity index 100% rename from tests/dash/dash_utils.py rename to tests/common/dash_utils.py diff --git a/tests/dash/conftest.py b/tests/dash/conftest.py index 1c40ee381c8..d297feeddcb 100644 --- a/tests/dash/conftest.py +++ b/tests/dash/conftest.py @@ -11,7 +11,7 @@ ROUTING_ACTION_TYPE, LOOKUP_OVERLAY_IP, ACL_GROUP, ACL_STAGE, LOCAL_DUT_INTF, REMOTE_DUT_INTF, \ REMOTE_PTF_SEND_INTF, REMOTE_PTF_RECV_INTF, LOCAL_REGION_ID, VXLAN_UDP_BASE_SRC_PORT, VXLAN_UDP_SRC_PORT_MASK, \ NPU_DATAPLANE_IP, NPU_DATAPLANE_MAC, NPU_DATAPLANE_PORT, DPU_DATAPLANE_IP, DPU_DATAPLANE_MAC, DPU_DATAPLANE_PORT -from dash_utils import render_template_to_host, apply_swssconfig_file +from tests.common.dash_utils import render_template_to_host, apply_swssconfig_file from gnmi_utils import generate_gnmi_cert, apply_gnmi_cert, recover_gnmi_cert, apply_gnmi_file from dash_acl import AclGroup, DEFAULT_ACL_GROUP, WAIT_AFTER_CONFIG, DefaultAclRule from tests.common.helpers.smartswitch_util import correlate_dpu_info_with_dpuhost, get_data_port_on_dpu, get_dpu_dataplane_port # noqa F401 diff --git a/tests/dash/test_dash_disable_enable_eni.py b/tests/dash/test_dash_disable_enable_eni.py index 7561d0f84d7..3d445670a27 100644 --- a/tests/dash/test_dash_disable_enable_eni.py +++ b/tests/dash/test_dash_disable_enable_eni.py @@ -6,7 +6,7 @@ from constants import LOCAL_PTF_INTF, REMOTE_PTF_INTF from tests.common.plugins.allure_wrapper import allure_step_wrapper as allure from gnmi_utils import apply_gnmi_file -from dash_utils import render_template_to_host +from tests.common.dash_utils import render_template_to_host from tests.common.utilities import wait_until from tests.common.helpers.assertions import pytest_assert diff --git a/tests/dash/test_dash_smartswitch_vnet.py b/tests/dash/test_dash_smartswitch_vnet.py index 93d1a065a78..b53daaa1a8b 100644 --- a/tests/dash/test_dash_smartswitch_vnet.py +++ b/tests/dash/test_dash_smartswitch_vnet.py @@ -8,7 +8,7 @@ from constants import LOCAL_PTF_INTF, REMOTE_PA_IP, REMOTE_PTF_RECV_INTF, REMOTE_DUT_INTF, \ VXLAN_UDP_BASE_SRC_PORT, VXLAN_UDP_SRC_PORT_MASK from gnmi_utils import apply_gnmi_file -from dash_utils import render_template_to_host, apply_swssconfig_file +from tests.common.dash_utils import render_template_to_host, apply_swssconfig_file from tests.dash.conftest import get_interface_ip from tests.common import config_reload diff --git a/tests/dash/test_eni_based_forwarding.py b/tests/dash/test_eni_based_forwarding.py index 947f5683c7b..6f513e6f893 100644 --- a/tests/dash/test_eni_based_forwarding.py +++ b/tests/dash/test_eni_based_forwarding.py @@ -9,7 +9,7 @@ import ptf.packet as scapy from ptf.mask import Mask -from dash_utils import render_template_to_host, apply_swssconfig_file +from tests.common.dash_utils import render_template_to_host, apply_swssconfig_file from tests.dash.conftest import get_interface_ip from tests.common.helpers.assertions import pytest_assert from tests.common.utilities import wait_until diff --git a/tests/dash/test_fnic.py b/tests/dash/test_fnic.py index 30f0f06744d..b9553bb5eb1 100644 --- a/tests/dash/test_fnic.py +++ b/tests/dash/test_fnic.py @@ -9,7 +9,7 @@ from tests.common.helpers.assertions import pytest_assert from configs.privatelink_config import TUNNEL1_ENDPOINT_IPS, TUNNEL2_ENDPOINT_IPS from tests.common import config_reload -from tests.dash.dash_utils import verify_tunnel_packets +from tests.common.dash_utils import verify_tunnel_packets logger = logging.getLogger(__name__) diff --git a/tests/ha/conftest.py b/tests/ha/conftest.py index 1714903bdac..31d8b96f7f9 100644 --- a/tests/ha/conftest.py +++ b/tests/ha/conftest.py @@ -20,7 +20,7 @@ LOCAL_DUT_INTF, REMOTE_DUT_INTF, \ REMOTE_PTF_SEND_INTF, REMOTE_PTF_RECV_INTF, VXLAN_UDP_BASE_SRC_PORT, VXLAN_UDP_SRC_PORT_MASK, \ NPU_DATAPLANE_IP, NPU_DATAPLANE_MAC, NPU_DATAPLANE_PORT, DPU_DATAPLANE_IP, DPU_DATAPLANE_MAC, DPU_DATAPLANE_PORT -from tests.dash.dash_utils import render_template_to_host, apply_swssconfig_file +from tests.common.dash_utils import render_template_to_host, apply_swssconfig_file from gnmi_utils import generate_gnmi_cert, apply_gnmi_cert, recover_gnmi_cert, apply_gnmi_file from tests.common.helpers.smartswitch_util import correlate_dpu_info_with_dpuhost, get_data_port_on_dpu, get_dpu_dataplane_port # noqa F401 from tests.common import config_reload From c8c957d0037460e2200aaab41c67ead1384628df Mon Sep 17 00:00:00 2001 From: Mihut Aronovici Date: Tue, 10 Feb 2026 09:20:15 -0800 Subject: [PATCH 05/19] update dash_utils import --- tests/dash/dash_acl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/dash/dash_acl.py b/tests/dash/dash_acl.py index 0248d4d6b91..0b1a53e3a4e 100644 --- a/tests/dash/dash_acl.py +++ b/tests/dash/dash_acl.py @@ -7,7 +7,7 @@ import random from collections.abc import Iterable from constants import * # noqa: F403 -from dash_utils import render_template +from tests.common.dash_utils import render_template from gnmi_utils import apply_gnmi_file import packets import ptf.testutils as testutils From 935c5284a3255605c7605214875ad0798d828d03 Mon Sep 17 00:00:00 2001 From: Mihut Aronovici Date: Tue, 10 Feb 2026 12:08:33 -0800 Subject: [PATCH 06/19] cleanup conftest file --- tests/ha/conftest.py | 85 -------------------------------------------- 1 file changed, 85 deletions(-) diff --git a/tests/ha/conftest.py b/tests/ha/conftest.py index 31d8b96f7f9..00dada270e2 100644 --- a/tests/ha/conftest.py +++ b/tests/ha/conftest.py @@ -22,7 +22,6 @@ NPU_DATAPLANE_IP, NPU_DATAPLANE_MAC, NPU_DATAPLANE_PORT, DPU_DATAPLANE_IP, DPU_DATAPLANE_MAC, DPU_DATAPLANE_PORT from tests.common.dash_utils import render_template_to_host, apply_swssconfig_file from gnmi_utils import generate_gnmi_cert, apply_gnmi_cert, recover_gnmi_cert, apply_gnmi_file -from tests.common.helpers.smartswitch_util import correlate_dpu_info_with_dpuhost, get_data_port_on_dpu, get_dpu_dataplane_port # noqa F401 from tests.common import config_reload import configs.privatelink_config as pl from tests.common.helpers.assertions import pytest_require as pt_require @@ -165,76 +164,6 @@ def get_interface_ip(duthost, interface): return ip_interface(output) -def pytest_addoption(parser): - """ - Adds pytest options that are used by DASH tests - """ - - parser.addoption( - "--skip_config", - action="store_true", - help="Don't apply configurations on DUT" - ) - - parser.addoption( - "--config_only", - action="store_true", - help="Apply new configurations on DUT without running tests" - ) - - parser.addoption( - "--skip_dataplane_checking", - action="store_true", - help="Skip dataplane checking" - ) - - parser.addoption( - "--vxlan_udp_dport", - action="store", - default="random", - help="The vxlan udp dst port used in the test" - ) - - parser.addoption( - "--skip_cert_cleanup", - action="store_true", - help="Skip certificates cleanup after test" - ) - - parser.addoption( - "--dpu_index", - action="store", - default=0, - type=int, - help="The default dpu used for the test" - ) - - -@pytest.fixture(scope="module") -def config_only(request): - return request.config.getoption("--config_only") - - -@pytest.fixture(scope="module") -def skip_config(request): - return request.config.getoption("--skip_config") - - -@pytest.fixture(scope="module") -def skip_cleanup(request): - return request.config.getoption("--skip_cleanup") - - -@pytest.fixture(scope="module") -def skip_dataplane_checking(request): - return request.config.getoption("--skip_dataplane_checking") - - -@pytest.fixture(scope="module") -def skip_cert_cleanup(request): - return request.config.getoption("--skip_cert_cleanup") - - @pytest.fixture(scope="module") def config_facts(duthost): return duthost.config_facts(host=duthost.hostname, source="running")['ansible_facts'] @@ -360,15 +289,6 @@ def setup_gnmi_server(duthosts, localhost, ptfhost, skip_cert_cleanup): recover_gnmi_cert(localhost, duthost, skip_cert_cleanup) -@pytest.fixture(scope="function") -def asic_db_checker(duthost): - def _check_asic_db(tables): - for table in tables: - output = duthost.shell("sonic-db-cli ASIC_DB keys 'ASIC_STATE:{}:*'".format(table)) - assert output["stdout"].strip() != "", "No entries found in ASIC_DB table {}".format(table) - yield _check_asic_db - - @pytest.fixture(scope="function", params=['udp', 'tcp', 'echo_request', 'echo_reply']) def inner_packet_type(request): return request.param @@ -451,11 +371,6 @@ def dpu_index(request): return request.config.getoption("--dpu_index") -@pytest.fixture(scope="module", params=[True, False], ids=["single-endpoint", "multi-endpoint"]) -def single_endpoint(request): - return request.param - - @pytest.fixture def dpu_setup(duthosts, dpuhosts, dpu_index, skip_config): if skip_config: From b9054376d69a31b7c0638014112fb5f94d06e652 Mon Sep 17 00:00:00 2001 From: Mihut Aronovici Date: Tue, 10 Feb 2026 12:59:44 -0800 Subject: [PATCH 07/19] update ha fixture names --- tests/ha/test_ha_dash_privatelink.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ha/test_ha_dash_privatelink.py b/tests/ha/test_ha_dash_privatelink.py index d52612eda1f..98b03b62b47 100644 --- a/tests/ha/test_ha_dash_privatelink.py +++ b/tests/ha/test_ha_dash_privatelink.py @@ -30,7 +30,7 @@ def common_setup_teardown( skip_config, dpuhosts, setup_ha_config, - program_ha_set_and_initial_ha_scope, + setup_dash_ha_from_json, set_vxlan_udp_sport_range, setup_npu_dpu # noqa: F811 ): @@ -91,7 +91,7 @@ def common_setup_teardown( @pytest.mark.parametrize("encap_proto", ["vxlan", "gre"]) def test_privatelink_basic_transform( ptfadapter, - ha_activate_role, + activate_dash_ha_from_json, dash_pl_config, encap_proto ): From 8771a457a59856a0e16672fcd1f6de1fc7963e48 Mon Sep 17 00:00:00 2001 From: Mihut Aronovici Date: Tue, 10 Feb 2026 16:44:30 -0800 Subject: [PATCH 08/19] fix the merge with master issues --- tests/ha/conftest.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/ha/conftest.py b/tests/ha/conftest.py index 5ac1d4c95b9..2eae42b3405 100644 --- a/tests/ha/conftest.py +++ b/tests/ha/conftest.py @@ -1,5 +1,6 @@ import pytest import logging +import time import random import json from pathlib import Path @@ -31,11 +32,6 @@ ENABLE_GNMI_API = True -@pytest.fixture(scope="module") -def setup_ha_config(duthosts): - pass - - @pytest.fixture(scope="module") def setup_dash_ha_from_json(duthosts): pass From 98b097d752aa445bfe2cd0d80eef9e5e7a78edf3 Mon Sep 17 00:00:00 2001 From: Mihut Aronovici Date: Tue, 10 Feb 2026 17:07:08 -0800 Subject: [PATCH 09/19] move the duplicate arguments in the common conftest --- tests/conftest.py | 8 +++++++- tests/dash/conftest.py | 38 -------------------------------------- tests/ha/conftest.py | 20 ++++++++++++++++++++ 3 files changed, 27 insertions(+), 39 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0963f2ae611..43434d444a3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -158,11 +158,17 @@ def pytest_addoption(parser): parser.addoption('--fw-pkg', action='store', help='Firmware package file') ##################################### - # dash, vxlan, route shared options # + # ha, dash, vxlan, route shared options # ##################################### parser.addoption("--skip_cleanup", action="store_true", help="Skip config cleanup after test (tests: dash, vxlan)") parser.addoption("--num_routes", action="store", default=None, type=int, help="Number of routes (tests: route, vxlan)") + parser.addoption("--skip_cert_cleanup", action="store_true",help="Skip certificates cleanup after test") + parser.addoption("--skip_config", action="store_true", help="Don't apply configurations on DUT") + parser.addoption("--vxlan_udp_dport", action="store", default="random", + help="The vxlan udp dst port used in the test") + parser.addoption("--dpu_index", action="store", default=0, type=int, + help="The default dpu used for the test") ############################ # pfc_asym options # diff --git a/tests/dash/conftest.py b/tests/dash/conftest.py index d297feeddcb..a88648a5283 100644 --- a/tests/dash/conftest.py +++ b/tests/dash/conftest.py @@ -35,50 +35,12 @@ def pytest_addoption(parser): Adds pytest options that are used by DASH tests """ - parser.addoption( - "--skip_config", - action="store_true", - help="Don't apply configurations on DUT" - ) - - parser.addoption( - "--config_only", - action="store_true", - help="Apply new configurations on DUT without running tests" - ) - parser.addoption( "--skip_dataplane_checking", action="store_true", help="Skip dataplane checking" ) - parser.addoption( - "--vxlan_udp_dport", - action="store", - default="random", - help="The vxlan udp dst port used in the test" - ) - - parser.addoption( - "--skip_cert_cleanup", - action="store_true", - help="Skip certificates cleanup after test" - ) - - parser.addoption( - "--dpu_index", - action="store", - default=0, - type=int, - help="The default dpu used for the test" - ) - - -@pytest.fixture(scope="module") -def config_only(request): - return request.config.getoption("--config_only") - @pytest.fixture(scope="module") def skip_config(request): diff --git a/tests/ha/conftest.py b/tests/ha/conftest.py index 2eae42b3405..edc927a4af8 100644 --- a/tests/ha/conftest.py +++ b/tests/ha/conftest.py @@ -160,6 +160,26 @@ def get_interface_ip(duthost, interface): return ip_interface(output) +@pytest.fixture(scope="module") +def config_only(request): + return request.config.getoption("--config_only") + + +@pytest.fixture(scope="module") +def skip_config(request): + return request.config.getoption("--skip_config") + + +@pytest.fixture(scope="module") +def skip_cleanup(request): + return request.config.getoption("--skip_cleanup") + + +@pytest.fixture(scope="module") +def skip_cert_cleanup(request): + return request.config.getoption("--skip_cert_cleanup") + + @pytest.fixture(scope="module") def config_facts(duthost): return duthost.config_facts(host=duthost.hostname, source="running")['ansible_facts'] From 4753229bfbddc012b7c033a1c885490e60beaf0e Mon Sep 17 00:00:00 2001 From: Mihut Aronovici Date: Tue, 10 Feb 2026 17:12:02 -0800 Subject: [PATCH 10/19] remove this test file that is not needed --- tests/ha/test_dash_privatelink.py | 101 ------------------------------ 1 file changed, 101 deletions(-) delete mode 100644 tests/ha/test_dash_privatelink.py diff --git a/tests/ha/test_dash_privatelink.py b/tests/ha/test_dash_privatelink.py deleted file mode 100644 index a3a2744b5db..00000000000 --- a/tests/ha/test_dash_privatelink.py +++ /dev/null @@ -1,101 +0,0 @@ -import logging - -import configs.privatelink_config as pl -import ptf.testutils as testutils -import pytest -from constants import LOCAL_PTF_INTF, REMOTE_PTF_RECV_INTF, REMOTE_PTF_SEND_INTF -from gnmi_utils import apply_messages -from packets import outbound_pl_packets, inbound_pl_packets -from tests.common.config_reload import config_reload - -logger = logging.getLogger(__name__) - -pytestmark = [ - pytest.mark.topology('t1-smartswitch-ha'), - pytest.mark.skip_check_dut_health -] - - -""" -Test prerequisites: -- Assign IPs to DPU-NPU dataplane interfaces -""" - - -@pytest.fixture(autouse=True, scope="function") -def common_setup_teardown( - localhost, - duthosts, - ptfhost, - dpu_index, - skip_config, - dpuhosts, - set_vxlan_udp_sport_range, - setup_npu_dpu # noqa: F811 -): - if skip_config: - return - for i in range(len(duthosts)): - duthost = duthosts[i] - dpuhost = dpuhosts[i] - base_config_messages = { - **pl.APPLIANCE_CONFIG, - **pl.ROUTING_TYPE_PL_CONFIG, - **pl.VNET_CONFIG, - **pl.ROUTE_GROUP1_CONFIG, - **pl.METER_POLICY_V4_CONFIG - } - logger.info(f"configure on {duthost.hostname} dpu {dpuhost.dpu_index} {base_config_messages}") - - apply_messages(localhost, duthost, ptfhost, base_config_messages, dpuhost.dpu_index) - - route_and_mapping_messages = { - **pl.PE_VNET_MAPPING_CONFIG, - **pl.PE_SUBNET_ROUTE_CONFIG, - **pl.VM_SUBNET_ROUTE_CONFIG - } - if 'bluefield' in dpuhost.facts['asic_type']: - route_and_mapping_messages.update({ - **pl.INBOUND_VNI_ROUTE_RULE_CONFIG - }) - - logger.info(route_and_mapping_messages) - apply_messages(localhost, duthost, ptfhost, route_and_mapping_messages, dpuhost.dpu_index) - - meter_rule_messages = { - **pl.METER_RULE1_V4_CONFIG, - **pl.METER_RULE2_V4_CONFIG, - } - logger.info(meter_rule_messages) - apply_messages(localhost, duthost, ptfhost, meter_rule_messages, dpuhost.dpu_index) - - logger.info(pl.ENI_CONFIG) - apply_messages(localhost, duthost, ptfhost, pl.ENI_CONFIG, dpuhost.dpu_index) - - logger.info(pl.ENI_ROUTE_GROUP1_CONFIG) - apply_messages(localhost, duthost, ptfhost, pl.ENI_ROUTE_GROUP1_CONFIG, dpuhost.dpu_index) - - yield - - config_reload(dpuhost, safe_reload=True, yang_validate=False) - # apply_messages(localhost, duthost, ptfhost, pl.ENI_ROUTE_GROUP1_CONFIG, dpuhost.dpu_index, False) - # apply_messages(localhost, duthost, ptfhost, pl.ENI_CONFIG, dpuhost.dpu_index, False) - # apply_messages(localhost, duthost, ptfhost, meter_rule_messages, dpuhost.dpu_index, False) - # apply_messages(localhost, duthost, ptfhost, route_and_mapping_messages, dpuhost.dpu_index, False) - # apply_messages(localhost, duthost, ptfhost, base_config_messages, dpuhost.dpu_index, False) - - -@pytest.mark.parametrize("encap_proto", ["vxlan", "gre"]) -def test_privatelink_basic_transform( - ptfadapter, - dash_pl_config, - encap_proto -): - vm_to_dpu_pkt, exp_dpu_to_pe_pkt = outbound_pl_packets(dash_pl_config[0], encap_proto) - pe_to_dpu_pkt, exp_dpu_to_vm_pkt = inbound_pl_packets(dash_pl_config[0]) - - ptfadapter.dataplane.flush() - testutils.send(ptfadapter, dash_pl_config[0][LOCAL_PTF_INTF], vm_to_dpu_pkt, 1) - testutils.verify_packet_any_port(ptfadapter, exp_dpu_to_pe_pkt, dash_pl_config[0][REMOTE_PTF_RECV_INTF]) - testutils.send(ptfadapter, dash_pl_config[0][REMOTE_PTF_SEND_INTF], pe_to_dpu_pkt, 1) - testutils.verify_packet(ptfadapter, exp_dpu_to_vm_pkt, dash_pl_config[0][LOCAL_PTF_INTF]) From 78f59e47b8073226826263ce09410469f562d9da Mon Sep 17 00:00:00 2001 From: Mihut Aronovici Date: Wed, 11 Feb 2026 19:42:52 -0800 Subject: [PATCH 11/19] change scope for some fixtures to insure correct execution order --- tests/ha/conftest.py | 20 ++------------------ tests/ha/test_ha_dash_privatelink.py | 1 + 2 files changed, 3 insertions(+), 18 deletions(-) diff --git a/tests/ha/conftest.py b/tests/ha/conftest.py index edc927a4af8..929f7ccfaaa 100644 --- a/tests/ha/conftest.py +++ b/tests/ha/conftest.py @@ -185,22 +185,6 @@ def config_facts(duthost): return duthost.config_facts(host=duthost.hostname, source="running")['ansible_facts'] -@pytest.fixture(scope="module") -def minigraph_facts(duthosts, rand_one_dut_hostname, tbinfo): - """ - Fixture to get minigraph facts - - Args: - duthost: DUT host object - - Returns: - Dictionary containing minigraph information - """ - duthost = duthosts[rand_one_dut_hostname] - - return duthost.get_extended_minigraph_facts(tbinfo) - - def get_intf_from_ip(local_ip, config_facts): for intf, config in list(config_facts["INTERFACE"].items()): for ip in config: @@ -220,7 +204,7 @@ def use_underlay_route(request): return request.param == "with-underlay-route" -@pytest.fixture(scope="module") +@pytest.fixture(scope="function") def dash_pl_config(duthosts, dpuhosts, dpu_index, duts_minigraph_facts): dash_info = [{ LOCAL_CA_IP: "10.2.2.2", @@ -292,7 +276,7 @@ def _apply_config(config_info): _apply_config(config_info) -@pytest.fixture(scope="module", autouse=True) +@pytest.fixture(scope="function") def setup_gnmi_server(duthosts, localhost, ptfhost, skip_cert_cleanup): if not ENABLE_GNMI_API: yield diff --git a/tests/ha/test_ha_dash_privatelink.py b/tests/ha/test_ha_dash_privatelink.py index 98b03b62b47..5d324cd859e 100644 --- a/tests/ha/test_ha_dash_privatelink.py +++ b/tests/ha/test_ha_dash_privatelink.py @@ -31,6 +31,7 @@ def common_setup_teardown( dpuhosts, setup_ha_config, setup_dash_ha_from_json, + setup_gnmi_server, set_vxlan_udp_sport_range, setup_npu_dpu # noqa: F811 ): From 1698bd77b173f12ef981cabfc01576c91ef9e355 Mon Sep 17 00:00:00 2001 From: Mihut Aronovici Date: Fri, 13 Feb 2026 09:22:50 -0800 Subject: [PATCH 12/19] fix flake8 issue in conftest --- tests/conftest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 43434d444a3..c2550ae2eac 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -163,12 +163,12 @@ def pytest_addoption(parser): parser.addoption("--skip_cleanup", action="store_true", help="Skip config cleanup after test (tests: dash, vxlan)") parser.addoption("--num_routes", action="store", default=None, type=int, help="Number of routes (tests: route, vxlan)") - parser.addoption("--skip_cert_cleanup", action="store_true",help="Skip certificates cleanup after test") + parser.addoption("--skip_cert_cleanup", action="store_true", help="Skip certificates cleanup after test") parser.addoption("--skip_config", action="store_true", help="Don't apply configurations on DUT") parser.addoption("--vxlan_udp_dport", action="store", default="random", - help="The vxlan udp dst port used in the test") + help="The vxlan udp dst port used in the test") parser.addoption("--dpu_index", action="store", default=0, type=int, - help="The default dpu used for the test") + help="The default dpu used for the test") ############################ # pfc_asym options # From b3fdb8f553b6060f69d255e6147fc00048c00fd2 Mon Sep 17 00:00:00 2001 From: Mihut Aronovici Date: Fri, 13 Feb 2026 10:19:20 -0800 Subject: [PATCH 13/19] rename the test --- .../{test_ha_dash_privatelink.py => test_ha_steady_state_pl.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/ha/{test_ha_dash_privatelink.py => test_ha_steady_state_pl.py} (100%) diff --git a/tests/ha/test_ha_dash_privatelink.py b/tests/ha/test_ha_steady_state_pl.py similarity index 100% rename from tests/ha/test_ha_dash_privatelink.py rename to tests/ha/test_ha_steady_state_pl.py From 4467385f4003bf3864170ed2c23894b1a96dbca3 Mon Sep 17 00:00:00 2001 From: Mihut Aronovici Date: Wed, 18 Feb 2026 08:36:58 -0800 Subject: [PATCH 14/19] skip the ha tests on anything but smartswitch platforms --- .../conditional_mark/tests_mark_conditions.yaml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/common/plugins/conditional_mark/tests_mark_conditions.yaml b/tests/common/plugins/conditional_mark/tests_mark_conditions.yaml index 854fbc90c28..a5ce64c4320 100644 --- a/tests/common/plugins/conditional_mark/tests_mark_conditions.yaml +++ b/tests/common/plugins/conditional_mark/tests_mark_conditions.yaml @@ -2760,6 +2760,17 @@ gnmi/test_gnoi_system_reboot.py::test_gnoi_system_reboot_warm: - "release in ['202412']" - "'f2' in topo_name" +####################################### +##### ha ##### +####################################### +ha/test_ha_steady_state_pl.py: + skip: + conditions_logical_operator: or + reason: "Currently ha tests are not supported on KVM or non-smartswitch T1s" + conditions: + - "asic_type in ['vs'] and https://github.com/sonic-net/sonic-mgmt/issues/16407" + - "hwsku not in ['Cisco-8102-28FH-DPU-O-T1', 'Mellanox-SN4280-O8C40', 'Mellanox-SN4280-O28', 'Cisco-8102-28FH-DPU-O']" + ####################################### ##### hash ##### ####################################### From 107ebd0d241d1ad4c2786d172d52b11dd949c431 Mon Sep 17 00:00:00 2001 From: Mihut Aronovici Date: Mon, 23 Feb 2026 15:22:05 -0800 Subject: [PATCH 15/19] fix a PR comment --- tests/ha/test_ha_steady_state_pl.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/ha/test_ha_steady_state_pl.py b/tests/ha/test_ha_steady_state_pl.py index 5d324cd859e..dad62871160 100644 --- a/tests/ha/test_ha_steady_state_pl.py +++ b/tests/ha/test_ha_steady_state_pl.py @@ -81,7 +81,8 @@ def common_setup_teardown( yield - config_reload(dpuhost, safe_reload=True, yang_validate=False) + for dpuhost in dpuhosts: + config_reload(dpuhost, safe_reload=True, yang_validate=False) # apply_messages(localhost, duthost, ptfhost, pl.ENI_ROUTE_GROUP1_CONFIG, dpuhost.dpu_index, False) # apply_messages(localhost, duthost, ptfhost, pl.ENI_CONFIG, dpuhost.dpu_index, False) # apply_messages(localhost, duthost, ptfhost, meter_rule_messages, dpuhost.dpu_index, False) From 1a5a20e1806fe243f6a3531f90abd68ef23931a2 Mon Sep 17 00:00:00 2001 From: Mihut Aronovici Date: Tue, 24 Feb 2026 18:51:52 -0800 Subject: [PATCH 16/19] fixes after merge with latest master --- tests/ha/conftest.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/tests/ha/conftest.py b/tests/ha/conftest.py index 67546aecda5..3302e89cf9f 100644 --- a/tests/ha/conftest.py +++ b/tests/ha/conftest.py @@ -1,4 +1,3 @@ -import logging import pytest import logging import time @@ -8,7 +7,6 @@ from collections import defaultdict import os -from tests.common.config_reload import config_reload from tests.common.helpers.constants import DEFAULT_NAMESPACE from tests.common.ha.smartswitch_ha_helper import PtfTcpTestAdapter from tests.common.ha.smartswitch_ha_io import SmartSwitchHaTrafficTest @@ -30,21 +28,6 @@ from tests.common import config_reload import configs.privatelink_config as pl from tests.common.helpers.assertions import pytest_require as pt_require - -logger = logging.getLogger(__name__) - -ENABLE_GNMI_API = True - - -@pytest.fixture(scope="module") -def setup_dash_ha_from_json(duthosts): - pass - - -@pytest.fixture(scope="module") -def activate_dash_ha_from_json(duthosts): - pass - from tests.ha.ha_utils import ( build_dash_ha_scope_args, @@ -54,7 +37,7 @@ def activate_dash_ha_from_json(duthosts): build_dash_ha_set_args, proto_utils_hset ) - +ENABLE_GNMI_API = True logger = logging.getLogger(__name__) From 3623e01915195fc988c217679f43e330108cc5e1 Mon Sep 17 00:00:00 2001 From: Mihut Aronovici Date: Tue, 24 Feb 2026 19:34:51 -0800 Subject: [PATCH 17/19] fix PR sanity error --- tests/dash/test_dash_metering.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/dash/test_dash_metering.py b/tests/dash/test_dash_metering.py index 376f5ade21f..b8851248cc4 100644 --- a/tests/dash/test_dash_metering.py +++ b/tests/dash/test_dash_metering.py @@ -10,7 +10,7 @@ from tests.common.helpers.assertions import pytest_assert from configs.privatelink_config import TUNNEL1_ENDPOINT_IPS, TUNNEL2_ENDPOINT_IPS from tests.common import config_reload -from tests.dash.dash_utils import verify_tunnel_packets +from tests.common.dash_utils import verify_tunnel_packets from dash_eni_counter_utils import get_eni_counter_oid, get_eni_meter_counters logger = logging.getLogger(__name__) From 86b2ea8b91406b44654f32f91c2e38242c340e53 Mon Sep 17 00:00:00 2001 From: Mihut Aronovici Date: Thu, 26 Feb 2026 09:24:38 -0800 Subject: [PATCH 18/19] HA activation needs to be function scope and not module --- tests/ha/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ha/conftest.py b/tests/ha/conftest.py index 3302e89cf9f..ce4b8c4bfef 100644 --- a/tests/ha/conftest.py +++ b/tests/ha/conftest.py @@ -774,7 +774,7 @@ def setup_dash_ha_from_json(duthosts): ) -@pytest.fixture(scope="module") +@pytest.fixture(scope="function") def activate_dash_ha_from_json(duthosts): # ------------------------------------------------- # Step 4: Activate Role (using pending_operation_ids) From c99a7a9d5d1696af9e93d6049a801d82cb9f2150 Mon Sep 17 00:00:00 2001 From: Mihut Aronovici Date: Thu, 26 Feb 2026 13:02:33 -0800 Subject: [PATCH 19/19] adding back the session fixtures that are needed --- tests/ha/conftest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/ha/conftest.py b/tests/ha/conftest.py index ce4b8c4bfef..6271cc34976 100644 --- a/tests/ha/conftest.py +++ b/tests/ha/conftest.py @@ -24,6 +24,7 @@ REMOTE_PTF_SEND_INTF, REMOTE_PTF_RECV_INTF, VXLAN_UDP_BASE_SRC_PORT, VXLAN_UDP_SRC_PORT_MASK, \ NPU_DATAPLANE_IP, NPU_DATAPLANE_MAC, NPU_DATAPLANE_PORT, DPU_DATAPLANE_IP, DPU_DATAPLANE_MAC, DPU_DATAPLANE_PORT from tests.common.dash_utils import render_template_to_host, apply_swssconfig_file +from tests.common.helpers.smartswitch_util import correlate_dpu_info_with_dpuhost, get_data_port_on_dpu, get_dpu_dataplane_port # noqa F401 from gnmi_utils import generate_gnmi_cert, apply_gnmi_cert, recover_gnmi_cert, apply_gnmi_file from tests.common import config_reload import configs.privatelink_config as pl @@ -686,6 +687,7 @@ def setup_ha_config(duthosts): final_cfg = {} + logger.info("HA: setup config") for switch_id in (0, 1): dut = duthosts[switch_id] cfg = generate_ha_config_for_dut(switch_id) @@ -724,6 +726,7 @@ def setup_dash_ha_from_json(duthosts): base_dir = os.path.join(current_dir, "..", "common", "ha") ha_set_file = os.path.join(base_dir, "dash_ha_set_dpu_config_table.json") + logger.info("HA: setup from json") with open(ha_set_file) as f: ha_set_data = json.load(f)["DASH_HA_SET_CONFIG_TABLE"] @@ -801,6 +804,7 @@ def activate_dash_ha_from_json(duthosts): }, ), ] + logger.info("HA: activate") for duthost, (key, fields) in zip(duthosts, activate_scope_per_dut): proto_utils_hset( duthost,