diff --git a/docs/testplan/MPLS-test-plan.md b/docs/testplan/MPLS-test-plan.md new file mode 100644 index 0000000000..c8b12f0682 --- /dev/null +++ b/docs/testplan/MPLS-test-plan.md @@ -0,0 +1,177 @@ +- [Overview](#overview) + * [Scope](#scope) + * [Testbed](#testbed) +- [Setup configuration](#setup-configuration) + + [Setup of DUT switch](#setup-of-dut-switch) +- [Test cases](#test-cases) + * [Test case \#1 - POP Label](#test-case-1---pop-label) + + [Test objective](#test-objective) + + [Test steps](#test-steps) + * [Test case \#2 - SWAP Label for single label](#test-case-2---swap-label-for-single-label) + + [Test objective](#test-objective-1) + + [Test steps](#test-steps-1) + * [Test case \#3 - PUSH Label](#test-case-3---push-label) + + [Test objective](#test-objective-2) + + [Test steps](#test-steps-2) + * [Test case \#4 - SWAP Label for multiple label stack](#test-case-4---swap-label-for-multiple-label-stack) + + [Test objective](#test-objective-3) + + [Test steps](#test-steps-3) + +## Overview +This is Test Plan to test MPLS feature on SONiC. The test enables MPLS on interfaces, configures static LSPs and assumes all basic configurations including BGP routes are already preconfigured. + +### Scope +The test is targeting a running SONiC system with basic functioning configuration. +Purpose of the test is to verify MPLS on a SONiC system bringing up the ingress, transit or egress static LSP and forwarding the traffic correctly. + +### Testbed +T1 + +## Setup configuration +MPLS will be enabled/disabled on interface command: +``` +config interface mpls +``` +MPLS configuration will be set on DUT dynamically. + +#### Setup of DUT switch +During testrun, Ansible will copy JSON file containing configuration for MPLS to DUT and push to SONiC APPL DB via swssconfig. + +JSON Sample: + +label_pop_routes.j2 + +``` +[ + { + "LABEL_ROUTE_TABLE:1000001": { + "nexthop": "{{ nexthop }}", + "ifname": "{{ port }}", + "mpls_pop": "1", + "weight": "1" + }, + "OP": "SET" + }, + { + "LABEL_ROUTE_TABLE:1000003": { + "nexthop": "{{ nexthop }}", + "ifname": "{{ port }}", + "mpls_pop": "1", + "weight": "1" + }, + "OP": "SET" + } +] + ``` + + label_push_routes.j2 + + ``` + [ + { + "ROUTE_TABLE:192.168.0.1": { + "nexthop": "{{ nexthop }}", + "ifname": "{{ port }}", + "mpls_nh": "push1000001", + "weight": "1" + }, + "OP": "SET" + } +] +``` +label_swap_routes.j2 + +``` +[ + { + "LABEL_ROUTE_TABLE:1000001": { + "nexthop": "{{ nexthop }}", + "ifname": "{{ port }}", + "mpls_nh": "swap1000002", + "mpls_pop": "1" + "weight": "1" + }, + "OP": "SET" + }, + { + "LABEL_ROUTE_TABLE:1000003": { + "nexthop": "{{ nexthop }}", + "ifname": "{{ port }}", + "mpls_nh": "swap1000004", + "mpls_pop": "1", + "weight": "1" + }, + "OP": "SET" + } +] +``` +label_del_routes.j2 + +``` +[ + { + "LABEL_ROUTE_TABLE:1000001": { + }, + "OP": "DEL" + }, + { + "LABEL_ROUTE_TABLE:1000003": { + }, + "OP": "DEL" + }, + { + "ROUTE_TABLE:192.168.0.1": { + }, + "OP": "DEL" + } +] +``` +## Test cases + +Each testcase configures static LSP, sends traffic, captures on receving port and verifies appropriate LABEL action is applied on packet. + +### Test case \#1 - POP Label + +#### Test objective + +Verify that the MPLS label is removed on the received packet. + +#### Test steps +- Enable MPLS on interfaces and configure pop label. +- Send MPLS packet. +- Capture the packet and verify that it is IP packet with MPLS removed. + +### Test case \#2 - SWAP Label for single label + +#### Test objective + +Verify that the MPLS label is swapped on the received packet. + +#### Test steps +- Enable MPLS on interfaces and configure swap label for MPLS packet. +- Send MPLS packet. +- Capture the packet and verify that it is MPLS packet with label swapped as per configuration. + +### Test case \#3 - PUSH Label + +#### Test objective + +Verify that the MPLS label is pushed on the received packet. + +#### Test steps +- Enable MPLS on interfaces and configure push label for MPLS packet. +- Send IP packet. +- Capture the packet and verify that it is MPLS packet with label added as per configuration. + + +### Test case \#4 - SWAP Label for multiple label stack + +#### Test objective + +Verify that the MPLS top label is swapped on the received packet. + +#### Test steps +- Enable MPLS on interfaces and configure swap label for MPLS packet. +- Send MPLS packet. +- Capture the packet and verify that it is MPLS packet with label swapped as per configuration for the top label. + diff --git a/tests/mpls/configs/label_del_routes.j2 b/tests/mpls/configs/label_del_routes.j2 new file mode 100644 index 0000000000..b45f7dcf48 --- /dev/null +++ b/tests/mpls/configs/label_del_routes.j2 @@ -0,0 +1,17 @@ +[ + { + "LABEL_ROUTE_TABLE:1000001": { + }, + "OP": "DEL" + }, + { + "LABEL_ROUTE_TABLE:1000003": { + }, + "OP": "DEL" + }, + { + "ROUTE_TABLE:192.168.0.1": { + }, + "OP": "DEL" + } +] diff --git a/tests/mpls/configs/label_pop_routes.j2 b/tests/mpls/configs/label_pop_routes.j2 new file mode 100644 index 0000000000..ddc6291c46 --- /dev/null +++ b/tests/mpls/configs/label_pop_routes.j2 @@ -0,0 +1,20 @@ +[ + { + "LABEL_ROUTE_TABLE:1000001": { + "nexthop": "{{ dst_peer_addr }}", + "ifname": "{{ dst_port }}", + "mpls_pop": "1", + "weight": "1" + }, + "OP": "SET" + }, + { + "LABEL_ROUTE_TABLE:1000003": { + "nexthop": "{{ src_peer_addr }}", + "ifname": "{{ src_port }}", + "mpls_pop": "1", + "weight": "1" + }, + "OP": "SET" + } +] diff --git a/tests/mpls/configs/label_push_routes.j2 b/tests/mpls/configs/label_push_routes.j2 new file mode 100644 index 0000000000..bcf55b58cd --- /dev/null +++ b/tests/mpls/configs/label_push_routes.j2 @@ -0,0 +1,11 @@ +[ + { + "ROUTE_TABLE:192.168.0.1": { + "nexthop": "{{ dst_peer_addr }}", + "ifname": "{{ dst_port }}", + "mpls_nh": "push1000001", + "weight": "1" + }, + "OP": "SET" + } +] diff --git a/tests/mpls/configs/label_swap_routes.j2 b/tests/mpls/configs/label_swap_routes.j2 new file mode 100644 index 0000000000..4d3e8e6d73 --- /dev/null +++ b/tests/mpls/configs/label_swap_routes.j2 @@ -0,0 +1,22 @@ +[ + { + "LABEL_ROUTE_TABLE:1000001": { + "nexthop": "{{ dst_peer_addr }}", + "ifname": "{{ dst_port }}", + "mpls_nh": "swap1000002", + "mpls_pop": "1", + "weight": "1" + }, + "OP": "SET" + }, + { + "LABEL_ROUTE_TABLE:1000003": { + "nexthop": "{{ src_peer_addr }}", + "ifname": "{{ src_port }}", + "mpls_nh": "swap1000004", + "mpls_pop": "1", + "weight": "1" + }, + "OP": "SET" + } +] diff --git a/tests/mpls/conftest.py b/tests/mpls/conftest.py new file mode 100644 index 0000000000..7540bf2130 --- /dev/null +++ b/tests/mpls/conftest.py @@ -0,0 +1,121 @@ +import logging +import pytest +import pprint + +logger = logging.getLogger(__name__) + +DUT_TMP_DIR='/tmp' + +LABEL_POP_ROUTES='label_pop_routes' +LABEL_PUSH_ROUTES='label_push_routes' +LABEL_SWAP_ROUTES='label_swap_routes' +LABEL_DEL_ROUTES='label_del_routes' + +@pytest.fixture(scope='module') +def setup(duthost, tbinfo, ptfadapter): + """ + setup fixture gathers all test required information from DUT facts and tbinfo + :param duthost: DUT host object + :param tbinfo: fixture provides information about testbed + :return: dictionary with all test required information + """ + if tbinfo['topo']['name'] not in ('t1'): + pytest.skip('Unsupported topology') + + # gather ansible facts + mg_facts=duthost.minigraph_facts(host=duthost.hostname)['ansible_facts'] + host_facts=duthost.setup()['ansible_facts'] + + tor_ports_ids={} + tor_ports=[] + spine_ports_ids={} + spine_ports=[] + tor_addr={} + tor_peer_addr={} + spine_addr={} + spine_peer_addr={} + tor_mac={} + spine_mac={} + + all_ifs=[] + + ip_ifaces=duthost.get_active_ip_interfaces(tbinfo, asic_index="all") + + for k,v in ip_ifaces[0].items(): + all_ifs.append(k) + logger.info(ip_ifaces[0][k]) + if 'T0' in v['bgp_neighbor']: + tor_ports.append(k) + tor_addr[k]=v['ipv4'] + tor_peer_addr[k]=v['peer_ipv4'] + elif 'T2' in v['bgp_neighbor']: + spine_ports.append(k) + spine_addr[k]=v['ipv4'] + spine_peer_addr[k]=v['peer_ipv4'] + + logger.info('tor_ports: {}'.format(tor_ports)) + logger.info('spine_ports: {}'.format(spine_ports)) + logger.info('tor_addr: {}'.format(tor_addr)) + + for dut_port in tor_ports: + port_id=mg_facts['minigraph_port_indices'][dut_port] + tor_ports_ids[dut_port]=port_id + ansible_port='ansible_'+dut_port + tor_mac[dut_port]=host_facts[ansible_port]['macaddress'] + + for dut_port in spine_ports: + port_id=mg_facts['minigraph_port_indices'][dut_port] + spine_ports_ids[dut_port]=port_id + ansible_port='ansible_'+dut_port + spine_mac[dut_port]=host_facts[ansible_port]['macaddress'] + + logger.info('spine_mac: {}'.format(spine_mac)) + logger.info('spine_ports_ids: {}'.format(spine_ports_ids)) + + src_port=random.choice(spine_ports) + dst_port=random.choice(tor_ports) + + dst_pid=tor_ports_ids[dst_port] + src_pid=spine_ports_ids[src_port] + + dst_mac=tor_mac[dst_port] + src_mac=spine_mac[src_port] + + dst_addr=tor_addr[dst_port] + src_addr=spine_addr[src_port] + + dst_peer_addr=tor_peer_addr[dst_port] + src_peer_addr=spine_peer_addr[src_port] + + setup_information={ + 'duthost': duthost, + 'dut_tmp_dir': DUT_TMP_DIR, + 'dst_ip_spine_blocked': '192.168.144.1', + 'src_port': src_port, + 'dst_port': dst_port, + 'src_addr': src_addr, + 'src_peer_addr': src_peer_addr, + 'dst_addr': dst_addr, + 'dst_peer_addr': dst_peer_addr, + 'src_pid': src_pid, + 'dst_pid': dst_pid, + 'src_mac': src_mac, + 'dst_mac': dst_mac, + } + + logger.info('setup variables {}'.format(pprint.pformat(setup))) + + # FIXME: There seems to be some issue with the initial setup of the ptfadapter, causing some of the + # TestBasicMPLS tests to fail because the forwarded packets are not being collected. This is an + # attempt to mitigate that issue while we continue to investigate the root cause. + # + # Ref: GitHub Issue #2032 + logger.info("setting up the ptfadapter") + ptfadapter.reinit() + + yield setup_information + + duthost.command('rm -rf {}'.format(os.path.join(DUT_TMP_DIR, LABEL_POP_ROUTES, '.json'))) + duthost.command('rm -rf {}'.format(os.path.join(DUT_TMP_DIR, LABEL_SWAP_ROUTES, '.json'))) + duthost.command('rm -rf {}'.format(os.path.join(DUT_TMP_DIR, LABEL_PUSH_ROUTES, '.json'))) + duthost.command('rm -rf {}'.format(os.path.join(DUT_TMP_DIR, LABEL_DEL_ROUTES, '.json'))) diff --git a/tests/mpls/test_mpls.py b/tests/mpls/test_mpls.py new file mode 100644 index 0000000000..0091ebbc78 --- /dev/null +++ b/tests/mpls/test_mpls.py @@ -0,0 +1,289 @@ +import logging +import os +import pprint +import ptf.mask as mask +import ptf.packet as packet +import ptf.testutils as testutils +import pytest +import time + +logger=logging.getLogger(__name__) + +pytestmark=[ + pytest.mark.topology('t1'), +] +CONFIGS_DIR=os.path.dirname(os.path.realpath(__file__)) +ADD_DIR=os.path.join(CONFIGS_DIR, 'configs') + +LABEL_POP_ROUTES='label_pop_routes' +LABEL_PUSH_ROUTES='label_push_routes' +LABEL_SWAP_ROUTES='label_swap_routes' +LABEL_DEL_ROUTES='label_del_routes' + +class TestBasicMpls: + """ + Base class for MPLS label testing. + Derivatives have to provide @setup_rules method to prepare DUT for MPLS traffic test and + optionally override @teardown_rules which base implementation is simply applying empty MPLS labels + configuration file + """ + def teardown_labels(self, setup): + """ + teardown MPLS label after test by applying empty configuration + :param dut: DUT host object + :param setup: setup information + :return: + """ + logger.info("Remove mpls comfigs") + self.config_interface_mpls(setup, LABEL_DEL_ROUTES, False) + + def icmp_packet(self, setup, ptfadapter): + """ create ICMP packet for testing """ + return testutils.simple_icmp_packet( + eth_dst=setup['src_mac'], + eth_src=ptfadapter.dataplane.get_mac(0, setup['src_pid']), + ip_dst='192.168.0.1', + ip_src=setup['src_addr'], + icmp_type=8, + icmp_code=0, + ip_ttl=64, + ) + def mpls_packet(self, setup, ptfadapter): + """ create MPLS packet for testing """ + return testutils.simple_mpls_packet( + eth_dst=setup['src_mac'], + eth_src=ptfadapter.dataplane.get_mac(0, setup['src_pid']), + mpls_tags=[ + { + 'label':1000001, + 'ttl': 63, + 's':1 + } + ], + inner_frame=testutils.simple_ip_only_packet( + ip_dst='192.168.0.1', + ip_src=setup['src_addr'], + ) + ) + + def mpls_stack_packet(self, setup, ptfadapter): + """ create MPLS packet for testing """ + return testutils.simple_mpls_packet( + eth_dst=setup['src_mac'], + eth_src=ptfadapter.dataplane.get_mac(0, setup['src_pid']), + mpls_tags=[ + { + 'label':1000001, + 'ttl': 255, + 's':0 + }, + { + 'label':1000010, + 'ttl': 255, + 's':0 + }, + { + 'label':1000011, + 'ttl': 255, + 's':1 + } + ], + inner_frame=testutils.simple_ip_only_packet( + ip_dst='192.168.0.1', + ip_src=setup['src_addr'], + ) + ) + + def expected_mask_ip_packet(self, pkt): + """ return mask for ip packet """ + + epkt=pkt.copy() + exp_pkt=pkt.copy() + exp_pkt['IP'].ttl=62 + pkt1=exp_pkt['IP'] + exp_pkt['Ethernet'].type=0x0800 + exp_pkt['Ethernet'].remove_payload() + exp_pkt /= pkt1 + exp_pkt=mask.Mask(exp_pkt) + exp_pkt=mask.Mask(exp_pkt) + 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') + return exp_pkt + + def expected_mask_mpls_swap_packet(self, pkt, exp_label): + """ return mask for mpls packet """ + + exp_pkt=pkt.copy() + exp_pkt['MPLS'].ttl -= 1 + exp_pkt['MPLS'].label=exp_label + exp_pkt=mask.Mask(exp_pkt) + 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') + + return exp_pkt + + def expected_mask_mpls_push_packet(self, pkt, exp_label): + """ return mask for mpls packet """ + + exp_pkt=pkt.copy() + exp_pkt['MPLS'].ttl=exp_pkt['IP'].ttl - 1 + exp_pkt['IP'].ttl -= 1 + exp_pkt['MPLS'].label=exp_label + exp_pkt=mask.Mask(exp_pkt) + 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') + + return exp_pkt + + def config_interface_mpls(self, setup, config_file, enable=True): + """ enable/disable mpls on interface """ + duthost=setup['duthost'] + + dst_port=setup['dst_port'] + src_port=setup['src_port'] + + dst_peer_addr=setup['dst_peer_addr'] + src_peer_addr=setup['src_peer_addr'] + + config_variables={ + 'dst_port': dst_port, + 'src_port': src_port, + 'dst_peer_addr': dst_peer_addr, + 'src_peer_addr': src_peer_addr, + } + + logger.info('extra variables for MPLS config:\n{}'.format(pprint.pformat(config_variables))) + duthost.host.options['variable_manager'].extra_vars.update(config_variables) + + logger.info('generate config for MPLS') + mpls_config='{}.json'.format(config_file) + mpls_config_path=os.path.join(setup['dut_tmp_dir'], mpls_config) + template_file='{}.j2'.format(config_file) + duthost.template(src=os.path.join(ADD_DIR, template_file), dest=mpls_config_path) + + + for intf in [dst_port, src_port]: + if enable: + result=duthost.shell('config interface mpls add {}'.format(intf), + module_ignore_errors=True) + if result['rc'] != 0: + pytest.fail('Failed to enable mplson interface {} : {}'.format(intf, result['stderr'])) + else: + result=duthost.shell('config interface mpls remove {}'.format(intf), + module_ignore_errors=True) + if result['rc'] != 0: + pytest.fail('Failed to disable mpls on interface {} : {}'.format(intf, result['stderr'])) + + # Apply config with swssconfig + result=duthost.shell('docker exec -i swss swssconfig /dev/stdin < {}'.format(mpls_config_path), + module_ignore_errors=True) + if result['rc'] != 0: + pytest.fail('Failed to apply labelroute configuration file: {}'.format(result['stderr'])) + + def test_pop_label(self, setup, ptfadapter): + """ test pop label """ + dst_pid=setup['dst_pid'] + src_pid=setup['src_pid'] + + self.config_interface_mpls(setup, LABEL_POP_ROUTES) + + time.sleep(2) + + pkt=self.mpls_packet(setup, ptfadapter) + exp_pkt=self.expected_mask_ip_packet(pkt) + + ptfadapter.dataplane.flush() + testutils.send(ptfadapter, src_pid, pkt) + try: + res=testutils.verify_packet_any_port(ptfadapter, exp_pkt, ports=[dst_pid]) + logger.info(res) + except Exception as e: + self.teardown_labels(setup) + pytest.fail('MPLS pop test failed \n'+ str(e)) + + self.teardown_labels(setup) + + def test_swap_label(self, setup, ptfadapter): + """ test swap label """ + + dst_pid=setup['dst_pid'] + src_pid=setup['src_pid'] + + self.config_interface_mpls(setup, LABEL_SWAP_ROUTES) + + time.sleep(2) + + pkt=self.mpls_packet(setup, ptfadapter) + exp_pkt=self.expected_mask_mpls_swap_packet(pkt, 1000002) + + ptfadapter.dataplane.flush() + testutils.send(ptfadapter, src_pid, pkt) + try: + res=testutils.verify_packet_any_port(ptfadapter, exp_pkt, ports=[dst_pid]) + logger.info(res) + except Exception as e: + self.teardown_labels(setup) + pytest.fail('MPLS swap test failed \n' + str(e)) + + self.teardown_labels(setup) + + def test_push_label(self, setup, ptfadapter): + """ test push label """ + + dst_pid=setup['dst_pid'] + src_pid=setup['src_pid'] + + self.config_interface_mpls(setup, LABEL_PUSH_ROUTES) + + time.sleep(2) + + pkt=self.icmp_packet(setup, ptfadapter) + epkt=pkt.copy() + pkt1=epkt['IP'] + epkt['Ethernet'].type=0x8847 + epkt['Ethernet'].remove_payload() + mp=MPLS(label=1000002, s=1, ttl=255) + mp.remove_payload() + epkt /= mp + epkt /= pkt1 + exp_pkt=self.expected_mask_mpls_push_packet(epkt, 1000001) + + ptfadapter.dataplane.flush() + testutils.send(ptfadapter, src_pid, pkt) + + try: + res=testutils.verify_packet_any_port(ptfadapter, exp_pkt, ports=[dst_pid]) + logger.info(res) + except Exception as e: + self.teardown_labels(setup) + pytest.fail('MPLS push test failed \n' + str(e)) + + self.teardown_labels(setup) + + def test_swap_labelstack(self, setup, ptfadapter): + """ test swap labelstack """ + + dst_pid=setup['dst_pid'] + src_pid=setup['src_pid'] + + self.config_interface_mpls(setup, LABEL_SWAP_ROUTES) + + time.sleep(2) + + pkt=self.mpls_stack_packet(setup, ptfadapter) + exp_pkt=self.expected_mask_mpls_swap_packet(pkt, 1000002) + + ptfadapter.dataplane.flush() + testutils.send(ptfadapter, src_pid, pkt) + + try: + res=testutils.verify_packet_any_port(ptfadapter, exp_pkt, ports=[dst_pid]) + logger.info(res) + except Exception as e: + self.teardown_labels(setup) + pytest.fail('MPLS swap labelstack test failed \n' + str(e)) + + self.teardown_labels(setup)