diff --git a/ansible/roles/test/files/ptftests/copp_tests.py b/ansible/roles/test/files/ptftests/copp_tests.py index 33f40c8eb77..499a7ed8570 100644 --- a/ansible/roles/test/files/ptftests/copp_tests.py +++ b/ansible/roles/test/files/ptftests/copp_tests.py @@ -57,6 +57,7 @@ def __init__(self): self.default_server_send_rate_limit_pps = test_params.get('send_rate_limit', 2000) self.needPreSend = None + self.has_trap = test_params.get('has_trap', True) def log(self, message, debug=False): current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") @@ -227,8 +228,12 @@ def check_constraints(self, send_count, recv_count, time_delta_ms, rx_pps): (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) + if self.has_trap: + assert (rx_pps > self.NO_POLICER_LIMIT) + assert (recv_count > pkt_rx_limit) + else: + assert (rx_pps < self.NO_POLICER_LIMIT) + assert (recv_count < pkt_rx_limit) class PolicyTest(ControlPlaneBaseTest): diff --git a/tests/copp/conftest.py b/tests/copp/conftest.py index 51f90b46cfe..6dfe6b4ee23 100644 --- a/tests/copp/conftest.py +++ b/tests/copp/conftest.py @@ -19,3 +19,10 @@ def pytest_addoption(parser): default=2000, help="Set custom server send rate limit", ) + parser.addoption( + "--copp_reboot_type", + action="store", + type=str, + default="cold", + help="reboot type such as cold, fast, warm, soft" + ) diff --git a/tests/copp/copp_utils.py b/tests/copp/copp_utils.py index ede5d5cd1a6..516ba1de91b 100644 --- a/tests/copp/copp_utils.py +++ b/tests/copp/copp_utils.py @@ -6,6 +6,9 @@ """ import re import logging +import json + +from tests.common.config_reload import config_reload DEFAULT_NN_TARGET_PORT = 3 @@ -22,7 +25,7 @@ _COPP_TEMPLATE_PATH = "/usr/share/sonic/templates/copp.json.j2" _SWSS_COPP_TEMPLATE = ":" + _COPP_TEMPLATE_PATH _DEFAULT_COPP_TEMPLATE = "/usr/share/sonic/templates/copp_cfg.j2" -_BASE_COPP_TEMPLATE = "/tmp/copp_cfg_base.j2" +_BASE_COPP_TEMPLATE = "/home/admin/copp_cfg_base.j2" _PTF_NN_TEMPLATE = "templates/ptf_nn_agent.conf.ptf.j2" _PTF_NN_DEST = "/etc/supervisor/conf.d/ptf_nn_agent.conf" @@ -31,6 +34,10 @@ _SYNCD_NN_DEST = "/tmp/ptf_nn_agent.conf" _SYNCD_NN_FILE = "ptf_nn_agent.conf" +_CONFIG_DB = "/etc/sonic/config_db.json" +_TEMP_CONFIG_DB = "/home/admin/config_db_copp_backup.json" + + def limit_policer(dut, pps_limit, nn_target_namespace): """ Updates the COPP configuration in the SWSS container to respect a given rate limit. @@ -236,3 +243,126 @@ def _get_http_and_https_proxy_ip(creds): return (re.findall(r'[0-9]+(?:\.[0-9]+){3}', creds.get('proxy_env', {}).get('http_proxy', ''))[0], re.findall(r'[0-9]+(?:\.[0-9]+){3}', creds.get('proxy_env', {}).get('https_proxy', ''))[0]) + + +def configure_always_enabled_for_trap(dut, trap_id, always_enabled): + """ + Configure the always_enabled to true or false for the specified trap id. + Args: + dut (SonicHost): The target device + trap_id (str): The trap id (e.g. bgp) + always_enabled (str): true or false + """ + copp_trap_config_json = "/tmp/copp_{}.json".format(trap_id) + cmd_copp_trap_always_enabled_config = """ +cat << EOF > %s +{ + "COPP_TRAP": { + "%s": { + "always_enabled": "%s" + } + } +} +EOF +""" % (copp_trap_config_json, trap_id, always_enabled) + + dut.shell(cmd_copp_trap_always_enabled_config) + dut.command("sudo config load {} -y".format(copp_trap_config_json)) + + +def get_config_db_json_obj(dut): + """ + Get config_db content from dut + Args: + dut (SonicHost): The target device + """ + config_db_json = dut.shell("sudo sonic-cfggen -d --print-data")["stdout"] + return json.loads(config_db_json) + + +def remove_feature_entry(dut, feature_name): + """ + Remove feature entry from dut + Args: + dut (SonicHost): The target device + feature_name (str): feature name (e.g bgp) + """ + dut.command('redis-cli -n 4 del "FEATURE|{}"'.format(feature_name)) + + +def disable_feature_entry(dut, feature_name): + """ + Disable feature entry on dut + Args: + dut (SonicHost): The target device + feature_name (str): feature name (e.g bgp) + """ + dut.command(' sudo config feature state {} disabled'.format(feature_name)) + + +def enable_feature_entry(dut, feature_name): + """ + Enabled feature entry dut + Args: + dut (SonicHost): The target device + feature_name (str): feature name (e.g bgp) + """ + dut.command(' sudo config feature state {} enabled'.format(feature_name)) + + +def backup_config_db(dut): + """ + Backup config db to /home/admin/ + Args: + dut (SonicHost): The target device + """ + dut.command("sudo cp {} {}".format(_CONFIG_DB, _TEMP_CONFIG_DB)) + + +def restore_config_db(dut): + """ + Restore config db + Args: + dut (SonicHost): The target device + """ + dut.command("sudo cp {} {}".format(_TEMP_CONFIG_DB, _CONFIG_DB)) + dut.command("sudo rm -f {}".format(_TEMP_CONFIG_DB)) + config_reload(dut) + + +def uninstall_trap(dut, feature_name, trap_id): + """ + Uninstall trap by disabling feature and set always_enable to false + + Args: + dut (SonicHost): The target device + feature_name (str): feature name corresponding to the trap + trap_id (str): trap id + """ + disable_feature_entry(dut, feature_name) + configure_always_enabled_for_trap(dut, trap_id, "false") + + +def verify_always_enable_value(dut, trap_id, always_enable_value): + """ + Verify the value of always_enable for the specified trap is expected one + + Args: + dut (SonicHost): The target device + trap_id (str): trap id + always_enable_value (str): true or false + """ + config_db_json = get_config_db_json_obj(dut) + assert config_db_json["COPP_TRAP"][trap_id]["always_enabled"] == always_enable_value, \ + "The value of always_enable not match. The expected value is:{}, the actual value is :{}".format( + always_enable_value, config_db_json["COPP_TRAP"][trap_id]["always_enabled"]) + + +def install_trap(dut, feature_name): + """ + Install trap by enabling feature status + Args: + dut (SonicHost): The target device + feature_name (str): feature name + """ + enable_feature_entry(dut, feature_name) diff --git a/tests/copp/test_copp.py b/tests/copp/test_copp.py index ff06570fd42..57f91f678c9 100644 --- a/tests/copp/test_copp.py +++ b/tests/copp/test_copp.py @@ -23,12 +23,17 @@ import logging import pytest import json +import random 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.reboot import reboot +from tests.common.utilities import skip_release +from tests.common.utilities import wait_until +from tests.common.helpers.assertions import pytest_assert # Module-level fixtures from tests.common.fixtures.ptfhost_utils import copy_ptftests_directory # lgtm[py/unused-import] @@ -55,11 +60,15 @@ _TOR_ONLY_PROTOCOL = ["DHCP"] _TEST_RATE_LIMIT = 600 +logger = logging.getLogger(__name__) + class TestCOPP(object): """ Tests basic COPP functionality in SONiC. """ + trap_id = "bgp" + feature_name = "bgp" @pytest.mark.parametrize("protocol", ["ARP", "IP2ME", @@ -98,6 +107,93 @@ def test_no_policer(self, protocol, duthosts, enum_rand_one_per_hwsku_frontend_h copp_testbed, dut_type) + def test_add_new_trap(self, duthosts, enum_rand_one_per_hwsku_frontend_hostname, ptfhost, check_image_version, copp_testbed, dut_type, backup_restore_config_db): + """ + Validates that one new trap(bgp) can be installed + + 1. The trap(bgp) should be uninstalled + 2. Set always_enabled of bgp to true + 3. Verify the trap status is installed by sending traffic + """ + duthost = duthosts[enum_rand_one_per_hwsku_frontend_hostname] + + logger.info("Uninstall trap {}".format(self.trap_id)) + copp_utils.uninstall_trap(duthost, self.feature_name, self.trap_id) + + logger.info("Verify {} trap status is uninstalled by sending traffic".format(self.trap_id)) + _copp_runner(duthost, + ptfhost, + self.trap_id.upper(), + copp_testbed, + dut_type, + has_trap=False) + + logger.info("Set always_enabled of {} to true".format(self.trap_id)) + copp_utils.configure_always_enabled_for_trap(duthost, self.trap_id, "true") + + logger.info("Verify {} trap status is installed by sending traffic".format(self.trap_id)) + pytest_assert( + wait_until(60, 20, 0, _copp_runner, duthost, ptfhost, self.trap_id.upper(), copp_testbed, dut_type), + "Installing {} trap fail".format(self.trap_id)) + + @pytest.mark.parametrize("remove_trap_type", ["delete_feature_entry", + "disable_feature_status"]) + def test_remove_trap(self, duthosts, enum_rand_one_per_hwsku_frontend_hostname, ptfhost, check_image_version, copp_testbed, dut_type, backup_restore_config_db, remove_trap_type): + """ + Validates that The trap(bgp) can be uninstalled after deleting the corresponding entry from the feature table + + 1. Pre condition: make the tested trap installed and always_enable is false + 2. Remove trap according to remove trap type + 4. Verify the trap status is uninstalled by sending traffic + """ + duthost = duthosts[enum_rand_one_per_hwsku_frontend_hostname] + + logger.info("Pre condition: make trap {} is installed".format(self.feature_name)) + pre_condition_install_trap(ptfhost, duthost, copp_testbed, self.trap_id, self.feature_name) + + if remove_trap_type == "delete_feature_entry": + logger.info("Remove feature entry: {}".format(self.feature_name)) + copp_utils.remove_feature_entry(duthost, self.feature_name) + else: + logger.info("Disable {} in feature table".format(self.feature_name)) + copp_utils.disable_feature_entry(duthost, self.feature_name) + + logger.info("Verify {} trap status is uninstalled by sending traffic".format(self.trap_id)) + pytest_assert( + wait_until(100, 20, 0, _copp_runner, duthost, ptfhost, self.trap_id.upper(), copp_testbed, dut_type, has_trap=False), + "uninstalling {} trap fail".format(self.trap_id)) + + def test_trap_config_save_after_reboot(self, duthosts, localhost, enum_rand_one_per_hwsku_frontend_hostname, ptfhost,check_image_version, copp_testbed, dut_type, backup_restore_config_db, request): + """ + Validates that the trap configuration is saved or not after reboot(reboot, fast-reboot, warm-reboot) + + 1. Set always_enabled of a trap(e.g. bgp) to true + 2. Config save -y + 3. Do reboot according to the specified parameter of copp_reboot_type (reboot/warm-reboot/fast-reboot/soft-reboot) + 4. Verify configuration are saved successfully + 5. Verify the trap status is installed by sending traffic + """ + + duthost = duthosts[enum_rand_one_per_hwsku_frontend_hostname] + + logger.info("Set always_enabled of {} to true".format(self.trap_id)) + copp_utils.configure_always_enabled_for_trap(duthost, self.trap_id, "true") + + logger.info("Config save") + duthost.command("sudo config save -y") + + reboot_type = request.config.getoption("--copp_reboot_type") + logger.info("Do {}".format(reboot_type)) + reboot(duthost, localhost, reboot_type=reboot_type, reboot_helper=None, reboot_kwargs=None) + + logger.info("Verify always_enable of {} == {} in config_db".format(self.trap_id, "true")) + copp_utils.verify_always_enable_value(duthost, self.trap_id, "true") + logger.info("Verify {} trap status is installed by sending traffic".format(self.trap_id)) + pytest_assert( + wait_until(100, 20, 0, _copp_runner, duthost, ptfhost, self.trap_id.upper(), copp_testbed, dut_type), + "Installing {} trap fail".format(self.trap_id)) + + @pytest.fixture(scope="class") def dut_type(duthosts, enum_rand_one_per_hwsku_frontend_hostname): duthost = duthosts[enum_rand_one_per_hwsku_frontend_hostname] @@ -157,7 +253,8 @@ 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): + +def _copp_runner(dut, ptf, protocol, test_params, dut_type, has_trap=True): """ Configures and runs the PTF test cases. """ @@ -166,7 +263,8 @@ def _copp_runner(dut, ptf, protocol, test_params, dut_type): "target_port": test_params.nn_target_port, "myip": test_params.myip, "peerip": test_params.peerip, - "send_rate_limit": test_params.send_rate_limit} + "send_rate_limit": test_params.send_rate_limit, + "has_trap": has_trap} dut_ip = dut.mgmt_ip device_sockets = ["0-{}@tcp://127.0.0.1:10900".format(test_params.nn_target_port), @@ -186,6 +284,8 @@ def _copp_runner(dut, ptf, protocol, test_params, dut_type): relax=None, debug_level=None, device_sockets=device_sockets) + return True + def _gather_test_params(tbinfo, duthost, request): """ @@ -331,3 +431,36 @@ 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"])) + + +@pytest.fixture(scope="function", autouse=False) +def backup_restore_config_db(duthosts, enum_rand_one_per_hwsku_frontend_hostname): + duthost = duthosts[enum_rand_one_per_hwsku_frontend_hostname] + copp_utils.backup_config_db(duthost) + + yield + copp_utils.restore_config_db(duthost) + + +def pre_condition_install_trap(ptfhost, duthost, copp_testbed, trap_id, feature_name): + copp_utils.install_trap(duthost, feature_name) + logger.info("Set always_enabled of {} to false".format(trap_id)) + copp_utils.configure_always_enabled_for_trap(duthost, trap_id, "false") + + logger.info("Verify {} trap status is installed by sending traffic in pre_condition".format(trap_id)) + pytest_assert( + wait_until(100, 20, 0, _copp_runner, duthost, ptfhost, trap_id.upper(), copp_testbed, dut_type), + "Installing {} trap fail".format(trap_id)) + + +@pytest.fixture(autouse=False, scope="class") +def check_image_version(duthosts, enum_rand_one_per_hwsku_frontend_hostname): + """Skips this test because new copp management logic works on 202012 branch and above + + Args: + duthost: Hostname of DUT. + + Returns: + None. + """ + skip_release(duthosts[enum_rand_one_per_hwsku_frontend_hostname], ["201911"])