API refactoring : policer
[vpp.git] / test / vpp_pg_interface.py
1 import os
2 import time
3 import socket
4 from traceback import format_exc, format_stack
5 from scapy.utils import wrpcap, rdpcap, PcapReader
6 from scapy.plist import PacketList
7 from vpp_interface import VppInterface
8
9 from scapy.layers.l2 import Ether, ARP
10 from scapy.layers.inet6 import IPv6, ICMPv6ND_NS, ICMPv6ND_NA,\
11     ICMPv6NDOptSrcLLAddr, ICMPv6NDOptDstLLAddr, ICMPv6ND_RA, RouterAlert, \
12     IPv6ExtHdrHopByHop
13 from util import ppp, ppc
14 from scapy.utils6 import in6_getnsma, in6_getnsmac, in6_ismaddr
15 from scapy.utils import inet_pton, inet_ntop
16
17
18 def is_ipv6_misc(p):
19     """ Is packet one of uninteresting IPv6 broadcasts? """
20     if p.haslayer(ICMPv6ND_RA):
21         if in6_ismaddr(p[IPv6].dst):
22             return True
23     if p.haslayer(IPv6ExtHdrHopByHop):
24         for o in p[IPv6ExtHdrHopByHop].options:
25             if isinstance(o, RouterAlert):
26                 return True
27     return False
28
29
30 class VppPGInterface(VppInterface):
31     """
32     VPP packet-generator interface
33     """
34
35     @property
36     def pg_index(self):
37         """packet-generator interface index assigned by VPP"""
38         return self._pg_index
39
40     @property
41     def out_path(self):
42         """pcap file path - captured packets"""
43         return self._out_path
44
45     @property
46     def in_path(self):
47         """ pcap file path - injected packets"""
48         return self._in_path
49
50     @property
51     def capture_cli(self):
52         """CLI string to start capture on this interface"""
53         return self._capture_cli
54
55     @property
56     def cap_name(self):
57         """capture name for this interface"""
58         return self._cap_name
59
60     @property
61     def input_cli(self):
62         """CLI string to load the injected packets"""
63         return self._input_cli
64
65     @property
66     def in_history_counter(self):
67         """Self-incrementing counter used when renaming old pcap files"""
68         v = self._in_history_counter
69         self._in_history_counter += 1
70         return v
71
72     @property
73     def out_history_counter(self):
74         """Self-incrementing counter used when renaming old pcap files"""
75         v = self._out_history_counter
76         self._out_history_counter += 1
77         return v
78
79     def __init__(self, test, pg_index):
80         """ Create VPP packet-generator interface """
81         r = test.vapi.pg_create_interface(pg_index)
82         self._sw_if_index = r.sw_if_index
83
84         super(VppPGInterface, self).__init__(test)
85
86         self._in_history_counter = 0
87         self._out_history_counter = 0
88         self._out_assert_counter = 0
89         self._pg_index = pg_index
90         self._out_file = "pg%u_out.pcap" % self.pg_index
91         self._out_path = self.test.tempdir + "/" + self._out_file
92         self._in_file = "pg%u_in.pcap" % self.pg_index
93         self._in_path = self.test.tempdir + "/" + self._in_file
94         self._capture_cli = "packet-generator capture pg%u pcap %s" % (
95             self.pg_index, self.out_path)
96         self._cap_name = "pcap%u" % self.sw_if_index
97         self._input_cli = \
98             "packet-generator new pcap %s source pg%u name %s" % (
99                 self.in_path, self.pg_index, self.cap_name)
100
101     def enable_capture(self):
102         """ Enable capture on this packet-generator interface"""
103         try:
104             if os.path.isfile(self.out_path):
105                 os.rename(self.out_path,
106                           "%s/history.[timestamp:%f].[%s-counter:%04d].%s" %
107                           (self.test.tempdir,
108                            time.time(),
109                            self.name,
110                            self.out_history_counter,
111                            self._out_file))
112         except:
113             pass
114         # FIXME this should be an API, but no such exists atm
115         self.test.vapi.cli(self.capture_cli)
116         self._pcap_reader = None
117
118     def add_stream(self, pkts):
119         """
120         Add a stream of packets to this packet-generator
121
122         :param pkts: iterable packets
123
124         """
125         try:
126             if os.path.isfile(self.in_path):
127                 os.rename(self.in_path,
128                           "%s/history.[timestamp:%f].[%s-counter:%04d].%s" %
129                           (self.test.tempdir,
130                            time.time(),
131                            self.name,
132                            self.in_history_counter,
133                            self._in_file))
134         except:
135             pass
136         wrpcap(self.in_path, pkts)
137         self.test.register_capture(self.cap_name)
138         # FIXME this should be an API, but no such exists atm
139         self.test.vapi.cli(self.input_cli)
140
141     def generate_debug_aid(self, kind):
142         """ Create a hardlink to the out file with a counter and a file
143         containing stack trace to ease debugging in case of multiple capture
144         files present. """
145         self.test.logger.debug("Generating debug aid for %s on %s" %
146                                (kind, self._name))
147         link_path, stack_path = ["%s/debug_%s_%s_%s.%s" %
148                                  (self.test.tempdir, self._name,
149                                   self._out_assert_counter, kind, suffix)
150                                  for suffix in ["pcap", "stack"]
151                                  ]
152         os.link(self.out_path, link_path)
153         with open(stack_path, "w") as f:
154             f.writelines(format_stack())
155         self._out_assert_counter += 1
156
157     def _get_capture(self, timeout, filter_out_fn=is_ipv6_misc):
158         """ Helper method to get capture and filter it """
159         try:
160             if not self.wait_for_capture_file(timeout):
161                 return None
162             output = rdpcap(self.out_path)
163             self.test.logger.debug("Capture has %s packets" % len(output.res))
164         except:
165             self.test.logger.debug("Exception in scapy.rdpcap (%s): %s" %
166                                    (self.out_path, format_exc()))
167             return None
168         before = len(output.res)
169         if filter_out_fn:
170             output.res = [p for p in output.res if not filter_out_fn(p)]
171         removed = before - len(output.res)
172         if removed:
173             self.test.logger.debug(
174                 "Filtered out %s packets from capture (returning %s)" %
175                 (removed, len(output.res)))
176         return output
177
178     def get_capture(self, expected_count=None, remark=None, timeout=1,
179                     filter_out_fn=is_ipv6_misc):
180         """ Get captured packets
181
182         :param expected_count: expected number of packets to capture, if None,
183                                then self.test.packet_count_for_dst_pg_idx is
184                                used to lookup the expected count
185         :param remark: remark printed into debug logs
186         :param timeout: how long to wait for packets
187         :param filter_out_fn: filter applied to each packet, packets for which
188                               the filter returns True are removed from capture
189         :returns: iterable packets
190         """
191         remaining_time = timeout
192         capture = None
193         name = self.name if remark is None else "%s (%s)" % (self.name, remark)
194         based_on = "based on provided argument"
195         if expected_count is None:
196             expected_count = \
197                 self.test.get_packet_count_for_if_idx(self.sw_if_index)
198             based_on = "based on stored packet_infos"
199             if expected_count == 0:
200                 raise Exception(
201                     "Internal error, expected packet count for %s is 0!" %
202                     name)
203         self.test.logger.debug("Expecting to capture %s (%s) packets on %s" % (
204             expected_count, based_on, name))
205         while remaining_time > 0:
206             before = time.time()
207             capture = self._get_capture(remaining_time, filter_out_fn)
208             elapsed_time = time.time() - before
209             if capture:
210                 if len(capture.res) == expected_count:
211                     # bingo, got the packets we expected
212                     return capture
213                 else:
214                     self.test.logger.debug("Partial capture containing %s "
215                                            "packets doesn't match expected "
216                                            "count %s (yet?)" %
217                                            (len(capture.res), expected_count))
218             elif expected_count == 0:
219                 # bingo, got None as we expected - return empty capture
220                 return PacketList()
221             remaining_time -= elapsed_time
222         if capture:
223             self.generate_debug_aid("count-mismatch")
224             raise Exception("Captured packets mismatch, captured %s packets, "
225                             "expected %s packets on %s" %
226                             (len(capture.res), expected_count, name))
227         else:
228             raise Exception("No packets captured on %s" % name)
229
230     def assert_nothing_captured(self, remark=None, filter_out_fn=is_ipv6_misc):
231         """ Assert that nothing unfiltered was captured on interface
232
233         :param remark: remark printed into debug logs
234         :param filter_out_fn: filter applied to each packet, packets for which
235                               the filter returns True are removed from capture
236         """
237         if os.path.isfile(self.out_path):
238             try:
239                 capture = self.get_capture(
240                     0, remark=remark, filter_out_fn=filter_out_fn)
241                 if not capture or len(capture.res) == 0:
242                     # junk filtered out, we're good
243                     return
244                 self.test.logger.error(
245                     ppc("Unexpected packets captured:", capture))
246             except:
247                 pass
248             self.generate_debug_aid("empty-assert")
249             if remark:
250                 raise AssertionError(
251                     "Non-empty capture file present for interface %s (%s)" %
252                     (self.name, remark))
253             else:
254                 raise AssertionError("Capture file present for interface %s" %
255                                      self.name)
256
257     def wait_for_capture_file(self, timeout=1):
258         """
259         Wait until pcap capture file appears
260
261         :param timeout: How long to wait for the packet (default 1s)
262
263         :returns: True/False if the file is present or appears within timeout
264         """
265         limit = time.time() + timeout
266         if not os.path.isfile(self.out_path):
267             self.test.logger.debug("Waiting for capture file %s to appear, "
268                                    "timeout is %ss" % (self.out_path, timeout))
269         else:
270             self.test.logger.debug(
271                 "Capture file %s already exists" %
272                 self.out_path)
273             return True
274         while time.time() < limit:
275             if os.path.isfile(self.out_path):
276                 break
277             time.sleep(0)  # yield
278         if os.path.isfile(self.out_path):
279             self.test.logger.debug("Capture file appeared after %fs" %
280                                    (time.time() - (limit - timeout)))
281         else:
282             self.test.logger.debug("Timeout - capture file still nowhere")
283             return False
284         return True
285
286     def wait_for_packet(self, timeout, filter_out_fn=is_ipv6_misc):
287         """
288         Wait for next packet captured with a timeout
289
290         :param timeout: How long to wait for the packet
291
292         :returns: Captured packet if no packet arrived within timeout
293         :raises Exception: if no packet arrives within timeout
294         """
295         deadline = time.time() + timeout
296         if self._pcap_reader is None:
297             if not self.wait_for_capture_file(timeout):
298                 raise Exception("Capture file %s did not appear within "
299                                 "timeout" % self.out_path)
300             while time.time() < deadline:
301                 try:
302                     self._pcap_reader = PcapReader(self.out_path)
303                     break
304                 except:
305                     self.test.logger.debug(
306                         "Exception in scapy.PcapReader(%s): %s" %
307                         (self.out_path, format_exc()))
308         if not self._pcap_reader:
309             raise Exception("Capture file %s did not appear within "
310                             "timeout" % self.out_path)
311
312         self.test.logger.debug("Waiting for packet")
313         while time.time() < deadline:
314             p = self._pcap_reader.recv()
315             if p is not None:
316                 if filter_out_fn is not None and filter_out_fn(p):
317                     self.test.logger.debug(
318                         "Packet received after %ss was filtered out" %
319                         (time.time() - (deadline - timeout)))
320                 else:
321                     self.test.logger.debug(
322                         "Packet received after %fs" %
323                         (time.time() - (deadline - timeout)))
324                     return p
325             time.sleep(0)  # yield
326         self.test.logger.debug("Timeout - no packets received")
327         raise Exception("Packet didn't arrive within timeout")
328
329     def create_arp_req(self):
330         """Create ARP request applicable for this interface"""
331         return (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.remote_mac) /
332                 ARP(op=ARP.who_has, pdst=self.local_ip4,
333                     psrc=self.remote_ip4, hwsrc=self.remote_mac))
334
335     def create_ndp_req(self):
336         """Create NDP - NS applicable for this interface"""
337         nsma = in6_getnsma(inet_pton(socket.AF_INET6, self.local_ip6))
338         d = inet_ntop(socket.AF_INET6, nsma)
339
340         return (Ether(dst=in6_getnsmac(nsma)) /
341                 IPv6(dst=d, src=self.remote_ip6) /
342                 ICMPv6ND_NS(tgt=self.local_ip6) /
343                 ICMPv6NDOptSrcLLAddr(lladdr=self.remote_mac))
344
345     def resolve_arp(self, pg_interface=None):
346         """Resolve ARP using provided packet-generator interface
347
348         :param pg_interface: interface used to resolve, if None then this
349             interface is used
350
351         """
352         if pg_interface is None:
353             pg_interface = self
354         self.test.logger.info("Sending ARP request for %s on port %s" %
355                               (self.local_ip4, pg_interface.name))
356         arp_req = self.create_arp_req()
357         pg_interface.add_stream(arp_req)
358         pg_interface.enable_capture()
359         self.test.pg_start()
360         self.test.logger.info(self.test.vapi.cli("show trace"))
361         try:
362             captured_packet = pg_interface.wait_for_packet(1)
363         except:
364             self.test.logger.info("No ARP received on port %s" %
365                                   pg_interface.name)
366             return
367         arp_reply = captured_packet.copy()  # keep original for exception
368         # Make Dot1AD packet content recognizable to scapy
369         if arp_reply.type == 0x88a8:
370             arp_reply.type = 0x8100
371             arp_reply = Ether(str(arp_reply))
372         try:
373             if arp_reply[ARP].op == ARP.is_at:
374                 self.test.logger.info("VPP %s MAC address is %s " %
375                                       (self.name, arp_reply[ARP].hwsrc))
376                 self._local_mac = arp_reply[ARP].hwsrc
377             else:
378                 self.test.logger.info("No ARP received on port %s" %
379                                       pg_interface.name)
380         except:
381             self.test.logger.error(
382                 ppp("Unexpected response to ARP request:", captured_packet))
383             raise
384
385     def resolve_ndp(self, pg_interface=None, timeout=1):
386         """Resolve NDP using provided packet-generator interface
387
388         :param pg_interface: interface used to resolve, if None then this
389             interface is used
390         :param timeout: how long to wait for response before giving up
391
392         """
393         if pg_interface is None:
394             pg_interface = self
395         self.test.logger.info("Sending NDP request for %s on port %s" %
396                               (self.local_ip6, pg_interface.name))
397         ndp_req = self.create_ndp_req()
398         pg_interface.add_stream(ndp_req)
399         pg_interface.enable_capture()
400         self.test.pg_start()
401         now = time.time()
402         deadline = now + timeout
403         # Enabling IPv6 on an interface can generate more than the
404         # ND reply we are looking for (namely MLD). So loop through
405         # the replies to look for want we want.
406         while now < deadline:
407             try:
408                 captured_packet = pg_interface.wait_for_packet(
409                     deadline - now, filter_out_fn=None)
410             except:
411                 self.test.logger.error(
412                     "Timeout while waiting for NDP response")
413                 raise
414             ndp_reply = captured_packet.copy()  # keep original for exception
415             # Make Dot1AD packet content recognizable to scapy
416             if ndp_reply.type == 0x88a8:
417                 ndp_reply.type = 0x8100
418                 ndp_reply = Ether(str(ndp_reply))
419             try:
420                 ndp_na = ndp_reply[ICMPv6ND_NA]
421                 opt = ndp_na[ICMPv6NDOptDstLLAddr]
422                 self.test.logger.info("VPP %s MAC address is %s " %
423                                       (self.name, opt.lladdr))
424                 self._local_mac = opt.lladdr
425                 self.test.logger.debug(self.test.vapi.cli("show trace"))
426                 # we now have the MAC we've been after
427                 return
428             except:
429                 self.test.logger.info(
430                     ppp("Unexpected response to NDP request:",
431                         captured_packet))
432             now = time.time()
433
434         self.test.logger.debug(self.test.vapi.cli("show trace"))
435         raise Exception("Timeout while waiting for NDP response")