Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
41 changes: 29 additions & 12 deletions ansible/roles/test/files/ptftests/inner_hash_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class InnerHashTest(BaseTest):
#---------------------------------------------------------------------
DEFAULT_BALANCING_RANGE = 0.25
BALANCING_TEST_TIMES = 625
OUTER_ENCAP_FORMATS = ["vxlan", "nvgre"]

_required_params = [
'fib_info',
Expand Down Expand Up @@ -88,14 +89,16 @@ def setUp(self):

self.balancing_range = self.test_params.get('balancing_range', self.DEFAULT_BALANCING_RANGE)
self.balancing_test_times = self.test_params.get('balancing_test_times', self.BALANCING_TEST_TIMES)
self.outer_encap_formats = self.test_params.get('outer_encap_formats', self.OUTER_ENCAP_FORMATS)


def check_hash(self, hash_key):
src_port = int(random.choice(self.src_ports))
logging.info("outer_dst_ip={}, src_port={}, exp_port_list={}".format(self.outer_dst_ip, src_port, self.exp_port_list))
outer_encap_formats = ['vxlan', 'nvgre']

for outer_encap_format in outer_encap_formats:
logging.info("balancing_test_times: {}".format(self.balancing_test_times))

for outer_encap_format in self.outer_encap_formats:
hit_count_map = {}
for _ in range(0, self.balancing_test_times*len(self.exp_port_list)):
src_port = int(random.choice(self.src_ports))
Expand Down Expand Up @@ -154,16 +157,30 @@ def generate_inner_pkt(self, sport, dport, ip_src, ip_dst, ip_proto):
rand_int = random.randint(1, 99)
src_mac = '00:12:ab:34:cd:' + str(rand_int)
dst_mac = str(rand_int) + ':12:ab:34:cd:00'
pkt = simple_tcp_packet(
eth_dst=dst_mac,
eth_src=src_mac,
ip_src=ip_src,
ip_dst=ip_dst,
tcp_sport=sport,
tcp_dport=dport,
ip_ttl=64)

pkt['IP'].proto = ip_proto
if ip_network(unicode(ip_src)).version == 4:
pkt = simple_tcp_packet(
eth_dst=dst_mac,
eth_src=src_mac,
ip_dst=ip_dst,
ip_src=ip_src,
tcp_sport=sport,
tcp_dport=dport,
ip_ttl=64
)

pkt["IP"].proto = ip_proto
else:
pkt = simple_tcpv6_packet(
eth_dst=dst_mac,
eth_src=src_mac,
ipv6_dst=ip_dst,
ipv6_src=ip_src,
tcp_sport=sport,
tcp_dport=dport,
ipv6_hlim=64
)

pkt["IPv6"].nh = ip_proto
return pkt


Expand Down
Empty file added tests/ecmp/__init__.py
Empty file.
Empty file.
326 changes: 326 additions & 0 deletions tests/ecmp/inner_hashing/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,326 @@
import time
import json
import logging
import tempfile

from datetime import datetime

import pytest

from tests.common.fixtures.ptfhost_utils import copy_ptftests_directory, change_mac_addresses # lgtm[py/unused-import]

logger = logging.getLogger(__name__)


# Standard HASH_KEYs of 'src-ip', 'dst-ip', 'src-port', 'dst-port', 'ip-proto' varied in the inner packets sent and used to validate hashing
# outer-tuples is also used as a HASH_KEY to validate that varying any outer tuples for encap traffic does not affect inner hashing
HASH_KEYS = ['src-ip', 'dst-ip', 'src-port', 'dst-port', 'ip-proto', 'outer-tuples']
SRC_IP_RANGE = ['8.0.0.0', '8.255.255.255']
DST_IP_RANGE = ['9.0.0.0', '9.255.255.255']
SRC_IPV6_RANGE = ['20D0:A800:0:00::', '20D0:A800:0:00::FFFF:FFFF']
DST_IPV6_RANGE = ['20D0:A800:0:01::', '20D0:A800:0:01::FFFF:FFFF']
PTF_QLEN = 2000

PTF_TEST_PORT_MAP = '/root/ptf_test_port_map.json'
FIB_INFO_FILE_DST = '/root/fib_info.txt'

VXLAN_PORT = 13330
DUT_VXLAN_PORT_JSON_FILE = '/tmp/vxlan.switch.json'

IP_VERSIONS_LIST = ["ipv4", "ipv6"]
OUTER_ENCAP_FORMATS = ["vxlan", "nvgre"]
TABLE_NAME = "pbh_table"
TABLE_DESCRIPTION = "NVGRE and VXLAN"
HASH_NAME = "inner_hash"
VXLAN_RULE_NAME = "vxlan_{}_{}"
NVGRE_RULE_NAME = "nvgre_{}_{}"
VXLAN_RULE_PRIO = "1"
NVGRE_RULE_PRIO = "2"
ECMP_PACKET_ACTION = "SET_ECMP_HASH"
V4_ETHER_TYPE = "0x0800"
V6_ETHER_TYPE = "0x86dd"
VXLAN_IP_PROTOCOL = "0x11"
NVGRE_IP_PROTOCOL = "0x2f"
VXLAN_L4_DST_PORT = "0x3412"
VXLAN_L4_DST_PORT_OPTION = " --l4-dst-port {}".format(VXLAN_L4_DST_PORT)
ADD_PBH_TABLE_CMD = "sudo config pbh table add '{}' --interface-list '{}' --description '{}'"
DEL_PBH_TABLE_CMD = "sudo config pbh table delete '{}'"
ADD_PBH_RULE_BASE_CMD = "sudo config pbh rule add '{}' '{}' --priority '{}' --ether-type {}" \
" --inner-ether-type '{}' --hash '{}' --packet-action '{}' --flow-counter 'ENABLED'"
DEL_PBH_RULE_CMD = "sudo config pbh rule delete '{}' '{}'"
ADD_PBH_HASH_CMD = "sudo config pbh hash add '{}' --hash-field-list '{}'"
DEL_PBH_HASH_CMD = "sudo config pbh hash delete '{}'"
ADD_PBH_HASH_FIELD_CMD = "sudo config pbh hash-field add '{}' --hash-field '{}' --sequence-id '{}'"
DEL_PBH_HASH_FIELD_CMD = "sudo config pbh hash-field delete '{}'"

PBH_HASH_FIELD_LIST = "inner_ip_proto," \
"inner_l4_dst_port,inner_l4_src_port," \
"inner_dst_ipv4,inner_src_ipv4," \
"inner_src_ipv6,inner_dst_ipv6"
HASH_FIELD_CONFIG = {
"inner_ip_proto": {"field": "INNER_IP_PROTOCOL", "sequence": "1"},
"inner_l4_dst_port": {"field": "INNER_L4_DST_PORT", "sequence": "2"},
"inner_l4_src_port": {"field": "INNER_L4_SRC_PORT", "sequence": "2"},
"inner_src_ipv4": {"field": "INNER_SRC_IPV4", "sequence": "3", "mask": "255.255.255.255"},
"inner_dst_ipv4": {"field": "INNER_DST_IPV4", "sequence": "3", "mask": "255.255.255.255"},
"inner_src_ipv6": {"field": "INNER_SRC_IPV6", "sequence": "4", "mask": "::ffff:ffff"},
"inner_dst_ipv6": {"field": "INNER_DST_IPV6", "sequence": "4", "mask": "::ffff:ffff"}
}


@pytest.fixture(scope='module')
def config_facts(duthosts, rand_one_dut_hostname):
duthost = duthosts[rand_one_dut_hostname]
return duthost.config_facts(host=duthost.hostname, source='running')['ansible_facts']


@pytest.fixture(scope='module', autouse=True)
def setup(duthosts, rand_one_dut_hostname):
duthost = duthosts[rand_one_dut_hostname]

vxlan_switch_config = [{
"SWITCH_TABLE:switch": {
"vxlan_port": VXLAN_PORT
},
"OP": "SET"
}]

logger.info("Copying vxlan.switch.json with data: " + str(vxlan_switch_config))

duthost.copy(content=json.dumps(vxlan_switch_config, indent=4), dest=DUT_VXLAN_PORT_JSON_FILE)
duthost.shell("docker cp {} swss:/vxlan.switch.json".format(DUT_VXLAN_PORT_JSON_FILE))
duthost.shell("docker exec swss sh -c \"swssconfig /vxlan.switch.json\"")
time.sleep(3)


@pytest.fixture(scope='module', autouse=True)
def build_fib(duthosts, rand_one_dut_hostname, ptfhost, config_facts, tbinfo):
duthost = duthosts[rand_one_dut_hostname]

timestamp = datetime.now().strftime('%Y-%m-%d-%H:%M:%S')

mg_facts = duthost.get_extended_minigraph_facts(tbinfo)

duthost.shell("redis-dump -d 0 -k 'ROUTE*' -y > /tmp/fib.{}.txt".format(timestamp))
duthost.fetch(src="/tmp/fib.{}.txt".format(timestamp), dest="/tmp/fib")

po = config_facts.get('PORTCHANNEL', {})
ports = config_facts.get('PORT', {})

tmp_fib_info = tempfile.NamedTemporaryFile()
with open("/tmp/fib/{}/tmp/fib.{}.txt".format(duthost.hostname, timestamp)) as fp:
fib = json.load(fp)
for k, v in fib.items():
skip = False
prefix = k.split(':', 1)[1]
ifnames = v['value']['ifname'].split(',')
nh = v['value']['nexthop']

oports = []
for ifname in ifnames:
if po.has_key(ifname):
oports.append([str(mg_facts['minigraph_ptf_indices'][x]) for x in po[ifname]['members']])
else:
if ports.has_key(ifname):
oports.append([str(mg_facts['minigraph_ptf_indices'][ifname])])
else:
logger.info("Route point to non front panel port {}:{}".format(k, v))
skip = True
# skip direct attached subnet
if nh == '0.0.0.0' or nh == '::' or nh == "":
skip = True

if not skip:
tmp_fib_info.write("{}".format(prefix))
for op in oports:
tmp_fib_info.write(" [{}]".format(" ".join(op)))
tmp_fib_info.write("\n")
else:
tmp_fib_info.write("{} []\n".format(prefix))
tmp_fib_info.flush()

ptfhost.copy(src=tmp_fib_info.name, dest=FIB_INFO_FILE_DST)
msg = "Copied FIB info to PTF host '{}': local_path={}, remote_path={}"
logger.info(msg.format(ptfhost.hostname, tmp_fib_info.name, FIB_INFO_FILE_DST))

tmp_fib_info.close()


@pytest.fixture(scope='module')
def vlan_ptf_ports(config_facts, tbinfo, duthost):
ports = []
mg_facts = duthost.get_extended_minigraph_facts(tbinfo)
for vlan_members in config_facts.get('VLAN_MEMBER', {}).values():
for intf in vlan_members.keys():
dut_port_index = mg_facts['minigraph_ptf_indices'][intf]
logging.info("Added " + str(dut_port_index))
ports.append(dut_port_index)

return ports


@pytest.fixture(scope='module')
def router_mac(duthosts, rand_one_dut_hostname):
duthost = duthosts[rand_one_dut_hostname]
return duthost.facts['router_mac']


@pytest.fixture(scope="module")
def hash_keys():
hash_keys = HASH_KEYS[:]
return hash_keys


@pytest.fixture(scope="module")
def symmetric_hashing(duthosts, rand_one_dut_hostname):
duthost = duthosts[rand_one_dut_hostname]
symmetric_hashing = False

if duthost.facts['asic_type'] in ["mellanox"]:
symmetric_hashing = True

return symmetric_hashing


@pytest.fixture(scope="module", params=IP_VERSIONS_LIST)
def outer_ipver(request):
return request.param


@pytest.fixture(scope="module", params=IP_VERSIONS_LIST)
def inner_ipver(request):
return request.param


@pytest.fixture(scope="module", autouse=True)
def setup_dynamic_pbh(request):
request.getfixturevalue("config_pbh_table")
request.getfixturevalue("config_hash_fields")
request.getfixturevalue("config_hash")
request.getfixturevalue("config_rules")


@pytest.fixture(scope="module")
def config_pbh_table(duthost, vlan_ptf_ports, tbinfo):
test_intfs_str = get_dut_test_intfs_str(duthost, vlan_ptf_ports, tbinfo)

duthost.command(ADD_PBH_TABLE_CMD.format(TABLE_NAME,
test_intfs_str,
TABLE_DESCRIPTION))

yield

duthost.command(DEL_PBH_TABLE_CMD.format(TABLE_NAME))


def get_dut_test_intfs_str(duthost, vlan_ptf_ports, tbinfo):
test_intfs = []
# get ports according to chosen ptf ports indices
mg_facts = duthost.get_extended_minigraph_facts(tbinfo)
for intf, index in mg_facts['minigraph_ptf_indices'].items():
if index in vlan_ptf_ports:
test_intfs.append(intf)
Copy link
Copy Markdown
Contributor

@anish-n anish-n Oct 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we may need validation to check if this works with other interface types like portchannels, Ethernet interface for comprehensive test coverage

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test is dedicated to t0 topology.
I'm using here the ports which are src_ports in ptf test (vlan_ptf_ports).

To use other interface types, need to update the test to t1-lag topology(as an example).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No not necessary, you can take ports out of the vlan and configure them in a static portchannel or Ethernet interface and use that as the src port too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, it's a static(custom) config. Need to run the static test. This part of the code is related to dynamic config only.

Or maybe I don't quite understand what you mean.
In a dynamic test, take out some ports from vlan, create portchannel/s, add ports w/o vlan as members to portchannel/s, add these portchannel/s to pbh table?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I mean we can modify the switch config here in the dynamic test to cover the other L3 interface types like portchannel interface and validate hashing with that

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I understand, it's mean to change the topology configurations especially for the test, and not use the base configurations
of topology with feature config only.
Also, it's not a simple change. Need time to design, implement, and testing.
I suggest creating the feature request for test extended with L3/PortChannel interfaces or/and supporting other topologies, on which the requirement will be implemented. Not block the test implementation and start to check the PBH feature in the regression.

return ",".join(test_intfs)


@pytest.fixture(scope="module")
def config_hash_fields(duthost):
for hash_field, hash_field_params_dict in HASH_FIELD_CONFIG.items():
cmd = get_hash_field_add_cmd(hash_field, hash_field_params_dict)
duthost.command(cmd)

yield

for hash_field in HASH_FIELD_CONFIG.keys():
duthost.command(DEL_PBH_HASH_FIELD_CMD.format(hash_field))


def get_hash_field_add_cmd(hash_field_name, hash_field_params_dict):
cmd = ADD_PBH_HASH_FIELD_CMD.format(hash_field_name,
hash_field_params_dict["field"],
hash_field_params_dict["sequence"])
if "mask" in hash_field_params_dict:
cmd += " --ip-mask '{}'".format(hash_field_params_dict["mask"])
return cmd


@pytest.fixture(scope="module")
def config_hash(duthost):
duthost.command(ADD_PBH_HASH_CMD.format(HASH_NAME, PBH_HASH_FIELD_LIST))

yield

duthost.command(DEL_PBH_HASH_CMD.format(HASH_NAME))


@pytest.fixture(scope="module")
def config_rules(duthost):
for ipver in IP_VERSIONS_LIST:
config_ipv4_rules(duthost, ipver)
config_ipv6_rules(duthost, ipver)

yield

for ipver in IP_VERSIONS_LIST:
delete_ipv4_rules(duthost, ipver)
delete_ipv6_rules(duthost, ipver)


def config_ipv4_rules(duthost, inner_ipver):
config_vxlan_rule(duthost, " --ip-protocol {}", V4_ETHER_TYPE, "ipv4", inner_ipver)
config_nvgre_rule(duthost, " --ip-protocol {}", V4_ETHER_TYPE, "ipv4", inner_ipver)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we maybe lacking test coverage for matching on:
gre_key = h32 "/" h32 ; GRE key (32 bits)

So would suggest adding a 3rd random packet format with a gre_key match, that way we can get full test coverage.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will be covered in the next PR



def config_ipv6_rules(duthost, inner_ipver):
config_vxlan_rule(duthost, " --ipv6-next-header {}", V6_ETHER_TYPE, "ipv6", inner_ipver)
config_nvgre_rule(duthost, " --ipv6-next-header {}", V6_ETHER_TYPE, "ipv6", inner_ipver)


def config_vxlan_rule(duthost, ip_ver_option, ether_type, outer_ipver, inner_ipver):
inner_ether_type = V4_ETHER_TYPE if inner_ipver == "ipv4" else V6_ETHER_TYPE
duthost.command((ADD_PBH_RULE_BASE_CMD + ip_ver_option + VXLAN_L4_DST_PORT_OPTION)
.format(TABLE_NAME,
VXLAN_RULE_NAME.format(outer_ipver, inner_ipver),
VXLAN_RULE_PRIO,
ether_type,
inner_ether_type,
HASH_NAME,
ECMP_PACKET_ACTION,
VXLAN_IP_PROTOCOL,
VXLAN_L4_DST_PORT))


def config_nvgre_rule(duthost, ip_ver_option, ether_type, outer_ipver, inner_ipver):
inner_ether_type = V4_ETHER_TYPE if inner_ipver == "ipv4" else V6_ETHER_TYPE
duthost.command((ADD_PBH_RULE_BASE_CMD + ip_ver_option)
.format(TABLE_NAME,
NVGRE_RULE_NAME.format(outer_ipver, inner_ipver),
NVGRE_RULE_PRIO,
ether_type,
inner_ether_type,
HASH_NAME,
ECMP_PACKET_ACTION,
NVGRE_IP_PROTOCOL))


def delete_ipv4_rules(duthost, inner_ipver):
delete_vxlan_nvgre_rules(duthost, "ipv4", inner_ipver)


def delete_ipv6_rules(duthost, inner_ipver):
delete_vxlan_nvgre_rules(duthost, "ipv6", inner_ipver)


def delete_vxlan_nvgre_rules(duthost, outer_ipver, inner_ipver):
duthost.command(DEL_PBH_RULE_CMD.format(TABLE_NAME, VXLAN_RULE_NAME.format(outer_ipver, inner_ipver)))
duthost.command(DEL_PBH_RULE_CMD.format(TABLE_NAME, NVGRE_RULE_NAME.format(outer_ipver, inner_ipver)))


def get_src_dst_ip_range(ipver):
if ipver == "ipv4":
src_ip_range = SRC_IP_RANGE
dst_ip_range = DST_IP_RANGE
else:
src_ip_range = SRC_IPV6_RANGE
dst_ip_range = DST_IPV6_RANGE
return src_ip_range, dst_ip_range
Loading