test: l2xc instances multi-context test (CSIT-491)

- add/delete L2XC instances and verify results by traffic

Change-Id: Ib538a98e499ae3c9c9601a22afcabb1afb84c881
Signed-off-by: Jan Gelety <jgelety@cisco.com>
diff --git a/test/test_l2xc_multi_instance.py b/test/test_l2xc_multi_instance.py
new file mode 100644
index 0000000..6f5ab6f
--- /dev/null
+++ b/test/test_l2xc_multi_instance.py
@@ -0,0 +1,351 @@
+#!/usr/bin/env python
+"""L2XC Multi-instance Test Case HLD:
+
+**NOTES:**
+    - higher number (more than 15) of pg-l2 interfaces causes problems => only \
+    14 pg-l2 interfaces and 10 cross-connects are tested
+    - jumbo packets in configuration with 14 l2-pg interfaces leads to \
+    problems too
+
+**config 1**
+    - add 14 pg-l2 interfaces
+    - add 10 cross-connects (two cross-connects per pair of l2-pg interfaces)
+
+**test 1**
+    - send L2 MAC frames between all pairs of pg-l2 interfaces
+
+**verify 1**
+    - all packets received correctly in case of cross-connected l2-pg interfaces
+    - no packet received in case of not cross-connected l2-pg interfaces
+
+**config 2**
+    - delete 4 cross-connects
+
+**test 2**
+    - send L2 MAC frames between all pairs of pg-l2 interfaces
+
+**verify 2**
+    - all packets received correctly in case of cross-connected l2-pg interfaces
+    - no packet received in case of not cross-connected l2-pg interfaces
+
+**config 3**
+    - add new 4 cross-connects
+
+**test 3**
+    - send L2 MAC frames between all pairs of pg-l2 interfaces
+
+**verify 3**
+    - all packets received correctly in case of cross-connected l2-pg interfaces
+    - no packet received in case of not cross-connected l2-pg interfaces
+
+**config 4**
+    - delete 10 cross-connects
+
+**test 4**
+    - send L2 MAC frames between all pairs of pg-l2 interfaces
+
+**verify 4**
+    - no packet received on all of l2-pg interfaces (no cross-connect created)
+"""
+
+import unittest
+import random
+
+from scapy.packet import Raw
+from scapy.layers.l2 import Ether
+from scapy.layers.inet import IP, UDP
+
+from framework import VppTestCase, VppTestRunner
+from util import Host
+
+
+class TestL2xcMultiInst(VppTestCase):
+    """ L2XC Multi-instance Test Case """
+
+    @classmethod
+    def setUpClass(cls):
+        """
+        Perform standard class setup (defined by class method setUpClass in
+        class VppTestCase) before running the test case, set test case related
+        variables and configure VPP.
+        """
+        super(TestL2xcMultiInst, cls).setUpClass()
+
+        try:
+            # Create pg interfaces
+            cls.create_pg_interfaces(range(14))
+
+            # Packet flows mapping pg0 -> pg1 etc.
+            cls.flows = dict()
+            for i in range(len(cls.pg_interfaces)):
+                delta = 1 if i % 2 == 0 else -1
+                cls.flows[cls.pg_interfaces[i]] = [cls.pg_interfaces[i+delta]]
+
+            # Mapping between packet-generator index and lists of test hosts
+            cls.hosts_by_pg_idx = dict()
+            for pg_if in cls.pg_interfaces:
+                cls.hosts_by_pg_idx[pg_if.sw_if_index] = []
+
+            # Create test host entries
+            cls.create_hosts(70)
+
+            # Packet sizes - jumbo packet (9018 bytes) skipped
+            cls.pg_if_packet_sizes = [64, 512, 1518]
+
+            # Set up all interfaces
+            for i in cls.pg_interfaces:
+                i.admin_up()
+
+            # Create list of x-connected pg_interfaces
+            cls.pg_in_xc = list()
+
+            # Create list of not x-connected pg_interfaces
+            cls.pg_not_in_xc = list()
+            for pg_if in cls.pg_interfaces:
+                cls.pg_not_in_xc.append(pg_if)
+
+        except Exception:
+            super(TestL2xcMultiInst, cls).tearDownClass()
+            raise
+
+    def setUp(self):
+        """
+        Clear trace and packet infos before running each test.
+        """
+        super(TestL2xcMultiInst, self).setUp()
+        self.packet_infos = {}
+
+    def tearDown(self):
+        """
+        Show various debug prints after each test.
+        """
+        super(TestL2xcMultiInst, self).tearDown()
+        if not self.vpp_dead:
+            self.logger.info(self.vapi.ppcli("show l2patch"))
+
+    @classmethod
+    def create_hosts(cls, count):
+        """
+        Create required number of host MAC addresses and distribute them among
+        interfaces. Create host IPv4 address for every host MAC address.
+
+        :param int count: Number of hosts to create MAC/IPv4 addresses for.
+        """
+        n_int = len(cls.pg_interfaces)
+        macs_per_if = count / n_int
+        i = -1
+        for pg_if in cls.pg_interfaces:
+            i += 1
+            start_nr = macs_per_if * i
+            end_nr = count if i == (n_int - 1) else macs_per_if * (i + 1)
+            hosts = cls.hosts_by_pg_idx[pg_if.sw_if_index]
+            for j in range(start_nr, end_nr):
+                host = Host(
+                    "00:00:00:ff:%02x:%02x" % (pg_if.sw_if_index, j),
+                    "172.17.1%02u.%u" % (pg_if.sw_if_index, j))
+                hosts.append(host)
+
+    def create_xconnects(self, count, start=0):
+        """
+        Create required number of cross-connects (always two cross-connects per
+        pair of packet-generator interfaces).
+
+        :param int count: Number of cross-connects to be created.
+        :param int start: Starting index of packet-generator interfaces. \
+        (Default value = 0)
+        """
+        for i in range(count):
+            rx_if = self.pg_interfaces[i+start]
+            delta = 1 if i % 2 == 0 else -1
+            tx_if = self.pg_interfaces[i+start+delta]
+            self.vapi.sw_interface_set_l2_xconnect(rx_if.sw_if_index,
+                                                   tx_if.sw_if_index, 1)
+            self.logger.info("Cross-connect from %s to %s created"
+                             % (tx_if.name, rx_if.name))
+            if self.pg_in_xc.count(rx_if) == 0:
+                self.pg_in_xc.append(rx_if)
+            if self.pg_not_in_xc.count(rx_if) == 1:
+                self.pg_not_in_xc.remove(rx_if)
+
+    def delete_xconnects(self, count, start=0):
+        """
+        Delete required number of cross-connects (always two cross-connects per
+        pair of packet-generator interfaces).
+
+        :param int count: Number of cross-connects to be deleted.
+        :param int start: Starting index of packet-generator interfaces. \
+        (Default value = 0)
+        """
+        for i in range(count):
+            rx_if = self.pg_interfaces[i+start]
+            delta = 1 if i % 2 == 0 else -1
+            tx_if = self.pg_interfaces[i+start+delta]
+            self.vapi.sw_interface_set_l2_xconnect(rx_if.sw_if_index,
+                                                   tx_if.sw_if_index, 0)
+            self.logger.info("Cross-connect from %s to %s deleted"
+                             % (tx_if.name, rx_if.name))
+            if self.pg_not_in_xc.count(rx_if) == 0:
+                self.pg_not_in_xc.append(rx_if)
+            if self.pg_in_xc.count(rx_if) == 1:
+                self.pg_in_xc.remove(rx_if)
+
+    def create_stream(self, src_if, packet_sizes):
+        """
+        Create input packet stream for defined interface using hosts list.
+
+        :param object src_if: Interface to create packet stream for.
+        :param list packet_sizes: List of required packet sizes.
+        :return: Stream of packets.
+        """
+        pkts = []
+        src_hosts = self.hosts_by_pg_idx[src_if.sw_if_index]
+        for dst_if in self.flows[src_if]:
+            dst_hosts = self.hosts_by_pg_idx[dst_if.sw_if_index]
+            n_int = len(dst_hosts)
+            for i in range(0, n_int):
+                dst_host = dst_hosts[i]
+                src_host = random.choice(src_hosts)
+                pkt_info = self.create_packet_info(
+                    src_if.sw_if_index, dst_if.sw_if_index)
+                payload = self.info_to_payload(pkt_info)
+                p = (Ether(dst=dst_host.mac, src=src_host.mac) /
+                     IP(src=src_host.ip4, dst=dst_host.ip4) /
+                     UDP(sport=1234, dport=1234) /
+                     Raw(payload))
+                pkt_info.data = p.copy()
+                size = random.choice(packet_sizes)
+                self.extend_packet(p, size)
+                pkts.append(p)
+        self.logger.debug("Input stream created for port %s. Length: %u pkt(s)"
+                          % (src_if.name, len(pkts)))
+        return pkts
+
+    def verify_capture(self, pg_if, capture):
+        """
+        Verify captured input packet stream for defined interface.
+
+        :param object pg_if: Interface to verify captured packet stream for.
+        :param list capture: Captured packet stream.
+        """
+        last_info = dict()
+        for i in self.pg_interfaces:
+            last_info[i.sw_if_index] = None
+        dst_sw_if_index = pg_if.sw_if_index
+        for packet in capture:
+            payload_info = self.payload_to_info(str(packet[Raw]))
+            try:
+                ip = packet[IP]
+                udp = packet[UDP]
+                packet_index = payload_info.index
+                self.assertEqual(payload_info.dst, dst_sw_if_index)
+                self.logger.debug("Got packet on port %s: src=%u (id=%u)" %
+                                  (pg_if.name, payload_info.src, packet_index))
+                next_info = self.get_next_packet_info_for_interface2(
+                    payload_info.src, dst_sw_if_index,
+                    last_info[payload_info.src])
+                last_info[payload_info.src] = next_info
+                self.assertTrue(next_info is not None)
+                self.assertEqual(packet_index, next_info.index)
+                saved_packet = next_info.data
+                # Check standard fields
+                self.assertEqual(ip.src, saved_packet[IP].src)
+                self.assertEqual(ip.dst, saved_packet[IP].dst)
+                self.assertEqual(udp.sport, saved_packet[UDP].sport)
+                self.assertEqual(udp.dport, saved_packet[UDP].dport)
+            except:
+                self.logger.error("Unexpected or invalid packet:")
+                self.logger.error(packet.show())
+                raise
+        for i in self.pg_interfaces:
+            remaining_packet = self.get_next_packet_info_for_interface2(
+                i, dst_sw_if_index, last_info[i.sw_if_index])
+            self.assertTrue(
+                remaining_packet is None,
+                "Port %u: Packet expected from source %u didn't arrive" %
+                (dst_sw_if_index, i.sw_if_index))
+
+    def run_verify_test(self):
+        """
+        Create packet streams for all configured l2-pg interfaces, send all
+        prepared packet streams and verify that:
+            - all packets received correctly on all pg-l2 interfaces assigned \
+        to cross-connects
+            - no packet received on all pg-l2 interfaces not assigned to \
+        cross-connects
+
+        :raise: RuntimeError if no packet captured on l2-pg interface assigned \
+        to the cross-connect or if any packet is captured on l2-pg interface \
+        not assigned to the cross-connect.
+        """
+        # Test
+        # Create incoming packet streams for packet-generator interfaces
+        for pg_if in self.pg_interfaces:
+            pkts = self.create_stream(pg_if, self.pg_if_packet_sizes)
+            pg_if.add_stream(pkts)
+
+        # Enable packet capture and start packet sending
+        self.pg_enable_capture(self.pg_interfaces)
+        self.pg_start()
+
+        # Verify
+        # Verify outgoing packet streams per packet-generator interface
+        for pg_if in self.pg_interfaces:
+            capture = pg_if.get_capture()
+            if pg_if in self.pg_in_xc:
+                if len(capture) == 0:
+                    raise RuntimeError("Interface %s is cross-connect sink but "
+                                       "the capture is empty!" % pg_if.name)
+                self.verify_capture(pg_if, capture)
+            elif pg_if in self.pg_not_in_xc:
+                try:
+                    self.assertEqual(len(capture), 0)
+                except AssertionError:
+                    raise RuntimeError("Interface %s is not cross-connect sink "
+                                       "but the capture is not empty!"
+                                       % pg_if.name)
+            else:
+                self.logger.error("Unknown interface: %s" % pg_if.name)
+
+    def test_l2xc_inst_01(self):
+        """ L2XC Multi-instance test 1 - create 10 cross-connects
+        """
+        # Config 1
+        # Create 10 cross-connects
+        self.create_xconnects(10)
+
+        # Test 1
+        self.run_verify_test()
+
+    def test_l2xc_inst_02(self):
+        """ L2XC Multi-instance test 2 - delete 4 cross-connects
+        """
+        # Config 2
+        # Delete 4 cross-connects
+        self.delete_xconnects(4)
+
+        # Test 2
+        self.run_verify_test()
+
+    def test_l2xc_inst_03(self):
+        """ L2BD Multi-instance 3 - add new 4 cross-connects
+        """
+        # Config 3
+        # Add new 4 cross-connects
+        self.create_xconnects(4, start=10)
+
+        # Test 3
+        self.run_verify_test()
+
+    def test_l2xc_inst_04(self):
+        """ L2XC Multi-instance test 4 - delete 10 cross-connects
+        """
+        # Config 4
+        # Delete 10 cross-connects
+        self.delete_xconnects(10, start=4)
+
+        # Test 4
+        self.run_verify_test()
+
+
+if __name__ == '__main__':
+    unittest.main(testRunner=VppTestRunner)