From c630ea15316368991124c7c24276b03b7a961524 Mon Sep 17 00:00:00 2001 From: Junchao-Mellanox <57339448+Junchao-Mellanox@users.noreply.github.com> Date: Tue, 16 Nov 2021 11:27:57 +0800 Subject: [PATCH] Add test case for trap flow counter feature (#4456) What is the motivation for this PR? Add test case for trap flow counter feature. How did you do it? This new test case is verifying the packet send number and packet rate. Test flow is like: * Enable and clear trap flow counter * Send packets via ptf * Verify the send number equals to the counter value, and the send rate is close to counter value How did you verify/test it? Manually run the test Any platform specific information? Any platform that support trap flow counter --- .../roles/test/files/ptftests/copp_tests.py | 119 ++++++++++++---- tests/copp/test_copp.py | 131 ++++++++++++++++-- 2 files changed, 218 insertions(+), 32 deletions(-) diff --git a/ansible/roles/test/files/ptftests/copp_tests.py b/ansible/roles/test/files/ptftests/copp_tests.py index 33f40c8eb77..c262a3ac735 100644 --- a/ansible/roles/test/files/ptftests/copp_tests.py +++ b/ansible/roles/test/files/ptftests/copp_tests.py @@ -55,6 +55,11 @@ def __init__(self): self.myip = test_params.get('myip', None) self.peerip = test_params.get('peerip', None) self.default_server_send_rate_limit_pps = test_params.get('send_rate_limit', 2000) + + # For counter test + self.expect_send_pkt_number = test_params.get('sent_pkt_number', None) + self.send_duration = test_params.get('send_duration', None) + self.is_counter_test = self.expect_send_pkt_number is not None and self.send_duration is not None self.needPreSend = None @@ -110,6 +115,9 @@ def copp_test(self, packet, send_intf, recv_intf): ''' Pre-send some packets for a second to absorb the CBS capacity. ''' + if self.is_counter_test: + return self.copp_counter_test(packet, send_intf, recv_intf) + if self.needPreSend: pre_send_count = 0 end_time = datetime.datetime.now() + datetime.timedelta(seconds=self.DEFAULT_PRE_SEND_INTERVAL_SEC) @@ -178,6 +186,67 @@ def copp_test(self, packet, send_intf, recv_intf): return send_count, recv_count, time_delta, time_delta_ms, tx_pps, rx_pps + def copp_counter_test(self, packet, send_intf, recv_intf): + pre_test_ptf_tx_counter = self.dataplane.get_counters(*send_intf) + pre_test_ptf_rx_counter = self.dataplane.get_counters(*recv_intf) + pre_test_nn_tx_counter = self.dataplane.get_nn_counters(*send_intf) + pre_test_nn_rx_counter = self.dataplane.get_nn_counters(*recv_intf) + + send_count = 0 + start_time = datetime.datetime.now() + send_window = float(self.send_duration) / float(self.expect_send_pkt_number) + while send_count < self.expect_send_pkt_number: + begin = time.time() + testutils.send_packet(self, send_intf, packet) + send_count += 1 + elapse = time.time() - begin + + # Depending on the server/platform combination it is possible for the server to + # overwhelm the DUT, so we add an artificial delay here to rate-limit the server. + if elapse > 0: + time.sleep(send_window - elapse) + + end_time = datetime.datetime.now() + time.sleep(self.DEFAULT_RECEIVE_WAIT_TIME) # Wait a little bit for all the packets to make it through + recv_count = testutils.count_matched_packets(self, packet, recv_intf[1], recv_intf[0]) + + post_test_ptf_tx_counter = self.dataplane.get_counters(*send_intf) + post_test_ptf_rx_counter = self.dataplane.get_counters(*recv_intf) + post_test_nn_tx_counter = self.dataplane.get_nn_counters(*send_intf) + post_test_nn_rx_counter = self.dataplane.get_nn_counters(*recv_intf) + + ptf_tx_count = int(post_test_ptf_tx_counter[1] - pre_test_ptf_tx_counter[1]) + nn_tx_count = int(post_test_nn_tx_counter[1] - pre_test_nn_tx_counter[1]) + ptf_rx_count = int(post_test_ptf_rx_counter[0] - pre_test_ptf_rx_counter[0]) + nn_rx_count = int(post_test_nn_rx_counter[0] - pre_test_nn_rx_counter[0]) + + self.log("", True) + self.log("Counters before the test:", True) + self.log("If counter (0, n): %s" % str(pre_test_ptf_tx_counter), True) + self.log("NN counter (0, n): %s" % str(pre_test_nn_tx_counter), True) + self.log("If counter (1, n): %s" % str(pre_test_ptf_rx_counter), True) + self.log("NN counter (1, n): %s" % str(pre_test_nn_rx_counter), True) + self.log("", True) + self.log("Counters after the test:", True) + self.log("If counter (0, n): %s" % str(post_test_ptf_tx_counter), True) + self.log("NN counter (0, n): %s" % str(post_test_nn_tx_counter), True) + self.log("If counter (1, n): %s" % str(post_test_ptf_rx_counter), True) + self.log("NN counter (1, n): %s" % str(post_test_nn_rx_counter), True) + self.log("") + self.log("Sent through NN to local ptf_nn_agent: %d" % ptf_tx_count) + self.log("Sent through If to remote ptf_nn_agent: %d" % nn_tx_count) + self.log("Recv from If on remote ptf_nn_agent: %d" % ptf_rx_count) + self.log("Recv from NN on from remote ptf_nn_agent: %d" % nn_rx_count) + + time_delta = end_time - start_time + self.log("Sent out %d packets in %ds" % (send_count, time_delta.seconds)) + time_delta_ms = (time_delta.microseconds + time_delta.seconds * 10**6) / 1000 + tx_pps = int(send_count / (float(time_delta_ms) / 1000)) + rx_pps = int(recv_count / (float(time_delta_ms) / 1000)) + + return send_count, recv_count, time_delta, time_delta_ms, tx_pps, rx_pps + + def contruct_packet(self, port_number): raise NotImplementedError @@ -214,21 +283,22 @@ def __init__(self): self.needPreSend = False def check_constraints(self, send_count, recv_count, time_delta_ms, rx_pps): - pkt_rx_limit = send_count * 0.90 + if not self.is_counter_test: + pkt_rx_limit = send_count * 0.90 - self.log("") - self.log("Checking constraints (NoPolicy):") - self.log( - "rx_pps (%d) > NO_POLICER_LIMIT (%d): %s" % - (int(rx_pps), int(self.NO_POLICER_LIMIT), str(rx_pps > self.NO_POLICER_LIMIT)) - ) - self.log( - "recv_count (%d) > pkt_rx_limit (%d): %s" % - (int(recv_count), int(pkt_rx_limit), str(recv_count > pkt_rx_limit)) - ) + self.log("") + self.log("Checking constraints (NoPolicy):") + self.log( + "rx_pps (%d) > NO_POLICER_LIMIT (%d): %s" % + (int(rx_pps), int(self.NO_POLICER_LIMIT), str(rx_pps > self.NO_POLICER_LIMIT)) + ) + self.log( + "recv_count (%d) > pkt_rx_limit (%d): %s" % + (int(recv_count), int(pkt_rx_limit), str(recv_count > pkt_rx_limit)) + ) - assert(rx_pps > self.NO_POLICER_LIMIT) - assert(recv_count > pkt_rx_limit) + assert(rx_pps > self.NO_POLICER_LIMIT) + assert(recv_count > pkt_rx_limit) class PolicyTest(ControlPlaneBaseTest): @@ -237,17 +307,18 @@ def __init__(self): self.needPreSend = True def check_constraints(self, send_count, recv_count, time_delta_ms, rx_pps): - self.log("") - self.log("Checking constraints (PolicyApplied):") - self.log( - "PPS_LIMIT_MIN (%d) <= rx_pps (%d) <= PPS_LIMIT_MAX (%d): %s" % - (int(self.PPS_LIMIT_MIN), - int(rx_pps), - int(self.PPS_LIMIT_MAX), - str(self.PPS_LIMIT_MIN <= rx_pps <= self.PPS_LIMIT_MAX)) - ) - - assert(self.PPS_LIMIT_MIN <= rx_pps <= self.PPS_LIMIT_MAX) + if not self.is_counter_test: + self.log("") + self.log("Checking constraints (PolicyApplied):") + self.log( + "PPS_LIMIT_MIN (%d) <= rx_pps (%d) <= PPS_LIMIT_MAX (%d): %s" % + (int(self.PPS_LIMIT_MIN), + int(rx_pps), + int(self.PPS_LIMIT_MAX), + str(self.PPS_LIMIT_MIN <= rx_pps <= self.PPS_LIMIT_MAX)) + ) + + assert(self.PPS_LIMIT_MIN <= rx_pps <= self.PPS_LIMIT_MAX) # SONIC config contains policer CIR=600 for ARP diff --git a/tests/copp/test_copp.py b/tests/copp/test_copp.py index 836ed52af67..47a42731d2b 100644 --- a/tests/copp/test_copp.py +++ b/tests/copp/test_copp.py @@ -23,12 +23,16 @@ import logging import pytest import json +import random +import threading +import time from collections import namedtuple from tests.copp import copp_utils from tests.ptf_runner import ptf_runner from tests.common import config_reload, constants from tests.common.system_utils import docker +from tests.common.utilities import wait_until # Module-level fixtures from tests.common.fixtures.ptfhost_utils import copy_ptftests_directory # lgtm[py/unused-import] @@ -53,6 +57,8 @@ _SUPPORTED_T2_TOPOS = ["t2"] _TOR_ONLY_PROTOCOL = ["DHCP"] _TEST_RATE_LIMIT = 600 +_SEND_PACKET_NUMBER = 1500 +_SEND_DURATION = 30 class TestCOPP(object): @@ -97,6 +103,51 @@ def test_no_policer(self, protocol, duthosts, enum_rand_one_per_hwsku_frontend_h copp_testbed, dut_type) + @pytest.mark.parametrize("protocol", ["LACP", + "LLDP", + "UDLD", + "IP2ME"]) + def test_counter(self, protocol, duthosts, rand_one_dut_hostname, ptfhost, copp_testbed, dut_type, counter_test): + duthost = duthosts[rand_one_dut_hostname] + trap_type = protocol.lower() + + # wait until the trap counter is enabled + assert wait_until(10, 1, 0, _check_trap_counter_enabled, duthost, trap_type), 'counter is not created for {}'.format(trap_type) + + # clean previous counter value + duthost.command('sonic-clear flowcnt-trap') + + # start a thread to collect the max PPS value + actual_rate = [] + t = threading.Thread(target=_collect_counter_rate, args=(duthost, trap_type, actual_rate)) + t.start() + + # init and start PTF + _copp_runner(duthost, + ptfhost, + protocol, + copp_testbed, + dut_type, + True) + + # wait for thread finish + t.join() + + # get final packet count from CLI + expect_rate = float(_SEND_PACKET_NUMBER / _SEND_DURATION) + actual_packet_number = None + cli_data = duthost.show_and_parse('show flowcnt-trap stats') + for line in cli_data: + if 'trap name' in line and line['trap name'] == trap_type: + actual_packet_number = int(line['packets'].replace(',', '')) + break + + assert actual_packet_number == _SEND_PACKET_NUMBER, 'Trap {} expect send packet number: {}, but actual: {}'.format(trap_type, _SEND_PACKET_NUMBER, actual_packet_number) + assert len(actual_rate) == 1, 'Failed to collect PPS value for trap {}'.format(trap_type) + # Allow a 10 percent threshold for trap rate + assert (expect_rate * 0.9) < actual_rate[0] < (expect_rate * 1.1), 'Trap {} expect send packet rate: {}, but actual: {}'.format(trap_type, expect_rate, actual_rate) + + @pytest.fixture(scope="class") def dut_type(duthosts, enum_rand_one_per_hwsku_frontend_hostname): duthost = duthosts[enum_rand_one_per_hwsku_frontend_hostname] @@ -156,17 +207,33 @@ def ignore_expected_loganalyzer_exceptions(enum_rand_one_per_hwsku_frontend_host if loganalyzer: # Skip if loganalyzer is disabled loganalyzer[enum_rand_one_per_hwsku_frontend_hostname].ignore_regex.extend(ignoreRegex) -def _copp_runner(dut, ptf, protocol, test_params, dut_type): +@pytest.fixture(scope="function") +def counter_test(duthosts, rand_one_dut_hostname): + duthost = duthosts[rand_one_dut_hostname] + duthost.command('counterpoll flowcnt-trap enable') + + yield + + duthost.command('counterpoll flowcnt-trap disable') + +def _copp_runner(dut, ptf, protocol, test_params, dut_type, counter_test=False): """ Configures and runs the PTF test cases. """ - - params = {"verbose": False, - "target_port": test_params.nn_target_port, - "myip": test_params.myip, - "peerip": test_params.peerip, - "send_rate_limit": test_params.send_rate_limit} - + if not counter_test: + params = {"verbose": False, + "target_port": test_params.nn_target_port, + "myip": test_params.myip, + "peerip": test_params.peerip, + "send_rate_limit": test_params.send_rate_limit} + else: + params = {"verbose": False, + "target_port": test_params.nn_target_port, + "myip": test_params.myip, + "peerip": test_params.peerip, + "send_rate_limit": test_params.send_rate_limit, + "sent_pkt_number": _SEND_PACKET_NUMBER, + "send_duration": _SEND_DURATION} dut_ip = dut.mgmt_ip device_sockets = ["0-{}@tcp://127.0.0.1:10900".format(test_params.nn_target_port), "1-{}@tcp://{}:10900".format(test_params.nn_target_port, dut_ip)] @@ -328,3 +395,51 @@ def _teardown_multi_asic_proxy(dut, creds, test_params, tbinfo): ns_ip = dut.shell("sudo ip -n {} -4 -o addr show eth0".format(test_params.nn_target_namespace) + " | awk '{print $4}' | cut -d'/' -f1")["stdout"] dut.command("sudo iptables -t nat -D PREROUTING -p tcp --dport 10900 -j DNAT --to-destination {}".format(ns_ip)) dut.command("sudo ip -n {} rule delete from {} to {} pref 3 lookup default".format(test_params.nn_target_namespace, ns_ip, tbinfo["ptf_ip"])) + +def _check_trap_counter_enabled(duthost, trap_type): + lines = duthost.command('show flowcnt-trap stats')['stdout'] + return trap_type in lines + +def _collect_counter_rate(duthost, trap_type, actual_rate): + rate_values = [] + # Wait up to _SEND_DURATION + 5 seconds for PTF to stop sending packet, + # as it might take some time for PTF to initialize itself + max_wait = _SEND_DURATION + 5 + packets = None + while max_wait > 0: + cli_data = duthost.show_and_parse('show flowcnt-trap stats') + for line in cli_data: + if 'trap name' in line and line['trap name'] == trap_type: + packets = line['packets'] + if packets == 'N/A': + # Packets value is not available yet + logging.debug('Trap {} packets value is not available yet'.format(trap_type)) + break + + pps_value = line['pps'] + if pps_value == 'N/A': + # PPS value is not available yet + logging.debug('Trap {} PPS value is not available yet'.format(trap_type)) + break + + packets = int(packets.replace(',', '')) + if packets == 0: + # PTF has not started yet + logging.debug('Trap {} packets value is still 0, PTF has not started yet'.format(trap_type)) + break + + logging.info('Trap {} current PPS value is {}, packets value is {}'.format(trap_type, pps_value, packets)) + rate_values.append(float(pps_value[:-2])) + break + if packets == _SEND_PACKET_NUMBER: + # Enough packets are sent, stop + break + time.sleep(0.5) + max_wait -= 0.5 + + if rate_values: + # Calculate max PPS + max_pps = max(rate_values) + logging.info('Trap {} max PPS is {}'.format(trap_type, max_pps)) + actual_rate.append(max_pps) +