From f15d81d2b485c448cc8e2c78c73740934b522c19 Mon Sep 17 00:00:00 2001 From: junchao Date: Mon, 13 Sep 2021 16:24:21 +0800 Subject: [PATCH 1/2] Add test case for flow counter feature Change-Id: I7f6b77dcb320fce1d9ea0be021486bb6b8e3ec87 --- .../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..da5c8f43e9e 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, _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) + From 4a5e3169d1519555519be6d7d7546c79237af10e Mon Sep 17 00:00:00 2001 From: junchao Date: Tue, 2 Nov 2021 09:43:28 +0800 Subject: [PATCH 2/2] Fix review comment Change-Id: I6e8491cf88330e141b5e0d8c809488c10a0be79f --- tests/copp/test_copp.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/copp/test_copp.py b/tests/copp/test_copp.py index da5c8f43e9e..47a42731d2b 100644 --- a/tests/copp/test_copp.py +++ b/tests/copp/test_copp.py @@ -112,7 +112,7 @@ def test_counter(self, protocol, duthosts, rand_one_dut_hostname, ptfhost, copp_ trap_type = protocol.lower() # wait until the trap counter is enabled - assert wait_until(10, 1, _check_trap_counter_enabled, duthost, trap_type), 'counter is not created for {}'.format(trap_type) + 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') @@ -132,7 +132,7 @@ def test_counter(self, protocol, duthosts, rand_one_dut_hostname, ptfhost, copp_ # wait for thread finish t.join() - + # get final packet count from CLI expect_rate = float(_SEND_PACKET_NUMBER / _SEND_DURATION) actual_packet_number = None @@ -141,7 +141,7 @@ def test_counter(self, protocol, duthosts, rand_one_dut_hostname, ptfhost, copp_ 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 @@ -211,7 +211,7 @@ def ignore_expected_loganalyzer_exceptions(enum_rand_one_per_hwsku_frontend_host 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') @@ -427,7 +427,7 @@ def _collect_counter_rate(duthost, trap_type, actual_rate): # 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 @@ -436,7 +436,7 @@ def _collect_counter_rate(duthost, trap_type, actual_rate): break time.sleep(0.5) max_wait -= 0.5 - + if rate_values: # Calculate max PPS max_pps = max(rate_values)