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/common/plugins/conditional_mark/tests_mark_conditions.yaml b/tests/common/plugins/conditional_mark/tests_mark_conditions.yaml index ea90185bd11..062518e4b74 100644 --- a/tests/common/plugins/conditional_mark/tests_mark_conditions.yaml +++ b/tests/common/plugins/conditional_mark/tests_mark_conditions.yaml @@ -2782,6 +2782,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 ##### ####################################### diff --git a/tests/conftest.py b/tests/conftest.py index 89507a14510..e1375d72322 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -173,11 +173,17 @@ def pytest_addoption(parser): parser.addoption('--minigraph2', action='store', type=str, help='path to the minigraph2') ##################################### - # 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") ############################ # sflow options # diff --git a/tests/dash/conftest.py b/tests/dash/conftest.py index 1c40ee381c8..a88648a5283 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 @@ -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/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 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_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__) 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/configs/privatelink_config.py b/tests/ha/configs/privatelink_config.py new file mode 100644 index 00000000000..9e9793f0d37 --- /dev/null +++ b/tests/ha/configs/privatelink_config.py @@ -0,0 +1,192 @@ +from dash_api.eni_pb2 import State +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 da62cfcd340..6271cc34976 100644 --- a/tests/ha/conftest.py +++ b/tests/ha/conftest.py @@ -1,12 +1,12 @@ -import logging import pytest +import logging +import time +import random +import json from pathlib import Path from collections import defaultdict import os -import json -import time -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 @@ -16,7 +16,19 @@ add_static_route_to_ptf, add_static_route_to_dut ) - +from ipaddress import ip_interface +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 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 +from tests.common.helpers.assertions import pytest_require as pt_require from tests.ha.ha_utils import ( build_dash_ha_scope_args, @@ -26,7 +38,7 @@ build_dash_ha_set_args, proto_utils_hset ) - +ENABLE_GNMI_API = True logger = logging.getLogger(__name__) @@ -141,6 +153,289 @@ def setup_namespaces_with_routes(ptfhost, duthosts, get_t2_info): 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) + + +@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'] + + +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="function") +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 + + 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="function") +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", 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 +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 + 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") + + +@pytest.fixture(scope="function") +def add_npu_static_routes( + duthosts, dash_pl_config, skip_config, skip_cleanup, dpu_index, dpuhosts +): + 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}") + duthost.shell_cmds(cmds=cmds) + + +@pytest.fixture(scope="function") +def setup_npu_dpu(dpu_setup, add_npu_static_routes): + yield ############################################################################### # VLAN CONFIG (COMMON) ############################################################################### @@ -392,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) @@ -430,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"] @@ -480,7 +777,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) @@ -507,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, 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/gnmi_utils.py b/tests/ha/gnmi_utils.py new file mode 100644 index 00000000000..bf884c36380 --- /dev/null +++ b/tests/ha/gnmi_utils.py @@ -0,0 +1,458 @@ +import json +import logging +import math +import time +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"/DPU_APPL_DB/dpu{dpu_index}/{gnmi_key}:$/root/{filename}" + else: + 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"/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) + 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 = "/DPU_APPL_DB/%s/%s:$/root/%s" % (host, k, filename) + else: + 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(): + if k == "OP": + continue + keys = k.split(":", 1) + k = keys[0] + "[key=" + keys[1] + "]" + path = "/DPU_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..4ac782feb58 --- /dev/null +++ b/tests/ha/packets.py @@ -0,0 +1,229 @@ +import logging +import random +from ipaddress import ip_address + +import ptf.packet as scapy +import ptf.testutils as testutils +from configs import privatelink_config as pl +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__) + + +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 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 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 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 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_ha_steady_state_pl.py b/tests/ha/test_ha_steady_state_pl.py new file mode 100644 index 00000000000..dad62871160 --- /dev/null +++ b/tests/ha/test_ha_steady_state_pl.py @@ -0,0 +1,107 @@ +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, + setup_ha_config, + setup_dash_ha_from_json, + setup_gnmi_server, + 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 + + 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) + # 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, + activate_dash_ha_from_json, + 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])