lb: remove api boilerplate
[vpp.git] / test / test_ip_ecmp.py
1 #!/usr/bin/env python
2
3 import unittest
4 import random
5 import socket
6 from ipaddress import IPv4Address, IPv6Address, AddressValueError
7
8 from framework import VppTestCase, VppTestRunner
9 from util import ppp
10
11 from scapy.packet import Raw
12 from scapy.layers.l2 import Ether
13 from scapy.layers.inet import IP, UDP
14 from scapy.layers.inet6 import IPv6
15 from vpp_ip_route import VppIpRoute, VppRoutePath
16
17 try:
18     text_type = unicode
19 except NameError:
20     text_type = str
21
22 #
23 # The number of packets to sent.
24 #
25 N_PKTS_IN_STREAM = 300
26
27
28 class TestECMP(VppTestCase):
29     """ Equal-cost multi-path routing Test Case """
30
31     @classmethod
32     def setUpClass(cls):
33         """
34         Perform standard class setup (defined by class method setUpClass in
35         class VppTestCase) before running the test case, set test case related
36         variables and configure VPP.
37         """
38         super(TestECMP, cls).setUpClass()
39
40         # create 4 pg interfaces
41         cls.create_pg_interfaces(range(4))
42
43         # packet sizes to test
44         cls.pg_if_packet_sizes = [64, 1500, 9018]
45
46         # setup interfaces
47         for i in cls.pg_interfaces:
48             i.admin_up()
49             i.generate_remote_hosts(5)
50             i.config_ip4()
51             i.resolve_arp()
52             i.configure_ipv4_neighbors()
53             i.config_ip6()
54             i.resolve_ndp()
55             i.configure_ipv6_neighbors()
56
57     @classmethod
58     def tearDownClass(cls):
59         if not cls.vpp_dead:
60             for i in cls.pg_interfaces:
61                 i.unconfig_ip4()
62                 i.unconfig_ip6()
63                 i.admin_down()
64
65         super(TestECMP, cls).tearDownClass()
66
67     def setUp(self):
68         super(TestECMP, self).setUp()
69         self.reset_packet_infos()
70
71     def tearDown(self):
72         """
73         Show various debug prints after each test.
74         """
75         super(TestECMP, self).tearDown()
76
77     def show_commands_at_teardown(self):
78         self.logger.info(self.vapi.ppcli("show ip arp"))
79         self.logger.info(self.vapi.ppcli("show ip6 neighbors"))
80
81     def get_ip_address(self, ip_addr_start, ip_prefix_len):
82         """
83
84         :param str ip_addr_start: Starting IPv4 or IPv6 address.
85         :param int ip_prefix_len: IP address prefix length.
86         :return: Random IPv4 or IPv6 address from required range.
87         """
88         try:
89             ip_addr = IPv4Address(text_type(ip_addr_start))
90             ip_max_len = 32
91         except (AttributeError, AddressValueError):
92             ip_addr = IPv6Address(text_type(ip_addr_start))
93             ip_max_len = 128
94
95         return str(ip_addr +
96                    random.randint(0, 2 ** (ip_max_len - ip_prefix_len) - 2))
97
98     def create_stream(self, src_if, src_ip_start, dst_ip_start,
99                       ip_prefix_len, packet_sizes, ip_l=IP):
100         """Create input packet stream for defined interfaces.
101
102         :param VppInterface src_if: Source Interface for packet stream.
103         :param str src_ip_start: Starting source IPv4 or IPv6 address.
104         :param str dst_ip_start: Starting destination IPv4 or IPv6 address.
105         :param int ip_prefix_len: IP address prefix length.
106         :param list packet_sizes: packet size to test.
107         :param Scapy ip_l: Required IP layer - IP or IPv6. (Default is IP.)
108         """
109         pkts = []
110         for i in range(0, N_PKTS_IN_STREAM):
111             info = self.create_packet_info(src_if, src_if)
112             payload = self.info_to_payload(info)
113             src_ip = self.get_ip_address(src_ip_start, ip_prefix_len)
114             dst_ip = self.get_ip_address(dst_ip_start, ip_prefix_len)
115             p = (Ether(dst=src_if.local_mac, src=src_if.remote_mac) /
116                  ip_l(src=src_ip, dst=dst_ip) /
117                  UDP(sport=1234, dport=1234) /
118                  Raw(payload))
119             info.data = p.copy()
120             size = random.choice(packet_sizes)
121             self.extend_packet(p, size)
122             pkts.append(p)
123         return pkts
124
125     def verify_capture(self, rx_if, capture, ip_l=IP):
126         """Verify captured input packet stream for defined interface.
127
128         :param VppInterface rx_if: Interface to verify captured packet stream.
129         :param list capture: Captured packet stream.
130         :param Scapy ip_l: Required IP layer - IP or IPv6. (Default is IP.)
131         """
132         self.logger.info("Verifying capture on interface %s" % rx_if.name)
133
134         count = 0
135         host_counters = {}
136         for host_mac in rx_if._hosts_by_mac:
137             host_counters[host_mac] = 0
138
139         for packet in capture:
140             try:
141                 ip_received = packet[ip_l]
142                 payload_info = self.payload_to_info(packet[Raw])
143                 packet_index = payload_info.index
144                 ip_sent = self._packet_infos[packet_index].data[ip_l]
145                 self.logger.debug("Got packet on port %s: src=%u (id=%u)" %
146                                   (rx_if.name, payload_info.src, packet_index))
147                 # Check standard fields
148                 self.assertIn(packet.dst, rx_if._hosts_by_mac,
149                               "Destination MAC address %s shouldn't be routed "
150                               "via interface %s" % (packet.dst, rx_if.name))
151                 self.assertEqual(packet.src, rx_if.local_mac)
152                 self.assertEqual(ip_received.src, ip_sent.src)
153                 self.assertEqual(ip_received.dst, ip_sent.dst)
154                 host_counters[packet.dst] += 1
155                 self._packet_infos.pop(packet_index)
156
157             except:
158                 self.logger.error(ppp("Unexpected or invalid packet:", packet))
159                 raise
160
161         # We expect packet routed via all host of pg interface
162         for host_mac in host_counters:
163             nr = host_counters[host_mac]
164             self.assertNotEqual(
165                 nr, 0, "No packet routed via host %s" % host_mac)
166             self.logger.info("%u packets routed via host %s of %s interface" %
167                              (nr, host_mac, rx_if.name))
168             count += nr
169         self.logger.info("Total amount of %u packets routed via %s interface" %
170                          (count, rx_if.name))
171
172         return count
173
174     def create_ip_routes(self, dst_ip_net, dst_prefix_len, is_ipv6=0):
175         """
176         Create IP routes for defined destination IP network.
177
178         :param str dst_ip_net: Destination IP network.
179         :param int dst_prefix_len: IP address prefix length.
180         :param int is_ipv6: 0 if an ip4 route, else ip6
181         """
182
183         paths = []
184         for pg_if in self.pg_interfaces[1:]:
185             for nh_host in pg_if.remote_hosts:
186                 nh_host_ip = nh_host.ip4 if is_ipv6 == 0 else nh_host.ip6
187                 paths.append(VppRoutePath(nh_host_ip,
188                                           pg_if.sw_if_index))
189
190         rip = VppIpRoute(self, dst_ip_net, dst_prefix_len, paths)
191         rip.add_vpp_config()
192         self.logger.info("Route via %s on %s created" %
193                          (nh_host_ip, pg_if.name))
194
195         self.logger.debug(self.vapi.ppcli("show ip fib"))
196         self.logger.debug(self.vapi.ppcli("show ip6 fib"))
197
198     def test_ip_ecmp(self):
199         """ IP equal-cost multi-path routing test """
200
201         src_ip_net = '16.0.0.1'
202         dst_ip_net = '32.0.0.1'
203         ip_prefix_len = 24
204
205         self.create_ip_routes(dst_ip_net, ip_prefix_len)
206
207         pkts = self.create_stream(self.pg0, src_ip_net, dst_ip_net,
208                                   ip_prefix_len, self.pg_if_packet_sizes)
209         self.pg0.add_stream(pkts)
210
211         self.pg_enable_capture(self.pg_interfaces)
212         self.pg_start()
213
214         # We expect packets on pg1, pg2 and pg3, but not on pg0
215         rx_count = 0
216         for pg_if in self.pg_interfaces[1:]:
217             capture = pg_if._get_capture(timeout=1)
218             self.assertNotEqual(
219                 len(capture), 0, msg="No packets captured on %s" % pg_if.name)
220             rx_count += self.verify_capture(pg_if, capture)
221         self.pg0.assert_nothing_captured(remark="IP packets forwarded on pg0")
222
223         # Check that all packets were forwarded via pg1, pg2 and pg3
224         self.assertEqual(rx_count, len(pkts))
225
226     def test_ip6_ecmp(self):
227         """ IPv6 equal-cost multi-path routing test """
228
229         src_ip_net = '3ffe:51::1'
230         dst_ip_net = '3ffe:71::1'
231         ip_prefix_len = 64
232
233         self.create_ip_routes(dst_ip_net, ip_prefix_len, is_ipv6=1)
234
235         pkts = self.create_stream(
236             self.pg0, src_ip_net, dst_ip_net,
237             ip_prefix_len, self.pg_if_packet_sizes, ip_l=IPv6)
238         self.pg0.add_stream(pkts)
239
240         self.pg_enable_capture(self.pg_interfaces)
241         self.pg_start()
242
243         # We expect packets on pg1, pg2 and pg3, but not on pg0
244         rx_count = 0
245         for pg_if in self.pg_interfaces[1:]:
246             capture = pg_if._get_capture(timeout=1)
247             self.assertNotEqual(
248                 len(capture), 0, msg="No packets captured on %s" % pg_if.name)
249             rx_count += self.verify_capture(pg_if, capture, ip_l=IPv6)
250         self.pg0.assert_nothing_captured(remark="IP packets forwarded on pg0")
251
252         # Check that all packets were forwarded via pg1, pg2 and pg3
253         self.assertEqual(rx_count, len(pkts))
254
255
256 if __name__ == '__main__':
257     unittest.main(testRunner=VppTestRunner)