diff --git a/tests/common/devices/sonic.py b/tests/common/devices/sonic.py index b9bed2ba05b..b74d1681b13 100644 --- a/tests/common/devices/sonic.py +++ b/tests/common/devices/sonic.py @@ -2405,7 +2405,7 @@ def is_backend_portchannel(self, port_channel, mg_facts): def is_backend_port(self, port, mg_facts): return True if "Ethernet-BP" in port else False - def active_ip_interfaces(self, ip_ifs, tbinfo, ns_arg=DEFAULT_NAMESPACE, intf_num="all"): + def active_ip_interfaces(self, ip_ifs, tbinfo, ns_arg=DEFAULT_NAMESPACE, intf_num="all", ip_type="ipv4"): """ Return a dict of active IP (Ethernet or PortChannel) interfaces, with interface and peer IPv4 address. @@ -2421,16 +2421,26 @@ def active_ip_interfaces(self, ip_ifs, tbinfo, ns_arg=DEFAULT_NAMESPACE, intf_nu if ((k.startswith("Ethernet") and config_facts_ports.get(k, {}).get("role", "") != "Dpc" and (not k.startswith("Ethernet-BP")) and not is_inband_port(k)) or (k.startswith("PortChannel") and not self.is_backend_portchannel(k, mg_facts))): - # Ping for some time to get ARP Re-learnt. - # We might have to tune it further if needed. - if (v["admin"] == "up" and v["oper_state"] == "up" and - self.ping_v4(v["peer_ipv4"], count=3, ns_arg=ns_arg)): - ip_ifaces[k] = { - "ipv4": v["ipv4"], - "peer_ipv4": v["peer_ipv4"], - "bgp_neighbor": v["bgp_neighbor"] - } - active_ip_intf_cnt += 1 + if ip_type == "ipv4": + # Ping for some time to get ARP Re-learnt. + # We might have to tune it further if needed. + if (v["admin"] == "up" and v["oper_state"] == "up" and + self.ping_v4(v["peer_ipv4"], count=3, ns_arg=ns_arg)): + ip_ifaces[k] = { + "ipv4": v["ipv4"], + "peer_ipv4": v["peer_ipv4"], + "bgp_neighbor": v["bgp_neighbor"] + } + active_ip_intf_cnt += 1 + elif ip_type == "ipv6": + if (v["admin"] == "up" and v["oper_state"] == "up" and + self.ping_v6(v["peer_ipv6"], count=3, ns_arg=ns_arg)): + ip_ifaces[k] = { + "ipv6": v["ipv6"], + "peer_ipv6": v["peer_ipv6"], + "bgp_neighbor": v["bgp_neighbor"] + } + active_ip_intf_cnt += 1 if isinstance(intf_num, int) and intf_num > 0 and active_ip_intf_cnt == intf_num: break diff --git a/tests/common/devices/sonic_asic.py b/tests/common/devices/sonic_asic.py index 9f68647f056..c5c39bdcd01 100644 --- a/tests/common/devices/sonic_asic.py +++ b/tests/common/devices/sonic_asic.py @@ -321,7 +321,7 @@ def is_backend_portchannel(self, port_channel): return False return True - def get_active_ip_interfaces(self, tbinfo, intf_num="all"): + def get_active_ip_interfaces(self, tbinfo, intf_num="all", ip_type="ipv4"): """ Return a dict of active IP (Ethernet or PortChannel) interfaces, with interface and peer IPv4 address. @@ -329,9 +329,15 @@ def get_active_ip_interfaces(self, tbinfo, intf_num="all"): Returns: Dict of Interfaces and their IPv4 address """ - ip_ifs = self.show_ip_interface()["ansible_facts"]["ip_interfaces"] + if ip_type == "ipv4": + ip_ifs = self.show_ip_interface()["ansible_facts"]["ip_interfaces"] + elif ip_type == "ipv6": + ip_ifs = self.show_ipv6_interface()["ansible_facts"]["ipv6_interfaces"] + else: + raise ValueError("Invalid IP type: {}".format(ip_type)) + return self.sonichost.active_ip_interfaces( - ip_ifs, tbinfo, self.namespace, intf_num=intf_num + ip_ifs, tbinfo, self.namespace, intf_num=intf_num, ip_type=ip_type ) def bgp_drop_rule(self, ip_version, state="present"): diff --git a/tests/qos/qos_sai_base.py b/tests/qos/qos_sai_base.py index 38fa7784862..ccae1fc1b9e 100644 --- a/tests/qos/qos_sai_base.py +++ b/tests/qos/qos_sai_base.py @@ -35,6 +35,8 @@ from tests.common.snappi_tests.qos_fixtures import get_pfcwd_config, reapply_pfcwd from tests.common.snappi_tests.common_helpers import \ stop_pfcwd, disable_packet_aging, enable_packet_aging +from tests.common.utilities import is_ipv6_only_topology + logger = logging.getLogger(__name__) @@ -150,8 +152,11 @@ def runPtfTest(self, ptfhost, testCase='', testParams={}, relax=False, pdb=False Raises: RunAnsibleModuleFail if ptf test fails """ - custom_options = " --disable-ipv6 --disable-vxlan --disable-geneve" \ + ip_type = testParams.get("ip_type", "ipv4") + custom_options = " --disable-vxlan --disable-geneve" \ " --disable-erspan --disable-mpls --disable-nvgre" + if ip_type != "ipv6": + custom_options += " --disable-ipv6" # Append a suffix to the logfile name if log_suffix is present in testParams log_suffix = testParams.get("log_suffix", "") logfile_suffix = "_{0}".format(log_suffix) if log_suffix else "" @@ -959,7 +964,7 @@ def configure_ip_on_ptf_intfs(self, ptfhost, get_src_dst_asic_and_duts, tbinfo): def dutConfig( self, request, duthosts, configure_ip_on_ptf_intfs, get_src_dst_asic_and_duts, lower_tor_host, tbinfo, dualtor_ports_for_duts, dut_qos_maps, # noqa: F811 - is_supported_per_dir, lossy_queue_traffic_direction + is_supported_per_dir, lossy_queue_traffic_direction, ip_type ): """ Build DUT host config pertaining to QoS SAI tests @@ -1006,6 +1011,9 @@ def dutConfig( for key, value in src_mgFacts['minigraph_ports'].items() if not key.startswith("Ethernet-BP") } + bgp_peer_ip_key = "peer_ipv6" if ip_type == "ipv6" else "peer_ipv4" + ip_version = 6 if ip_type == "ipv6" else 4 + vlan_info = {} # LAG ports in T1 TOPO need to be removed in Mellanox devices if topo in self.SUPPORTED_T0_TOPOS or (topo in self.SUPPORTED_PTF_TOPOS and isMellanoxDevice(src_dut)): @@ -1021,6 +1029,7 @@ def dutConfig( dutLagInterfaces.append(src_mgFacts["minigraph_ptf_indices"][intf]) config_facts = duthosts.config_facts(host=src_dut.hostname, source="running") + vlan_info = config_facts[src_dut.hostname]['VLAN'] port_speeds = self.__buildPortSpeeds(config_facts[src_dut.hostname]) low_speed_portIds = [] if src_dut.facts['hwsku'] in self.BREAKOUT_SKUS and 'backend' not in topo: @@ -1054,7 +1063,7 @@ def dutConfig( for portConfig in intf_map: intf = portConfig["attachto"].split(".")[0] portIndex = src_mgFacts["minigraph_ptf_indices"][intf] - if ipaddress.ip_interface(portConfig['peer_addr']).ip.version == 4: + if ipaddress.ip_interface(portConfig['peer_addr']).ip.version == ip_version: if portIndex in testPortIds[src_dut_index][src_asic_index]: portIpMap = {'peer_addr': portConfig["peer_addr"]} if 'vlan' in portConfig: @@ -1114,14 +1123,14 @@ def dutConfig( testPortIds[src_dut_index] = {} for dut_asic in get_src_dst_asic_and_duts['all_asics']: dutPortIps[src_dut_index][dut_asic.asic_index] = {} - for iface, addr in dut_asic.get_active_ip_interfaces(tbinfo).items(): + for iface, addr in dut_asic.get_active_ip_interfaces(tbinfo, ip_type=ip_type).items(): vlan_id = None if iface.startswith("Ethernet"): portName = iface if "." in iface: portName, vlan_id = iface.split(".") portIndex = src_mgFacts["minigraph_ptf_indices"][portName] - portIpMap = {'peer_addr': addr["peer_ipv4"]} + portIpMap = {'peer_addr': addr[bgp_peer_ip_key]} if vlan_id is not None: portIpMap['vlan_id'] = vlan_id dutPortIps[src_dut_index][dut_asic.asic_index].update({portIndex: portIpMap}) @@ -1130,7 +1139,7 @@ def dutConfig( iter(src_mgFacts["minigraph_portchannels"][iface]["members"]) ) portIndex = src_mgFacts["minigraph_ptf_indices"][portName] - portIpMap = {'peer_addr': addr["peer_ipv4"]} + portIpMap = {'peer_addr': addr[bgp_peer_ip_key]} dutPortIps[src_dut_index][dut_asic.asic_index].update({portIndex: portIpMap}) # If the leaf router is using separated DSCP_TO_TC_MAP on uplink/downlink ports. # we also need to test them separately @@ -1142,11 +1151,11 @@ def dutConfig( neighName = src_mgFacts["minigraph_neighbors"].get(portName, {}).get("name", "").lower() if 't0' in neighName: downlinkPortIds.append(portIndex) - downlinkPortIps.append(addr["peer_ipv4"]) + downlinkPortIps.append(addr[bgp_peer_ip_key]) downlinkPortNames.append(portName) elif 't2' in neighName: uplinkPortIds.append(portIndex) - uplinkPortIps.append(addr["peer_ipv4"]) + uplinkPortIps.append(addr[bgp_peer_ip_key]) uplinkPortNames.append(portName) testPortIds[src_dut_index][dut_asic.asic_index] = sorted( @@ -1387,7 +1396,9 @@ def dutConfig( "srcDutInstance": src_dut, "dstDutInstance": dst_dut, "dualTor": request.config.getoption("--qos_dual_tor"), - "dualTorScenario": len(dualtor_ports_for_duts) != 0 and "dualtor" not in tbinfo["topo"]["name"] + "dualTorScenario": len(dualtor_ports_for_duts) != 0 and "dualtor" not in tbinfo["topo"]["name"], + "ip_type": ip_type, + "vlan_info": vlan_info } @pytest.fixture(scope='class') @@ -1936,6 +1947,42 @@ def handleFdbAging(self, duthosts, get_src_dst_asic_and_duts): self.__loadSwssConfig(duthost) self.__deleteTmpSwitchConfig(duthost) + @pytest.fixture(scope='class', autouse=True) + def update_delay_first_probe_time_for_v6_top(self, get_src_dst_asic_and_duts, tbinfo, dutConfig): + """ + Update delay first probe time for v6 t0 topology. + Because when generating arp by sending NS packet, the default delay first probe time is 5 seconds, + which is too short to let the ip neighbor status changed from delay to probe, then to fail. + Therefore, we need to update the delay first probe time to a very large value + so that the arp entries can work during executing the qos sai case. + """ + ip_type = dutConfig.get('ip_type', 'ipv4') + if ip_type != 'ipv6' or 't0' not in tbinfo["topo"]["type"]: + yield + return + + Vlan_name = list(dutConfig['vlan_info'].keys())[0] + dut_asic = get_src_dst_asic_and_duts['src_asic'] + file_path_v6_delay_first_probe_time = f"/proc/sys/net/ipv6/neigh/{Vlan_name}/delay_first_probe_time" + cmd_get_v6_delay_first_probe_time = f"cat {file_path_v6_delay_first_probe_time}" + + # a very large value 100000 seconds which far exceeds the qos sai case execution time + # so that the status of tested ip v6 neighbor item not be changed from delay to probe, then to fail. + new_v6_delay_first_probe_time = 100000 + + original_v6_delay_first_probe_time = dut_asic.shell(cmd_get_v6_delay_first_probe_time)['stdout'] + + cmd_update_v6_delay_first_probe_time = \ + f"echo {new_v6_delay_first_probe_time} | sudo tee {file_path_v6_delay_first_probe_time}" + dut_asic.shell(cmd_update_v6_delay_first_probe_time) + + yield + + cmd_restore_v6_delay_first_probe_time = \ + f"echo {original_v6_delay_first_probe_time} | sudo tee {file_path_v6_delay_first_probe_time}" + dut_asic.shell(cmd_restore_v6_delay_first_probe_time) + return + @pytest.fixture(scope='function', autouse=True) def populateArpEntries_T2( self, duthosts, get_src_dst_asic_and_duts, ptfhost, dutTestParams, dutConfig): @@ -1983,7 +2030,8 @@ def populateArpEntries_T2( @pytest.fixture(scope='class', autouse=True) def populateArpEntries( self, duthosts, get_src_dst_asic_and_duts, lossy_queue_traffic_direction, - ptfhost, dutTestParams, dutConfig, releaseAllPorts, handleFdbAging, tbinfo, lower_tor_host # noqa: F811 + ptfhost, dutTestParams, dutConfig, releaseAllPorts, handleFdbAging, tbinfo, lower_tor_host, # noqa: F811 + ip_type, update_delay_first_probe_time_for_v6_top # noqa: F811 ): """ Update ARP entries of QoS SAI test ports @@ -2012,13 +2060,19 @@ def populateArpEntries( self.populate_arp_entries( get_src_dst_asic_and_duts, ptfhost, dutTestParams, - dutConfig, releaseAllPorts, handleFdbAging, tbinfo, lower_tor_host) + dutConfig, releaseAllPorts, handleFdbAging, tbinfo, lower_tor_host, ip_type) yield return @pytest.fixture(scope='module', autouse=True) - def dut_disable_ipv6(self, duthosts, tbinfo, lower_tor_host, swapSyncd_on_selected_duts): # noqa: F811 + def dut_disable_ipv6(self, duthosts, tbinfo, lower_tor_host, swapSyncd_on_selected_duts, ip_type): # noqa: F811 + + if ip_type == "ipv6": + logger.info("skip dut_disable_ipv6 fixture for ipv6") + yield + return + if 'dualtor' in tbinfo['topo']['name']: dut_list = [lower_tor_host] else: @@ -2677,7 +2731,8 @@ def skip_pacific_dst_asic(self, dutConfig): def populate_arp_entries( self, get_src_dst_asic_and_duts, - ptfhost, dutTestParams, dutConfig, releaseAllPorts, handleFdbAging, tbinfo, lower_tor_host # noqa: F811 + ptfhost, dutTestParams, dutConfig, releaseAllPorts, handleFdbAging, tbinfo, lower_tor_host, # noqa: F811 + ip_type='ipv4' ): """ Update ARP entries of QoS SAI test ports @@ -2688,16 +2743,20 @@ def populate_arp_entries( dut_asic.command('sonic-clear arp') saiQosTest = None - if dutTestParams["topo"] in self.SUPPORTED_T0_TOPOS: - saiQosTest = "sai_qos_tests.ARPpopulate" - elif dutTestParams["topo"] in self.SUPPORTED_PTF_TOPOS: - saiQosTest = "sai_qos_tests.ARPpopulatePTF" + if ip_type == "ipv6": + saiQosTest = "sai_qos_tests.ARPpopulateIPv6" else: - for dut_asic in get_src_dst_asic_and_duts['all_asics']: - result = dut_asic.command("arp -n") - pytest_assert(result["rc"] == 0, "failed to run arp command on {0}".format(dut_asic.sonichost.hostname)) - if result["stdout"].find("incomplete") == -1: - saiQosTest = "sai_qos_tests.ARPpopulate" + if dutTestParams["topo"] in self.SUPPORTED_T0_TOPOS: + saiQosTest = "sai_qos_tests.ARPpopulate" + elif dutTestParams["topo"] in self.SUPPORTED_PTF_TOPOS: + saiQosTest = "sai_qos_tests.ARPpopulatePTF" + else: + for dut_asic in get_src_dst_asic_and_duts['all_asics']: + result = dut_asic.command("arp -n") + pytest_assert(result["rc"] == 0, "failed to run arp command on {0}".format( + dut_asic.sonichost.hostname)) + if result["stdout"].find("incomplete") == -1: + saiQosTest = "sai_qos_tests.ARPpopulate" if saiQosTest: testParams = dutTestParams["basicParams"] @@ -2705,7 +2764,8 @@ def populate_arp_entries( testParams.update({ "testPortIds": dutConfig["testPortIds"], "testPortIps": dutConfig["testPortIps"], - "testbed_type": dutTestParams["topo"] + "testbed_type": dutTestParams["topo"], + "ip_type": ip_type }) self.runPtfTest( ptfhost, testCase=saiQosTest, testParams=testParams @@ -3013,6 +3073,11 @@ def lossy_queue_traffic_direction(self, is_supported_per_dir): logging.info(f"Device not support per dir: {lossy_queue_dir_test}") yield lossy_queue_dir_test + @pytest.fixture(scope='module', autouse=True) + def ip_type(self, tbinfo): + ip_type = "ipv6" if is_ipv6_only_topology(tbinfo) else "ipv4" + yield ip_type + def get_src_and_dst_ports_when_support_per_dir(self, uplinkPortIds, downlinkPortIds, lossy_queue_traffic_direction): if 'src_uplink_dst_downlink' == lossy_queue_traffic_direction: src_port_ids = uplinkPortIds @@ -3069,7 +3134,7 @@ def get_queue_weights_based_dynamic_th(self, duthost, queue_table_postfix_list): queue_dynamic_th_map = {} weights_list = [] for queue in queue_table_postfix_list: - key_str = f"BUFFER_PROFILE_TABLE:queue{queue}_downlink_lossy_profile" + key_str = f"BUFFER_PROFILE_TABLE:queue{queue}_downlink_lossy_profile" # noqa: E231 dynamic_th_res = duthost.run_redis_cmd(argv=["redis-cli", "-n", 0, "HGET", key_str, "dynamic_th"]) if dynamic_th_res: queue_dynamic_th_map[queue] = dynamic_th_res[0] diff --git a/tests/qos/test_qos_sai.py b/tests/qos/test_qos_sai.py index 8bb174814e6..d0f99cd7cef 100644 --- a/tests/qos/test_qos_sai.py +++ b/tests/qos/test_qos_sai.py @@ -1308,7 +1308,8 @@ def testQosSaiLossyQueue( "src_port_vlan": dutConfig["testPorts"]["src_port_vlan"], "pkts_num_leak_out": dutQosConfig["param"][portSpeedCableLength]["pkts_num_leak_out"], "pkts_num_trig_egr_drp": qosConfig["lossy_queue_1"]["pkts_num_trig_egr_drp"], - "hwsku": dutTestParams['hwsku'] + "hwsku": dutTestParams['hwsku'], + "ip_type": dutConfig["ip_type"] }) if "platform_asic" in dutTestParams["basicParams"]: @@ -1476,7 +1477,8 @@ def testQosSaiDscpQueueMapping( "hwsku": dutTestParams['hwsku'], "dual_tor": dutConfig['dualTor'], "dual_tor_scenario": dutConfig['dualTorScenario'], - "tc_to_dscp_count_map": tc_to_dscp_count + "tc_to_dscp_count_map": tc_to_dscp_count, + 'ip_type': dutConfig["ip_type"] }) if "platform_asic" in dutTestParams["basicParams"]: @@ -1783,7 +1785,8 @@ def testQosSaiPgSharedWatermark( "pkts_num_fill_min": qosConfig[pgProfile]["pkts_num_fill_min"], "pkts_num_fill_shared": pktsNumFillShared, "cell_size": qosConfig[pgProfile]["cell_size"], - "hwsku": dutTestParams['hwsku'] + "hwsku": dutTestParams['hwsku'], + "ip_type": dutConfig["ip_type"] }) if "platform_asic" in dutTestParams["basicParams"]: @@ -1992,7 +1995,8 @@ def testQosSaiQSharedWatermark( "pkts_num_trig_drp": triggerDrop, "cell_size": qosConfig[queueProfile]["cell_size"], "hwsku": dutTestParams['hwsku'], - "dut_asic": dutConfig["dutAsic"] + "dut_asic": dutConfig["dutAsic"], + "ip_type": dutConfig["ip_type"] }) if "platform_asic" in dutTestParams["basicParams"]: diff --git a/tests/saitests/py3/sai_qos_tests.py b/tests/saitests/py3/sai_qos_tests.py index db7866df4d3..1df354a9cf8 100755 --- a/tests/saitests/py3/sai_qos_tests.py +++ b/tests/saitests/py3/sai_qos_tests.py @@ -5,7 +5,7 @@ import time import logging import ptf.packet as scapy -from scapy.all import Ether, IP +from scapy.all import Ether, IP, IPv6 import socket import sai_base_test import operator @@ -25,7 +25,8 @@ simple_ipv4ip_packet, hex_dump_buffer, verify_packet_any_port, - port_to_tuple) + port_to_tuple, + simple_udpv6_packet) from ptf.mask import Mask from switch import (switch_init, sai_thrift_create_scheduler_profile, @@ -46,6 +47,7 @@ from switch_sai_thrift.ttypes import (sai_thrift_attribute_value_t, sai_thrift_attribute_t) from switch_sai_thrift.sai_headers import SAI_PORT_ATTR_QOS_SCHEDULER_PROFILE_ID +from scapy.layers.inet6 import ICMPv6ND_NS, ICMPv6NDOptDstLLAddr # Counters @@ -523,6 +525,43 @@ def construct_ip_pkt(pkt_len, dst_mac, src_mac, src_ip, dst_ip, dscp, src_vlan, return pkt +def construct_ipv6_pkt(pkt_len, dst_mac, src_mac, src_ip, dst_ip, dscp, src_vlan, **kwargs): + ipv6_ecn = kwargs.get('ecn', 1) + ipv6_hlim = kwargs.get('ttl', None) + exp_pkt = kwargs.get('exp_pkt', False) + + pkt_args = { + 'pktlen': pkt_len, + 'eth_dst': dst_mac, + 'eth_src': src_mac, + 'ipv6_src': src_ip, + 'ipv6_dst': dst_ip, + 'ipv6_dscp': dscp, + 'ipv6_ecn': ipv6_ecn + } + + if ipv6_hlim is not None: + pkt_args['ipv6_hlim'] = ipv6_hlim + + if src_vlan is not None: + pkt_args['dl_vlan_enable'] = True + pkt_args['vlan_vid'] = int(src_vlan) + pkt_args['vlan_pcp'] = dscp + + pkt = simple_udpv6_packet(**pkt_args) + + if exp_pkt: + masked_exp_pkt = Mask(pkt, ignore_extra_bytes=True) + masked_exp_pkt.set_do_not_care_scapy(scapy.Ether, "dst") + masked_exp_pkt.set_do_not_care_scapy(scapy.Ether, "src") + masked_exp_pkt.set_do_not_care_scapy(scapy.IPv6, "hlim") + if src_vlan is not None: + masked_exp_pkt.set_do_not_care_scapy(scapy.Dot1Q, "vlan") + return masked_exp_pkt + else: + return pkt + + def construct_arp_pkt(eth_dst, eth_src, arp_op, src_ip, dst_ip, hw_dst, src_vlan): pkt_args = { 'eth_dst': eth_dst, @@ -542,6 +581,89 @@ def construct_arp_pkt(eth_dst, eth_src, arp_op, src_ip, dst_ip, hw_dst, src_vlan return pkt +def create_solicited_node_multicast(ipv6_binary): + """ + Create solicited-node multicast address from IPv6 binary. + + Args: + ipv6_binary (bytes): IPv6 address in binary format (16 bytes) + + Returns: + bytes: Solicited-node multicast address in binary format + """ + # Create ff02::1:ff00:0/104 + last 24 bits of target IP + multicast_bytes = bytearray(16) + multicast_bytes[0] = 0xff # ff + multicast_bytes[1] = 0x02 # 02 + multicast_bytes[11] = 0x01 # 1 + multicast_bytes[12] = 0xff # ff + # Copy last 24 bits (3 bytes) from the target IPv6 + multicast_bytes[13:16] = ipv6_binary[13:16] + + return bytes(multicast_bytes) + + +def create_multicast_mac(multicast_ipv6_binary): + """ + Create multicast MAC address from IPv6 multicast address. + + Args: + multicast_ipv6_binary (bytes): IPv6 multicast address in binary format + + Returns: + str: Multicast MAC address in format "33:33:xx:xx:xx:xx" + """ + # Create 33:33 + last 32 bits of multicast IPv6 + mac_bytes = bytearray(6) + mac_bytes[0] = 0x33 # 33 + mac_bytes[1] = 0x33 # 33 + # Copy last 32 bits (4 bytes) from multicast IPv6 + mac_bytes[2:6] = multicast_ipv6_binary[12:16] + + return ':'.join(f'{b:02x}' for b in mac_bytes) # noqa: E231 + + +def convert_to_ns_multicast(ipv6_address): + """ + Convert an IPv6 address to the corresponding multicast MAC and IP + addresses used in Neighbor Solicitation (NS) protocol. + + Args: + ipv6_address (str): The target IPv6 address (e.g., "fc02:1000::1") + Returns: + dict: Dictionary containing: + - solicited_node_multicast_ip: The solicited-node multicast IPv6 address + - solicited_node_multicast_mac: The corresponding multicast MAC address + e.g. + when ipv6 address is fc02:1000::1, + return solicited_node_multicast_ip is FF02::1:FF00:0001, solicited_node_multicast_mac is 33:33:ff:00:00:01 + """ + + # Step 1: Convert IPv6 address to binary format + ipv6_binary = socket.inet_pton(socket.AF_INET6, ipv6_address) + + # Step 2: Create solicited-node multicast address manually + # Extract last 24 bits and append to ff02::1:ff00:0/104 + multicast_addr_binary = create_solicited_node_multicast(ipv6_binary) + solicited_node_multicast_ip = socket.inet_ntop(socket.AF_INET6, multicast_addr_binary) + + # Step 3: Create corresponding multicast MAC address + # Convert multicast IPv6 to MAC (33:33:xx:xx:xx:xx) + solicited_node_multicast_mac = create_multicast_mac(multicast_addr_binary) + + return solicited_node_multicast_ip, solicited_node_multicast_mac + + +def construct_ns_pkt(eth_src, src_ip, dst_ip='fc02:1000::1'): + dst_multicast_ip, dst_multicast_mac = convert_to_ns_multicast(dst_ip) + ether = Ether(src=eth_src, dst=dst_multicast_mac) + ipv6 = IPv6(src=src_ip, dst=dst_multicast_ip) + icmpv6 = ICMPv6ND_NS(tgt=dst_ip) + lladdr = ICMPv6NDOptDstLLAddr(type=1, lladdr=eth_src) + pkt = ether/ipv6/icmpv6/lladdr + return pkt + + def get_rx_port(dp, device_number, src_port_id, dst_mac, dst_ip, src_ip, src_vlan=None): ip_id = 0xBABE src_port_mac = dp.dataplane.get_mac(device_number, src_port_id) @@ -572,6 +694,34 @@ def get_rx_port(dp, device_number, src_port_id, dst_mac, dst_ip, src_ip, src_vla return result.port +def get_rx_port_ipv6(dp, device_number, src_port_id, dst_mac, dst_ip, src_ip, src_vlan=None): + src_port_mac = dp.dataplane.get_mac(device_number, src_port_id) + pkt = construct_ipv6_pkt(64, dst_mac, src_port_mac, src_ip, dst_ip, 0, src_vlan) + # Send initial packet for any potential ARP resolution, which may cause the LAG + # destination to change. Can occur especially when running tests in isolation on a + # first test attempt. + send_packet(dp, src_port_id, pkt, 1) + # Observed experimentally this sleep needs to be at least 0.02 seconds. Setting higher. + time.sleep(1) + send_packet(dp, src_port_id, pkt, 1) + + masked_exp_pkt = construct_ipv6_pkt( + 64, dst_mac, src_port_mac, src_ip, dst_ip, 0, src_vlan, exp_pkt=True) + + pre_result = dp.dataplane.poll( + device_number=0, exp_pkt=masked_exp_pkt, timeout=3) + result = dp.dataplane.poll( + device_number=0, exp_pkt=masked_exp_pkt, timeout=3) + if pre_result.port != result.port: + logging.debug("During get_rx_port, corrected LAG destination from {} to {}".format( + pre_result.port, result.port)) + if isinstance(result, dp.dataplane.PollFailure): + dp.fail("Expected packet was not received. Received on port:{} {}".format( + result.port, result.format())) + + return result.port + + def get_counter_names(sonic_version): ingress_counters = [INGRESS_DROP] egress_counters = [EGRESS_DROP] @@ -898,6 +1048,40 @@ def runTest(self): time.sleep(8) +class ARPpopulateIPv6(ARPpopulate): + + def runTest(self): + # ARP Populate + # Ping only required for testports + arpreq_pkt = construct_ns_pkt(self.src_port_mac, self.src_port_ip) + + send_packet(self, self.src_port_id, arpreq_pkt) + arpreq_pkt = construct_ns_pkt(self.dst_port_mac, self.dst_port_ip) + send_packet(self, self.dst_port_id, arpreq_pkt) + arpreq_pkt = construct_ns_pkt(self.dst_port_2_mac, self.dst_port_2_ip) + send_packet(self, self.dst_port_2_id, arpreq_pkt) + arpreq_pkt = construct_ns_pkt(self.dst_port_3_mac, self.dst_port_3_ip) + send_packet(self, self.dst_port_3_id, arpreq_pkt) + + for dut_i in self.test_port_ids: + for asic_i in self.test_port_ids[dut_i]: + for dst_port_id in self.test_port_ids[dut_i][asic_i]: + dst_port_ip = self.test_port_ips[dut_i][asic_i][dst_port_id] + dst_port_mac = self.dataplane.get_mac(0, dst_port_id) + arpreq_pkt = construct_ns_pkt(dst_port_mac, dst_port_ip['peer_addr']) + send_packet(self, dst_port_id, arpreq_pkt) + + # ptf don't know the address of neighbor, use ping to learn relevant arp entries instead of send arp request + if self.test_port_ips and not self.t0_src_uplink_dst_downlink: + ips = [ip for ip in get_peer_addresses(self.test_port_ips)] + if ips: + cmd = 'for ip in {}; do ping -6 -c 4 -i 0.2 -W 1 -q $ip > /dev/null 2>&1 & done'.format(' '.join(ips)) + self.exec_cmd_on_dut(self.server, self.test_params['dut_username'], + self.test_params['dut_password'], cmd) + + time.sleep(8) + + class ARPpopulatePTF(sai_base_test.ThriftInterfaceDataPlane): def runTest(self): # ARP Populate @@ -938,6 +1122,93 @@ def get_port_id(self, client, port_name): ), file=sys.stderr) return sai_port_id + def verify_v4_pkt( + self, pkt_dst_mac, src_port_mac, src_port_ip, dst_port_ip, dst_port_id, src_port_id, + exp_ip_id, exp_ttl, ip_ttl): + for dscp in range(0, 64): + tos = (dscp << 2) + tos |= 1 + pkt = simple_ip_packet(pktlen=64, + eth_dst=pkt_dst_mac, + eth_src=src_port_mac, + ip_src=src_port_ip, + ip_dst=dst_port_ip, + ip_tos=tos, + ip_id=exp_ip_id, + ip_ttl=ip_ttl) + send_packet(self, src_port_id, pkt, 1) + print("dscp: %d, calling send_packet()" % + (tos >> 2), file=sys.stderr) + + cnt = 0 + dscp_received = False + while not dscp_received: + result = self.dataplane.poll( + device_number=0, port_number=dst_port_id, timeout=3) + if isinstance(result, self.dataplane.PollFailure): + self.fail("Expected packet was not received on port %d. Total received: %d.\n%s" % ( + dst_port_id, cnt, result.format())) + + recv_pkt = scapy.Ether(result.packet) + cnt += 1 + + # Verify dscp flag + try: + if (recv_pkt.payload.tos == tos and + recv_pkt.payload.src == src_port_ip and + recv_pkt.payload.dst == dst_port_ip and + recv_pkt.payload.ttl == exp_ttl and + recv_pkt.payload.id == exp_ip_id): + dscp_received = True + print("dscp: %d, total received: %d" % + (tos >> 2, cnt), file=sys.stderr) + except AttributeError: + print("dscp: %d, total received: %d, attribute error!" % ( + tos >> 2, cnt), file=sys.stderr) + continue + + def verify_v6_pkt( + self, pkt_dst_mac, src_port_mac, src_port_ip, dst_port_ip, dst_port_id, src_port_id, + exp_ttl, ip_ttl): + for dscp in range(0, 64): + pkt = simple_udpv6_packet(pktlen=64, + eth_dst=pkt_dst_mac, + eth_src=src_port_mac, + ipv6_src=src_port_ip, + ipv6_dst=dst_port_ip, + ipv6_dscp=dscp, + ipv6_hlim=ip_ttl) + send_packet(self, src_port_id, pkt, 1) + print("dscp: %d, calling send_packet()" % + (dscp), file=sys.stderr) + + cnt = 0 + dscp_received = False + while not dscp_received: + result = self.dataplane.poll( + device_number=0, port_number=dst_port_id, timeout=3) + if isinstance(result, self.dataplane.PollFailure): + self.fail("Expected packet was not received on port %d. Total received: %d.\n%s" % ( + dst_port_id, cnt, result.format())) + + recv_pkt = scapy.Ether(result.packet) + cnt += 1 + + # Verify dscp flag + try: + tc = dscp << 2 + if (recv_pkt.payload.tc == tc and + recv_pkt.payload.src == src_port_ip and + recv_pkt.payload.dst == dst_port_ip and + recv_pkt.payload.hlim == exp_ttl): + dscp_received = True + print("dscp: %d, total received: %d" % + (dscp, cnt), file=sys.stderr) + except AttributeError: + print("dscp: %d, total received: %d, attribute error!" % ( + dscp, cnt), file=sys.stderr) + continue + def runTest(self): switch_init(self.clients) @@ -953,6 +1224,7 @@ def runTest(self): leaf_downstream = self.test_params.get('leaf_downstream', None) asic_type = self.test_params['sonic_asic_type'] tc_to_dscp_count_map = self.test_params.get('tc_to_dscp_count_map', None) + ip_type = self.test_params.get('ip_type', 'ipv4') exp_ip_id = 101 exp_ttl = 63 pkt_dst_mac = router_mac if router_mac != '' else dst_port_mac @@ -961,9 +1233,14 @@ def runTest(self): # in case dst_port_id is part of LAG, find out the actual dst port # for given IP parameters - dst_port_id = get_rx_port( - self, 0, src_port_id, pkt_dst_mac, dst_port_ip, src_port_ip - ) + if ip_type == 'ipv6': + dst_port_id = get_rx_port_ipv6( + self, 0, src_port_id, pkt_dst_mac, dst_port_ip, src_port_ip + ) + else: + dst_port_id = get_rx_port( + self, 0, src_port_id, pkt_dst_mac, dst_port_ip, src_port_ip + ) print("actual dst_port_id: %d" % (dst_port_id), file=sys.stderr) print("dst_port_mac: %s, src_port_mac: %s, src_port_ip: %s, dst_port_ip: %s" % ( dst_port_mac, src_port_mac, src_port_ip, dst_port_ip), file=sys.stderr) @@ -991,48 +1268,14 @@ def runTest(self): ip_ttl = ip_ttl if test_dst_port_name is None else ip_ttl + 2 if asic_type in ["cisco-8000"] and masic: ip_ttl = ip_ttl + 1 if masic else ip_ttl - - for dscp in range(0, 64): - tos = (dscp << 2) - tos |= 1 - pkt = simple_ip_packet(pktlen=64, - eth_dst=pkt_dst_mac, - eth_src=src_port_mac, - ip_src=src_port_ip, - ip_dst=dst_port_ip, - ip_tos=tos, - ip_id=exp_ip_id, - ip_ttl=ip_ttl) - send_packet(self, src_port_id, pkt, 1) - print("dscp: %d, calling send_packet()" % - (tos >> 2), file=sys.stderr) - - cnt = 0 - dscp_received = False - while not dscp_received: - result = self.dataplane.poll( - device_number=0, port_number=dst_port_id, timeout=3) - if isinstance(result, self.dataplane.PollFailure): - self.fail("Expected packet was not received on port %d. Total received: %d.\n%s" % ( - dst_port_id, cnt, result.format())) - - recv_pkt = scapy.Ether(result.packet) - cnt += 1 - - # Verify dscp flag - try: - if (recv_pkt.payload.tos == tos and - recv_pkt.payload.src == src_port_ip and - recv_pkt.payload.dst == dst_port_ip and - recv_pkt.payload.ttl == exp_ttl and - recv_pkt.payload.id == exp_ip_id): - dscp_received = True - print("dscp: %d, total received: %d" % - (tos >> 2, cnt), file=sys.stderr) - except AttributeError: - print("dscp: %d, total received: %d, attribute error!" % ( - tos >> 2, cnt), file=sys.stderr) - continue + if ip_type == 'ipv4': + self.verify_v4_pkt( + pkt_dst_mac, src_port_mac, src_port_ip, dst_port_ip, dst_port_id, + src_port_id, exp_ip_id, exp_ttl, ip_ttl) + else: + self.verify_v6_pkt( + pkt_dst_mac, src_port_mac, src_port_ip, dst_port_ip, dst_port_id, + src_port_id, exp_ttl, ip_ttl) # Read Counters time.sleep(3) @@ -4430,6 +4673,7 @@ def runTest(self): asic_type = self.test_params['sonic_asic_type'] hwsku = self.test_params['hwsku'] platform_asic = self.test_params['platform_asic'] + ip_type = self.test_params.get('ip_type', 'ipv4') # get counter names to query ingress_counters, egress_counters = get_counter_names(sonic_version) @@ -4453,22 +4697,36 @@ def runTest(self): packet_length = 64 pkt_dst_mac = router_mac if router_mac != '' else dst_port_mac - pkt = construct_ip_pkt(packet_length, - pkt_dst_mac, - src_port_mac, - src_port_ip, - dst_port_ip, - dscp, - src_port_vlan, - ecn=ecn, - ttl=ttl) + if ip_type == 'ipv6': + pkt = construct_ipv6_pkt(packet_length, + pkt_dst_mac, + src_port_mac, + src_port_ip, + dst_port_ip, + dscp, + src_port_vlan, + ecn=ecn, + ttl=ttl) + else: + pkt = construct_ip_pkt(packet_length, + pkt_dst_mac, + src_port_mac, + src_port_ip, + dst_port_ip, + dscp, + src_port_vlan, + ecn=ecn, + ttl=ttl) log_message("dst_port_id: {}, src_port_id: {} src_port_vlan: {}".format( dst_port_id, src_port_id, src_port_vlan), to_stderr=True) # in case dst_port_id is part of LAG, find out the actual dst port # for given IP parameters - dst_port_id = get_rx_port( - self, 0, src_port_id, pkt_dst_mac, dst_port_ip, src_port_ip, src_port_vlan - ) + if ip_type == 'ipv6': + dst_port_id = get_rx_port_ipv6( + self, 0, src_port_id, pkt_dst_mac, dst_port_ip, src_port_ip, src_port_vlan) + else: + dst_port_id = get_rx_port( + self, 0, src_port_id, pkt_dst_mac, dst_port_ip, src_port_ip, src_port_vlan) log_message("actual dst_port_id: {}".format(dst_port_id), to_stderr=True) capture_diag_counter(self, 'GetRxPort') @@ -4896,6 +5154,7 @@ def runTest(self): hwsku = self.test_params['hwsku'] internal_hdr_size = self.test_params.get('internal_hdr_size', 0) platform_asic = self.test_params['platform_asic'] + ip_type = self.test_params.get('ip_type', 'ipv4') if 'packet_size' in list(self.test_params.keys()): packet_length = int(self.test_params['packet_size']) @@ -4921,23 +5180,39 @@ def runTest(self): [(src_port_id, src_port_ip)], packets_per_port=1)[src_port_id][0][0] else: - pkt = construct_ip_pkt(packet_length, - pkt_dst_mac, - src_port_mac, - src_port_ip, - dst_port_ip, - dscp, - src_port_vlan, - ecn=ecn, - ttl=ttl) + if ip_type == 'ipv6': + pkt = construct_ipv6_pkt(packet_length, + pkt_dst_mac, + src_port_mac, + src_port_ip, + dst_port_ip, + dscp, + src_port_vlan, + ecn=ecn, + ttl=ttl) + else: + pkt = construct_ip_pkt(packet_length, + pkt_dst_mac, + src_port_mac, + src_port_ip, + dst_port_ip, + dscp, + src_port_vlan, + ecn=ecn, + ttl=ttl) print("dst_port_id: %d, src_port_id: %d src_port_vlan: %s" % (dst_port_id, src_port_id, src_port_vlan), file=sys.stderr) # in case dst_port_id is part of LAG, find out the actual dst port # for given IP parameters - dst_port_id = get_rx_port( - self, 0, src_port_id, pkt_dst_mac, dst_port_ip, src_port_ip, src_port_vlan - ) + if ip_type == 'ipv6': + dst_port_id = get_rx_port_ipv6( + self, 0, src_port_id, pkt_dst_mac, dst_port_ip, src_port_ip, src_port_vlan + ) + else: + dst_port_id = get_rx_port( + self, 0, src_port_id, pkt_dst_mac, dst_port_ip, src_port_ip, src_port_vlan + ) print("actual dst_port_id: %d" % (dst_port_id), file=sys.stderr) # Add slight tolerance in threshold characterization to consider @@ -5533,6 +5808,7 @@ def runTest(self): hwsku = self.test_params['hwsku'] platform_asic = self.test_params['platform_asic'] dut_asic = self.test_params['dut_asic'] + ip_type = self.test_params.get('ip_type', 'ipv4') if 'packet_size' in list(self.test_params.keys()): packet_length = int(self.test_params['packet_size']) @@ -5550,23 +5826,39 @@ def runTest(self): if is_dualtor and def_vlan_mac is not None: pkt_dst_mac = def_vlan_mac - pkt = construct_ip_pkt(packet_length, - pkt_dst_mac, - src_port_mac, - src_port_ip, - dst_port_ip, - dscp, - src_port_vlan, - ecn=ecn, - ttl=ttl) + if ip_type == 'ipv6': + pkt = construct_ipv6_pkt(packet_length, + pkt_dst_mac, + src_port_mac, + src_port_ip, + dst_port_ip, + dscp, + src_port_vlan, + ecn=ecn, + ttl=ttl) + else: + pkt = construct_ip_pkt(packet_length, + pkt_dst_mac, + src_port_mac, + src_port_ip, + dst_port_ip, + dscp, + src_port_vlan, + ecn=ecn, + ttl=ttl) print("dst_port_id: %d, src_port_id: %d, src_port_vlan: %s" % (dst_port_id, src_port_id, src_port_vlan), file=sys.stderr) # in case dst_port_id is part of LAG, find out the actual dst port # for given IP parameters - dst_port_id = get_rx_port( - self, 0, src_port_id, pkt_dst_mac, dst_port_ip, src_port_ip, src_port_vlan - ) + if ip_type == 'ipv6': + dst_port_id = get_rx_port_ipv6( + self, 0, src_port_id, pkt_dst_mac, dst_port_ip, src_port_ip, src_port_vlan + ) + else: + dst_port_id = get_rx_port( + self, 0, src_port_id, pkt_dst_mac, dst_port_ip, src_port_ip, src_port_vlan + ) print("actual dst_port_id: %d" % (dst_port_id), file=sys.stderr) # Add slight tolerance in threshold characterization to consider