Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0027a66
ACL stress test
opcoder0 Oct 28, 2024
49233f3
Fix error
opcoder0 Oct 29, 2024
0ca7d3d
Merge branch 'master' into testgap/add-acl-stress-test
opcoder0 Dec 29, 2024
076a47f
Add ACL stress test with rules and option to run via pytest-stress
opcoder0 Dec 31, 2024
9bd5bc7
Remove no_ from other test
opcoder0 Dec 31, 2024
f6e8316
Use completeness level to control stress test loops
opcoder0 Feb 20, 2025
800c437
restore other modifications
opcoder0 Feb 20, 2025
2bb0337
Merge branch 'master' into testgap/add-acl-stress-test
opcoder0 Feb 20, 2025
eaf0daa
Add step to verify if rules are installed successfully
opcoder0 Feb 21, 2025
52ff391
Add debug log
opcoder0 Feb 21, 2025
928fd23
Remove debug logs and add check to verify if rules are setup
opcoder0 Feb 21, 2025
7acc313
Refactor tests to run as stress test using pytest-loop
opcoder0 Feb 26, 2025
de03849
Dynamically generate rules based on hwsku
opcoder0 Feb 28, 2025
aef14c8
Merge branch 'master' into testgap/add-acl-stress-test
opcoder0 Feb 28, 2025
c6d6209
add --run-stress-tests
opcoder0 Feb 28, 2025
ae33d6d
Remove pytest-loop code
opcoder0 Feb 28, 2025
4150b8f
Merge run_tests.sh from master
opcoder0 Feb 28, 2025
5a6bff5
Fix formatting
opcoder0 Feb 28, 2025
8a91c5d
Create rules for platform, remove TCP_FLAG unused code, modify code t…
opcoder0 Mar 3, 2025
7a08210
Fix linter issues
opcoder0 Mar 3, 2025
10bba34
Fix linter issues
opcoder0 Mar 3, 2025
75e3ef4
Run tests marked with stress_test only when --run-stress-tests are pa…
opcoder0 Mar 4, 2025
705ee12
Merge branch 'master' into testgap/add-acl-stress-test
opcoder0 Mar 5, 2025
2cb403c
Merge branch 'master' into testgap/add-acl-stress-test
opcoder0 Mar 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
296 changes: 294 additions & 2 deletions tests/acl/test_stress_acl.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import ipaddress
import logging
import math
import random
import pytest
import json
import time
import ptf.testutils as testutils
from ptf import mask, packet
from collections import defaultdict
from tests.common.dualtor.mux_simulator_control import toggle_all_simulator_ports_to_rand_selected_tor # noqa F401
from tests.common.utilities import wait_until
from tests.common.fixtures.ptfhost_utils import skip_traffic_test # noqa F401

pytestmark = [
pytest.mark.topology("t0", "t1", "m0", "mx", "m1", "m2", "m3"),
Expand All @@ -24,17 +28,91 @@
# Template json file used to test scale rules
STRESS_ACL_TABLE_TEMPLATE = "acl/templates/acltb_test_stress_acl_table.j2"
STRESS_ACL_RULE_TEMPLATE = "acl/templates/acltb_test_stress_acl_rules.j2"
STRESS_ACL_READD_RULE_TEMPLATE = "acl/templates/acltb_test_stress_acl_readd_rules.j2"
DEL_STRESS_ACL_TABLE_TEMPLATE = "acl/templates/del_acltb_test_stress_acl_table.j2"
STRESS_ACL_TABLE_JSON_FILE = "/tmp/acltb_test_stress_acl_table.json"
STRESS_ACL_RULE_JSON_FILE = "/tmp/acltb_test_stress_acl_rules.json"
DEL_STRESS_ACL_TABLE_TEMPLATE = "acl/templates/del_acltb_test_stress_acl_table.j2"
DEL_STRESS_ACL_TABLE_JSON_FILE = "/tmp/del_acltb_test_stress_acl_table.json"

LOG_EXPECT_ACL_TABLE_CREATE_RE = ".*Created ACL table.*"
LOG_EXPECT_ACL_RULE_FAILED_RE = ".*Failed to create ACL rule.*"

ACL_RULE_NUMS = 10

DEFAULT_MAX_ACL_ENTRIES = 200

# key: platform name
# value: max number of ACL entries supported by the platform
rules_per_platform = {
# arista
'7050CX3': 512,
'7050-QX': 256,
'7050QX': 256,
'7060CX': 256,
'7060DX5': DEFAULT_MAX_ACL_ENTRIES,
'7060X6': 767,
'7170': DEFAULT_MAX_ACL_ENTRIES,
'7260CX3': 512,
'7280CR3': DEFAULT_MAX_ACL_ENTRIES,
'7800R3': DEFAULT_MAX_ACL_ENTRIES,
# celestica
'DX010': 256
}


@pytest.fixture(scope="module")
def setup_table_and_rules(rand_selected_dut, prepare_test_port):

logger.debug('Setting up rules')
_, _, dut_port = prepare_test_port
logger.debug(f'dut_port: {dut_port}')
table_name = 'STRESS_ACL_MANY'

# Get the max number of ACL entries supported by the platform
hwsku = rand_selected_dut.facts['hwsku']
model_str = hwsku.split('-')[1]
max_acl_entries = rules_per_platform.get(model_str.upper(), DEFAULT_MAX_ACL_ENTRIES)
logger.debug(f'HwSKU: {hwsku}. Max ACL entries supported by the platform {model_str}: {max_acl_entries}')
rules = generate_acl_rules(table_name, max_acl_entries)
f_name = f'generated_acl_rules_{max_acl_entries}.json'
file_path = f'/tmp/{f_name}'
with open(file_path, 'w') as f:
json.dump(rules, f)
logger.debug(f'Generated ACL rules written to {file_path}')
# Add table
cmd_add_table = f'config acl add table {table_name} L3 -s ingress -p {dut_port}'
rand_selected_dut.shell(cmd_add_table)
logger.debug('Table created')
# Copy rules file and add rules
rand_selected_dut.copy(src=file_path, dest=f'/tmp/{f_name}', mode="0755")
cmd_add_rules = f'sonic-cfggen -j /tmp/{f_name} -w'
rand_selected_dut.shell(cmd_add_rules)
logger.debug('Rules created. Sleep for 1 minute for rules to be active')
# verify if rules have been setup
time.sleep(60)
rules_out = rand_selected_dut.shell(f'show acl rule {table_name}')['stdout_lines']
logger.debug(f'Installed rules: {rules_out}')
active_count = 0
rules_not_installed = []
for line in rules_out:
logger.debug(f'line: {line}')
if 'RULE_' in line:
if line.split()[-1] == 'Active':
active_count += 1
else:
rules_not_installed.append(line)
logger.debug(f'Number of active rules: {active_count}')
if active_count != len(rules['ACL_RULE']):
logger.debug('Warning: Some ACL rules did not install succesfully')
pytest.fail(f'List of rules not installed: {rules_not_installed}')
logger.info("Setup of ACL table for stress test done")

yield rules['ACL_RULE']

cmd_del_rules = f"acl-loader delete {table_name}"
rand_selected_dut.shell(cmd_del_rules)
cmd_del_table = f"config acl remove table {table_name}"
rand_selected_dut.shell(cmd_del_table)


@pytest.fixture(scope="module", autouse=True)
def remove_dataacl_table(duthosts, rand_selected_dut):
Expand Down Expand Up @@ -220,3 +298,217 @@ def test_acl_add_del_stress(rand_selected_dut, tbinfo, ptfadapter, prepare_test_
rand_selected_dut.shell(cmd_rm_all_rules)
rand_selected_dut.shell(cmd_remove_table)
logger.info("End")


########################################
# Stress test with large number of rules
########################################
def tcp_packet(rand_selected_dut, ptfadapter, ip_version,
src_ip, dst_ip, proto, dport, sport=54321, flags=None):
"""Generate a TCP packet for testing."""
if ip_version == "ipv4":
pkt = testutils.simple_tcp_packet(
eth_dst=rand_selected_dut.facts['router_mac'],
eth_src=ptfadapter.dataplane.get_mac(0, 0),
ip_dst=dst_ip,
ip_src=src_ip,
tcp_sport=int(sport),
tcp_dport=int(dport),
ip_ttl=64,
tcp_flags=""
)
if proto:
pkt["IP"].proto = int(proto)
else:
pkt = testutils.simple_tcpv6_packet(
eth_dst=rand_selected_dut.facts['router_mac'],
eth_src=ptfadapter.dataplane.get_mac(0, 0),
ipv6_dst=dst_ip,
ipv6_src=src_ip,
tcp_sport=int(sport),
tcp_dport=int(dport),
ipv6_hlim=64
)
if proto:
pkt["IPv6"].nh = proto
if flags:
flag_val = ''
prefix_len = len('TCP_')
for f in flags:
flag_val += f[prefix_len:prefix_len+1]
pkt["TCP"].flags = flag_val

return pkt


def udp_packet(rand_selected_dut, ptfadapter, ip_version,
src_ip, dst_ip, dport, sport=54321):
"""Generate a UDP packet for testing."""
if ip_version == "ipv4":
return testutils.simple_udp_packet(
eth_dst=rand_selected_dut.facts['router_mac'],
eth_src=ptfadapter.dataplane.get_mac(0, 0),
ip_dst=dst_ip,
ip_src=src_ip,
udp_sport=int(sport),
udp_dport=int(dport),
ip_ttl=64
)
else:
return testutils.simple_udpv6_packet(
eth_dst=rand_selected_dut.facts['router_mac'],
eth_src=ptfadapter.dataplane.get_mac(0, 0),
ipv6_dst=dst_ip,
ipv6_src=src_ip,
udp_sport=int(sport),
udp_dport=int(dport),
ipv6_hlim=64
)


def ip_packet(rand_selected_dut, ptfadapter,
ip_proto, src_ip, dst_ip, ptf_src_port):
return testutils.simple_ip_packet(
eth_dst=rand_selected_dut.facts['router_mac'],
eth_src=ptfadapter.dataplane.get_mac(0, ptf_src_port),
ip_src=src_ip,
ip_dst=dst_ip,
ip_proto=ip_proto,
ip_tos=0x84,
ip_id=0,
ip_ihl=5,
ip_ttl=121
)


def generate_ipv4_addresses(subnet):
network = ipaddress.IPv4Network(subnet)
ip_addresses = [str(ip) for ip in network.hosts()]
return ip_addresses


def generate_acl_rules(table_name, n_rules):
# Generate rules with various destination IP addresses
# based on the subnets below.
subnets = [
'192.168.8.0/25',
'192.168.8.128/25',
'192.168.16.0/25',
'192.168.16.128/25',
'192.168.24.0/25',
'192.168.24.128/25',
'193.11.176.0/25',
'193.11.176.128/25',
'193.11.184.0/25',
'193.11.184.128/25',
'193.11.192.0/25',
'193.11.192.128/25',
'193.11.200.0/25',
'193.11.200.128/25',
'193.11.208.0/25',
'193.11.208.128/25',
'193.11.216.0/25',
'193.11.216.128/25',
'193.11.224.0/25',
'193.11.224.128/25',
'193.11.232.0/25',
'193.11.232.128/25',
'193.11.240.0/25',
'193.11.240.128/25',
'193.11.248.0/25',
'193.11.248.128/25'
]
rules = {}
rules['ACL_RULE'] = {}
# /25 subnets have 126 usable IP addresses
use_n_subnets = math.ceil(n_rules / 126)
src_ip = '20.0.0.1' # don't care what it is
j = 1
finish = False
action = 'FORWARD'
for i in range(use_n_subnets):
if finish is True:
break
subnet = subnets[i]
ip_addresses = generate_ipv4_addresses(subnet)
for ip in ip_addresses:
rule_name = f'{table_name}|RULE_{j}'
rules['ACL_RULE'][rule_name] = {
'PACKET_ACTION': action,
'SRC_IP': src_ip,
'DST_IP': ip,
'IP_PROTOCOL': '6',
'PRIORITY': str(j)
}
# toggle actions
if action == 'FORWARD':
action = 'DROP'
if action == 'DROP':
action = 'FORWARD'
if j == n_rules:
finish = True
break
j += 1
return rules


@pytest.mark.stress_test
def test_scale_acl_rules(request, rand_selected_dut, prepare_test_port, tbinfo, # noqa: F811
ptfadapter, setup_table_and_rules,
get_function_completeness_level, skip_traffic_test): # noqa: F811

if skip_traffic_test:
return

normalized_level = get_function_completeness_level
if normalized_level is None:
normalized_level = 'debug'
loop_times = LOOP_TIMES_LEVEL_MAP[normalized_level]

logger.debug('Starting ACL scale test')
ptf_src_port, ptf_dst_ports, dut_port = prepare_test_port
logger.debug(f'DUT port used in test {dut_port}')
acl_rules = setup_table_and_rules
logger.debug(f'Number of rules: {len(acl_rules)}')
pkt = None
loop = 0
while loop < loop_times:
loop += 1
for rule_name, rule in acl_rules.items():
if rule.get('IP_PROTOCOL') == '6':
dport = rule.get('L4_DST_PORT') if rule.get('L4_DST_PORT') else '12345'
pkt = tcp_packet(rand_selected_dut=rand_selected_dut,
ptfadapter=ptfadapter,
ip_version='ipv4',
src_ip=rule['SRC_IP'],
dst_ip=rule['DST_IP'],
proto=rule['IP_PROTOCOL'],
dport=dport)
elif rule.get('IP_PROTOCOL') == '17':
dport = rule.get('L4_DST_PORT') if rule.get('L4_DST_PORT') else '12345'
pkt = udp_packet(rand_selected_dut=rand_selected_dut,
ptfadapter=ptfadapter,
ip_version='ipv4',
src_ip=rule['SRC_IP'],
dst_ip=rule['DST_IP'],
dport=dport)
else:
pkt = ip_packet(rand_selected_dut=rand_selected_dut,
ptfadapter=ptfadapter,
ip_proto=47,
src_ip=rule['SRC_IP'],
dst_ip=rule['DST_IP'],
ptf_src_port=ptf_src_port)

pkt_copy = pkt.copy()
pkt_copy.ttl = pkt_copy.ttl - 1
exp_pkt = mask.Mask(pkt_copy)
exp_pkt.set_do_not_care_scapy(packet.Ether, 'dst')
exp_pkt.set_do_not_care_scapy(packet.Ether, 'src')
exp_pkt.set_do_not_care_scapy(packet.IP, "chksum")
ptfadapter.dataplane.flush()
testutils.send(test=ptfadapter, port_id=ptf_src_port, pkt=pkt)
if rule['PACKET_ACTION'] == 'FORWARD':
testutils.verify_packet_any_port(test=ptfadapter, pkt=exp_pkt, ports=ptf_dst_ports)
elif rule['PACKET_ACTION'] == 'DROP':
testutils.verify_no_packet_any(test=ptfadapter, pkt=exp_pkt, ports=ptf_dst_ports)
14 changes: 14 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,11 @@ def pytest_addoption(parser):
parser.addoption("--performance-meter-run", action="store", default=1, type=int,
help="Number of run for performance meter")

#################################
# Stress test options #
#################################
parser.addoption("--run-stress-tests", action="store_true", default=False, help="Run only tests stress tests")


def pytest_configure(config):
if config.getoption("enable_macsec"):
Expand Down Expand Up @@ -2879,3 +2884,12 @@ def start_platform_api_service(duthosts, enum_rand_one_per_hwsku_hostname, local

res = localhost.wait_for(host=dut_ip, port=SERVER_PORT, state='started', delay=1, timeout=10)
assert res['failed'] is False


def pytest_collection_modifyitems(config, items):
# Skip all stress_tests if --run-stress-test is not set
if not config.getoption("--run-stress-tests"):
skip_stress_tests = pytest.mark.skip(reason="Stress tests run only if --run-stress-tests is passed")
for item in items:
if "stress_test" in item.keywords:
item.add_marker(skip_stress_tests)
1 change: 1 addition & 0 deletions tests/pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ markers:
static_config: static_config marker
dependency: dependency marker
skip_traffic_test: skip_traffic_test marker
stress_test: mark test as stress test

log_cli_format: %(asctime)s %(funcNamewithModule)-40.40s L%(lineno)-.4d %(levelname)-7s| %(message)s
log_file_format: %(asctime)s %(funcNamewithModule)-40.40s L%(lineno)-.4d %(levelname)-7s| %(message)s
Expand Down
Loading