X-Git-Url: https://gerrit.fd.io/r/gitweb?a=blobdiff_plain;f=test%2Fvpp_pg_interface.py;h=cdb91ed1e417ea74820a99544d2f56785917eb70;hb=4941afb4f96a20df7dc8b6688f7921a3d713b77d;hp=c4b2e0a96db27d072cc188eb22799c0a44040061;hpb=2699fe2ba8fcad06ce04715049e9a55587f02f1f;p=vpp.git diff --git a/test/vpp_pg_interface.py b/test/vpp_pg_interface.py old mode 100755 new mode 100644 index c4b2e0a96db..cdb91ed1e41 --- a/test/vpp_pg_interface.py +++ b/test/vpp_pg_interface.py @@ -1,30 +1,42 @@ import os +import shutil import socket from socket import inet_pton, inet_ntop import struct import time from traceback import format_exc, format_stack +from sh import tshark +from pathlib import Path -import scapy.compat +from config import config from scapy.utils import wrpcap, rdpcap, PcapReader from scapy.plist import PacketList from vpp_interface import VppInterface +from vpp_papi import VppEnum from scapy.layers.l2 import Ether, ARP -from scapy.layers.inet6 import IPv6, ICMPv6ND_NS, ICMPv6ND_NA,\ - ICMPv6NDOptSrcLLAddr, ICMPv6NDOptDstLLAddr, ICMPv6ND_RA, RouterAlert, \ - IPv6ExtHdrHopByHop -from util import ppp, ppc +from scapy.layers.inet6 import ( + IPv6, + ICMPv6ND_NS, + ICMPv6ND_NA, + ICMPv6NDOptSrcLLAddr, + ICMPv6NDOptDstLLAddr, + ICMPv6ND_RA, + RouterAlert, + IPv6ExtHdrHopByHop, +) +from util import ppp, ppc, UnexpectedPacketError from scapy.utils6 import in6_getnsma, in6_getnsmac, in6_ismaddr class CaptureTimeoutError(Exception): - """ Exception raised if capture or packet doesn't appear within timeout """ + """Exception raised if capture or packet doesn't appear within timeout""" + pass def is_ipv6_misc(p): - """ Is packet one of uninteresting IPv6 broadcasts? """ + """Is packet one of uninteresting IPv6 broadcasts?""" if p.haslayer(ICMPv6ND_RA): if in6_ismaddr(p[IPv6].dst): return True @@ -69,29 +81,35 @@ class VppPGInterface(VppInterface): """pcap file path - captured packets""" return self._out_path - @property - def in_path(self): - """ pcap file path - injected packets""" - return self._in_path + def get_in_path(self, worker): + """pcap file path - injected packets""" + if worker is not None: + return "%s/pg%u_wrk%u_in.pcap" % (self.test.tempdir, self.pg_index, worker) + return "%s/pg%u_in.pcap" % (self.test.tempdir, self.pg_index) @property def capture_cli(self): """CLI string to start capture on this interface""" return self._capture_cli - @property - def cap_name(self): - """capture name for this interface""" + def get_cap_name(self, worker=None): + """return capture name for this interface and given worker""" + if worker is not None: + return self._cap_name + "-worker%d" % worker return self._cap_name - @property - def input_cli(self): - """CLI string to load the injected packets""" - if self._nb_replays is not None: - return "%s limit %d" % (self._input_cli, self._nb_replays) - if self._worker is not None: - return "%s worker %d" % (self._input_cli, self._worker) - return self._input_cli + def get_input_cli(self, nb_replays=None, worker=None): + """return CLI string to load the injected packets""" + input_cli = "packet-generator new pcap %s source pg%u name %s" % ( + self.get_in_path(worker), + self.pg_index, + self.get_cap_name(worker), + ) + if nb_replays is not None: + return "%s limit %d" % (input_cli, nb_replays) + if worker is not None: + return "%s worker %d" % (input_cli, worker) + return input_cli @property def in_history_counter(self): @@ -107,11 +125,11 @@ class VppPGInterface(VppInterface): self._out_history_counter += 1 return v - def __init__(self, test, pg_index, gso, gso_size): - """ Create VPP packet-generator interface """ - super(VppPGInterface, self).__init__(test) + def __init__(self, test, pg_index, gso, gso_size, mode): + """Create VPP packet-generator interface""" + super().__init__(test) - r = test.vapi.pg_create_interface(pg_index, gso, gso_size) + r = test.vapi.pg_create_interface_v2(pg_index, gso, gso_size, mode) self.set_sw_if_index(r.sw_if_index) self._in_history_counter = 0 @@ -123,44 +141,68 @@ class VppPGInterface(VppInterface): self._coalesce_enabled = 0 self._out_file = "pg%u_out.pcap" % self.pg_index self._out_path = self.test.tempdir + "/" + self._out_file - self._in_file = "pg%u_in.pcap" % self.pg_index - self._in_path = self.test.tempdir + "/" + self._in_file self._capture_cli = "packet-generator capture pg%u pcap %s" % ( - self.pg_index, self.out_path) - self._cap_name = "pcap%u-sw_if_index-%s" % ( - self.pg_index, self.sw_if_index) - self._input_cli = \ - "packet-generator new pcap %s source pg%u name %s" % ( - self.in_path, self.pg_index, self.cap_name) - self._nb_replays = None - - def _rename_previous_capture_file(self, path, counter, file): - # if a file from a previous capture exists, rename it. + self.pg_index, + self.out_path, + ) + self._cap_name = "pcap%u-sw_if_index-%s" % (self.pg_index, self.sw_if_index) + + def link_pcap_file(self, path, direction, counter): + if not config.keep_pcaps: + return + filename = os.path.basename(path) + test_name = ( + self.test_name + if hasattr(self, "test_name") + else f"suite{self.test.__name__}" + ) + name = f"{self.test.tempdir}/{test_name}.[timestamp:{time.time():.8f}].{self.name}-{direction}-{counter:04}.{filename}" + if os.path.isfile(name): + self.test.logger.debug( + f"Skipping hard link creation: {name} already exists!" + ) + return try: if os.path.isfile(path): - name = "%s/history.[timestamp:%f].[%s-counter:%04d].%s" % \ - (self.test.tempdir, - time.time(), - self.name, - counter, - file) - self.test.logger.debug("Renaming %s->%s" % - (path, name)) - os.rename(path, name) + self.test.logger.debug(f"Creating hard link {path}->{name}") + os.link(path, name) except OSError: - self.test.logger.debug("OSError: Could not rename %s %s" % - (path, file)) + self.test.logger.debug( + f"OSError: Could not create hard link {path}->{name}" + ) + + def remove_old_pcap_file(self, path): + self.wait_for_pg_stop() + self.test.unlink_testcase_file(self.test, Path(path)) + return + + def decode_pcap_files(self, pcap_dir, filename_prefix): + # Generate tshark packet trace of testcase pcap files + pg_decode = f"{pcap_dir}/pcap-decode-{filename_prefix}.txt" + if os.path.isfile(pg_decode): + self.test.logger.debug( + f"The pg streams decode file already exists: {pg_decode}" + ) + return + self.test.logger.debug( + f"Generating testcase pg streams decode file: {pg_decode}" + ) + ts_opts = "-Vr" + for p in sorted(Path(pcap_dir).glob(f"{filename_prefix}*.pcap")): + self.test.logger.debug(f"Decoding {p}") + with open(f"{pg_decode}", "a", buffering=1) as f: + print(f"tshark {ts_opts} {p}", file=f) + tshark(ts_opts, f"{p}", _out=f) + print("", file=f) def enable_capture(self): - """ Enable capture on this packet-generator interface - of at most n packets. - If n < 0, this is no limit + """Enable capture on this packet-generator interface + of at most n packets. + If n < 0, this is no limit """ # disable the capture to flush the capture self.disable_capture() - self._rename_previous_capture_file(self.out_path, - self.out_history_counter, - self._out_file) + self.remove_old_pcap_file(self.out_path) # FIXME this should be an API, but no such exists atm self.test.vapi.cli(self.capture_cli) self._pcap_reader = None @@ -169,16 +211,14 @@ class VppPGInterface(VppInterface): self.test.vapi.cli("%s disable" % self.capture_cli) def coalesce_enable(self): - """ Enable packet coalesce on this packet-generator interface""" + """Enable packet coalesce on this packet-generator interface""" self._coalesce_enabled = 1 - self.test.vapi.pg_interface_enable_disable_coalesce(self.sw_if_index, - 1) + self.test.vapi.pg_interface_enable_disable_coalesce(self.sw_if_index, 1) def coalesce_disable(self): - """ Disable packet coalesce on this packet-generator interface""" + """Disable packet coalesce on this packet-generator interface""" self._coalesce_enabled = 0 - self.test.vapi.pg_interface_enable_disable_coalesce(self.sw_if_index, - 0) + self.test.vapi.pg_interface_enable_disable_coalesce(self.sw_if_index, 0) def add_stream(self, pkts, nb_replays=None, worker=None): """ @@ -187,42 +227,41 @@ class VppPGInterface(VppInterface): :param pkts: iterable packets """ - self._worker = worker - self._nb_replays = nb_replays - self._rename_previous_capture_file(self.in_path, - self.in_history_counter, - self._in_file) - wrpcap(self.in_path, pkts) - self.test.register_capture(self.cap_name) + in_pcap = self.get_in_path(worker) + if os.path.isfile(in_pcap): + self.remove_old_pcap_file(in_pcap) + wrpcap(in_pcap, pkts) + self.test.register_pcap(self, worker) # FIXME this should be an API, but no such exists atm - self.test.vapi.cli(self.input_cli) + self.test.vapi.cli(self.get_input_cli(nb_replays, worker)) + self.link_pcap_file(self.get_in_path(worker), "inp", self.in_history_counter) def generate_debug_aid(self, kind): - """ Create a hardlink to the out file with a counter and a file + """Create a hardlink to the out file with a counter and a file containing stack trace to ease debugging in case of multiple capture - files present. """ - self.test.logger.debug("Generating debug aid for %s on %s" % - (kind, self._name)) - link_path, stack_path = ["%s/debug_%s_%s_%s.%s" % - (self.test.tempdir, self._name, - self._out_assert_counter, kind, suffix) - for suffix in ["pcap", "stack"] - ] + files present.""" + self.test.logger.debug("Generating debug aid for %s on %s" % (kind, self._name)) + link_path, stack_path = [ + "%s/debug_%s_%s_%s.%s" + % (self.test.tempdir, self._name, self._out_assert_counter, kind, suffix) + for suffix in ["pcap", "stack"] + ] os.link(self.out_path, link_path) with open(stack_path, "w") as f: f.writelines(format_stack()) self._out_assert_counter += 1 def _get_capture(self, timeout, filter_out_fn=is_ipv6_misc): - """ Helper method to get capture and filter it """ + """Helper method to get capture and filter it""" try: if not self.wait_for_capture_file(timeout): return None output = rdpcap(self.out_path) - self.test.logger.debug("Capture has %s packets" % len(output.res)) + self.test.logger.debug(f"Capture has {len(output.res)} packets") except: - self.test.logger.debug("Exception in scapy.rdpcap (%s): %s" % - (self.out_path, format_exc())) + self.test.logger.debug( + "Exception in scapy.rdpcap (%s): %s" % (self.out_path, format_exc()) + ) return None before = len(output.res) if filter_out_fn: @@ -230,13 +269,15 @@ class VppPGInterface(VppInterface): removed = before - len(output.res) if removed: self.test.logger.debug( - "Filtered out %s packets from capture (returning %s)" % - (removed, len(output.res))) + "Filtered out %s packets from capture (returning %s)" + % (removed, len(output.res)) + ) return output - def get_capture(self, expected_count=None, remark=None, timeout=1, - filter_out_fn=is_ipv6_misc): - """ Get captured packets + def get_capture( + self, expected_count=None, remark=None, timeout=1, filter_out_fn=is_ipv6_misc + ): + """Get captured packets :param expected_count: expected number of packets to capture, if None, then self.test.packet_count_for_dst_pg_idx is @@ -252,15 +293,16 @@ class VppPGInterface(VppInterface): name = self.name if remark is None else "%s (%s)" % (self.name, remark) based_on = "based on provided argument" if expected_count is None: - expected_count = \ - self.test.get_packet_count_for_if_idx(self.sw_if_index) + expected_count = self.test.get_packet_count_for_if_idx(self.sw_if_index) based_on = "based on stored packet_infos" if expected_count == 0: raise Exception( - "Internal error, expected packet count for %s is 0!" % - name) - self.test.logger.debug("Expecting to capture %s (%s) packets on %s" % ( - expected_count, based_on, name)) + "Internal error, expected packet count for %s is 0!" % name + ) + self.test.logger.debug( + "Expecting to capture %s (%s) packets on %s" + % (expected_count, based_on, name) + ) while remaining_time > 0: before = time.time() capture = self._get_capture(remaining_time, filter_out_fn) @@ -271,49 +313,52 @@ class VppPGInterface(VppInterface): return capture elif len(capture.res) > expected_count: self.test.logger.error( - ppc("Unexpected packets captured:", capture)) + ppc( + f"Unexpected packets captured, got {len(capture.res)}, expected {expected_count}:", + capture, + ) + ) break else: - self.test.logger.debug("Partial capture containing %s " - "packets doesn't match expected " - "count %s (yet?)" % - (len(capture.res), expected_count)) + self.test.logger.debug( + "Partial capture containing %s " + "packets doesn't match expected " + "count %s (yet?)" % (len(capture.res), expected_count) + ) elif expected_count == 0: # bingo, got None as we expected - return empty capture return PacketList() remaining_time -= elapsed_time if capture: self.generate_debug_aid("count-mismatch") - raise Exception("Captured packets mismatch, captured %s packets, " - "expected %s packets on %s" % - (len(capture.res), expected_count, name)) + if len(capture) > 0 and 0 == expected_count: + rem = f"\n{remark}" if remark else "" + raise UnexpectedPacketError( + capture[0], + f"\n({len(capture)} packets captured in total){rem} on {name}", + ) + msg = f"Captured packets mismatch, captured {len(capture.res)} packets, expected {expected_count} packets on {name}:" + raise Exception(f"{ppc(msg, capture)}") else: - raise Exception("No packets captured on %s" % name) + if 0 == expected_count: + return + raise Exception(f"No packets captured on {name} (timeout = {timeout}s)") - def assert_nothing_captured(self, remark=None, filter_out_fn=is_ipv6_misc): - """ Assert that nothing unfiltered was captured on interface + def assert_nothing_captured( + self, timeout=1, remark=None, filter_out_fn=is_ipv6_misc + ): + """Assert that nothing unfiltered was captured on interface :param remark: remark printed into debug logs :param filter_out_fn: filter applied to each packet, packets for which the filter returns True are removed from capture """ - if os.path.isfile(self.out_path): - try: - capture = self.get_capture( - 0, remark=remark, filter_out_fn=filter_out_fn) - if not capture or len(capture.res) == 0: - # junk filtered out, we're good - return - except: - pass - self.generate_debug_aid("empty-assert") - if remark: - raise AssertionError( - "Non-empty capture file present for interface %s (%s)" % - (self.name, remark)) - else: - raise AssertionError("Capture file present for interface %s" % - self.name) + capture = self.get_capture( + 0, timeout=timeout, remark=remark, filter_out_fn=filter_out_fn + ) + if not capture or len(capture.res) == 0: + # junk filtered out, we're good + return def wait_for_pg_stop(self): # wait till packet-generator is stopped @@ -323,7 +368,7 @@ class VppPGInterface(VppInterface): # # also have a 5-minute timeout just in case things go terribly wrong... deadline = time.time() + 300 - while self.test.vapi.cli('show packet-generator').find("Yes") != -1: + while self.test.vapi.cli("show packet-generator").find("Yes") != -1: self._test.sleep(0.01) # yield if time.time() > deadline: self.test.logger.debug("Timeout waiting for pg to stop") @@ -340,22 +385,26 @@ class VppPGInterface(VppInterface): self.wait_for_pg_stop() deadline = time.time() + timeout if not os.path.isfile(self.out_path): - self.test.logger.debug("Waiting for capture file %s to appear, " - "timeout is %ss" % (self.out_path, timeout)) + self.test.logger.debug( + f"Waiting for capture file {self.out_path} to appear, timeout is {timeout}s\n" + f"{' '.join(format_stack(limit=10))}" + ) else: - self.test.logger.debug("Capture file %s already exists" % - self.out_path) + self.test.logger.debug("Capture file %s already exists" % self.out_path) + self.link_pcap_file(self.out_path, "out", self.out_history_counter) return True while time.time() < deadline: if os.path.isfile(self.out_path): break self._test.sleep(0) # yield if os.path.isfile(self.out_path): - self.test.logger.debug("Capture file appeared after %fs" % - (time.time() - (deadline - timeout))) + self.test.logger.debug( + "Capture file appeared after %fs" % (time.time() - (deadline - timeout)) + ) else: self.test.logger.debug("Timeout - capture file still nowhere") return False + self.link_pcap_file(self.out_path, "out", self.out_history_counter) return True def verify_enough_packet_data_in_pcap(self): @@ -375,7 +424,8 @@ class VppPGInterface(VppInterface): if len(hdr) == packet_header_size: # parse the capture length - caplen sec, usec, caplen, wirelen = struct.unpack( - self._pcap_reader.endian + "IIII", hdr) + self._pcap_reader.endian + "IIII", hdr + ) self._pcap_reader.f.seek(0, 2) # seek to end of file end_pos = self._pcap_reader.f.tell() # get position at end if end_pos >= orig_pos + len(hdr) + caplen: @@ -395,19 +445,22 @@ class VppPGInterface(VppInterface): deadline = time.time() + timeout if self._pcap_reader is None: if not self.wait_for_capture_file(timeout): - raise CaptureTimeoutError("Capture file %s did not appear " - "within timeout" % self.out_path) + raise CaptureTimeoutError( + "Capture file %s did not appear within timeout" % self.out_path + ) while time.time() < deadline: try: self._pcap_reader = PcapReader(self.out_path) break except: self.test.logger.debug( - "Exception in scapy.PcapReader(%s): %s" % - (self.out_path, format_exc())) + "Exception in scapy.PcapReader(%s): %s" + % (self.out_path, format_exc()) + ) if not self._pcap_reader: - raise CaptureTimeoutError("Capture file %s did not appear within " - "timeout" % self.out_path) + raise CaptureTimeoutError( + "Capture file %s did not appear within timeout" % self.out_path + ) poll = False if timeout > 0: @@ -424,23 +477,28 @@ class VppPGInterface(VppInterface): if p is not None: if filter_out_fn is not None and filter_out_fn(p): self.test.logger.debug( - "Packet received after %ss was filtered out" % - (time.time() - (deadline - timeout))) + "Packet received after %ss was filtered out" + % (time.time() - (deadline - timeout)) + ) else: self.test.logger.debug( - "Packet received after %fs" % - (time.time() - (deadline - timeout))) + "Packet received after %fs" + % (time.time() - (deadline - timeout)) + ) return p self._test.sleep(0) # yield poll = False - self.test.logger.debug("Timeout - no packets received") - raise CaptureTimeoutError("Packet didn't arrive within timeout") + self.test.logger.debug(f"Timeout ({timeout}) - no packets received") + raise CaptureTimeoutError(f"Packet didn't arrive within timeout ({timeout})") def create_arp_req(self): """Create ARP request applicable for this interface""" - return (Ether(dst="ff:ff:ff:ff:ff:ff", src=self.remote_mac) / - ARP(op=ARP.who_has, pdst=self.local_ip4, - psrc=self.remote_ip4, hwsrc=self.remote_mac)) + return Ether(dst="ff:ff:ff:ff:ff:ff", src=self.remote_mac) / ARP( + op=ARP.who_has, + pdst=self.local_ip4, + psrc=self.remote_ip4, + hwsrc=self.remote_mac, + ) def create_ndp_req(self, addr=None): """Create NDP - NS applicable for this interface""" @@ -449,10 +507,12 @@ class VppPGInterface(VppInterface): nsma = in6_getnsma(inet_pton(socket.AF_INET6, addr)) d = inet_ntop(socket.AF_INET6, nsma) - return (Ether(dst=in6_getnsmac(nsma)) / - IPv6(dst=d, src=self.remote_ip6) / - ICMPv6ND_NS(tgt=addr) / - ICMPv6NDOptSrcLLAddr(lladdr=self.remote_mac)) + return ( + Ether(dst=in6_getnsmac(nsma)) + / IPv6(dst=d, src=self.remote_ip6) + / ICMPv6ND_NS(tgt=addr) + / ICMPv6NDOptSrcLLAddr(lladdr=self.remote_mac) + ) def resolve_arp(self, pg_interface=None): """Resolve ARP using provided packet-generator interface @@ -463,8 +523,10 @@ class VppPGInterface(VppInterface): """ if pg_interface is None: pg_interface = self - self.test.logger.info("Sending ARP request for %s on port %s" % - (self.local_ip4, pg_interface.name)) + self.test.logger.info( + "Sending ARP request for %s on port %s" + % (self.local_ip4, pg_interface.name) + ) arp_req = self.create_arp_req() pg_interface.add_stream(arp_req) pg_interface.enable_capture() @@ -473,21 +535,21 @@ class VppPGInterface(VppInterface): try: captured_packet = pg_interface.wait_for_packet(1) except: - self.test.logger.info("No ARP received on port %s" % - pg_interface.name) + self.test.logger.info("No ARP received on port %s" % pg_interface.name) return arp_reply = captured_packet.copy() # keep original for exception try: if arp_reply[ARP].op == ARP.is_at: - self.test.logger.info("VPP %s MAC address is %s " % - (self.name, arp_reply[ARP].hwsrc)) + self.test.logger.info( + "VPP %s MAC address is %s " % (self.name, arp_reply[ARP].hwsrc) + ) self._local_mac = arp_reply[ARP].hwsrc else: - self.test.logger.info("No ARP received on port %s" % - pg_interface.name) + self.test.logger.info("No ARP received on port %s" % pg_interface.name) except: self.test.logger.error( - ppp("Unexpected response to ARP request:", captured_packet)) + ppp("Unexpected response to ARP request:", captured_packet) + ) raise def resolve_ndp(self, pg_interface=None, timeout=1, link_layer=False): @@ -503,8 +565,9 @@ class VppPGInterface(VppInterface): if pg_interface is None: pg_interface = self addr = self.local_ip6_ll if link_layer else self.local_ip6 - self.test.logger.info("Sending NDP request for %s on port %s" % - (addr, pg_interface.name)) + self.test.logger.info( + "Sending NDP request for %s on port %s" % (addr, pg_interface.name) + ) ndp_req = self.create_ndp_req(addr) pg_interface.add_stream(ndp_req) pg_interface.enable_capture() @@ -517,25 +580,26 @@ class VppPGInterface(VppInterface): while now < deadline: try: captured_packet = pg_interface.wait_for_packet( - deadline - now, filter_out_fn=None) + deadline - now, filter_out_fn=None + ) except: - self.test.logger.error( - "Timeout while waiting for NDP response") + self.test.logger.error("Timeout while waiting for NDP response") raise ndp_reply = captured_packet.copy() # keep original for exception try: ndp_na = ndp_reply[ICMPv6ND_NA] opt = ndp_na[ICMPv6NDOptDstLLAddr] - self.test.logger.info("VPP %s MAC address is %s " % - (self.name, opt.lladdr)) + self.test.logger.info( + "VPP %s MAC address is %s " % (self.name, opt.lladdr) + ) self._local_mac = opt.lladdr self.test.logger.debug(self.test.vapi.cli("show trace")) # we now have the MAC we've been after return except: self.test.logger.info( - ppp("Unexpected response to NDP request:", - captured_packet)) + ppp("Unexpected response to NDP request:", captured_packet) + ) now = time.time() self.test.logger.debug(self.test.vapi.cli("show trace"))