diff --git a/requirements.txt b/requirements.txt index 2543bed..a340af3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ python-subunit >= 0.0.18 testtools >= 0.9.30 dpkt >= 1.8.8 # BSD -netaddr +scapy netifaces pyroute2 >= 0.6.6 diff --git a/whitebox_neutron_tempest_plugin/common/tcpdump_capture.py b/whitebox_neutron_tempest_plugin/common/tcpdump_capture.py new file mode 100644 index 0000000..8361552 --- /dev/null +++ b/whitebox_neutron_tempest_plugin/common/tcpdump_capture.py @@ -0,0 +1,137 @@ +# Copyright 2019 Red Hat, Inc. +# All Rights Reserved. + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import time + +import fixtures +from oslo_log import log +from scapy.all import ICMP +from scapy.all import rdpcap +from tempest import config +from tempest.lib import exceptions + +from whitebox_neutron_tempest_plugin.common import utils + +CONF = config.CONF +LOG = log.getLogger(__name__) + + +class TcpdumpCapture(fixtures.Fixture): + capture_files = None + processes = None + + def __init__(self, client, interfaces, filter_str=''): + self.client = client + self.interfaces = [ifc.strip() for ifc in interfaces.split(',')] + self.filter_str = filter_str + self.timeout = CONF.whitebox_options.capture_timeout + + def _setUp(self): + self.start() + + def start(self): + if not self.capture_files: + # mktemp needs to be executed with sudo - otherwise the later + # tcpdump command (run with sudo) fails because the created temp + # file cannot be written + # This happens in RHEL9 because fs.protected_regular is enabled + self.capture_files = [] + self.processes = [] + + for interface in self.interfaces: + process = self.client.open_session() + capture_file = self.client.exec_command('sudo mktemp').rstrip() + cmd = 'sudo timeout {} tcpdump -s0 -Uni {} {} -w {}'.format( + self.timeout, interface, self.filter_str, + capture_file) + self.capture_files.append(capture_file) + LOG.debug('Executing command: {}'.format(cmd)) + process.exec_command(cmd) + self.processes.append(process) + self.addCleanup(self.cleanup) + + def stop(self): + for process in (self.processes or []): + process.close() + self.processes = None + + def cleanup(self): + self.stop() + if self.capture_files: + if utils.host_responds_to_ping(self.client.host): + self.client.exec_command( + 'sudo rm -f ' + ' '.join(self.capture_files)) + self.capture_files = None + + def is_empty(self): + try: + pcap = rdpcap(self._open_capture_file()) + except Exception as e: + LOG.debug('Error reading pcap file: ', str(e)) + return True + for record in pcap: + return False + return True + + def get_next_hop_mtu(self): + pcap = rdpcap(self._open_capture_file()) + for record in pcap: + if 'IP' in record and 'ICMP' in record: + icmp = record[ICMP] + # ICMP type 3 = Destionation Unreachable + if icmp.type == 3: + return repr(icmp.nexthopmtu) + return None + + def _open_capture_file(self): + if not self.capture_files: + raise ValueError('No capture files available') + elif len(self.capture_files) == 1: + merged_cap_file = self.capture_files[0] + else: + cap_file_candidates = [] + print_pcap_file_cmd = 'sudo tcpdump -r {} | wc -l' + for cap_file in self.capture_files: + if 0 < int(self.client.exec_command( + print_pcap_file_cmd.format(cap_file)).rstrip()): + # cap files that are not empty + cap_file_candidates.append(cap_file) + + if not cap_file_candidates: + # they are all empty + merged_cap_file = self.capture_files[0] + elif 1 == len(cap_file_candidates): + merged_cap_file = cap_file_candidates[0] + else: + merged_cap_file = self.client.exec_command( + 'sudo mktemp').rstrip() + n_retries = 5 + for i in range(n_retries): + try: + self.client.exec_command( + 'sudo tcpslice -w {} {}'.format( + merged_cap_file, + ' '.join(cap_file_candidates))) + except exceptions.SSHExecCommandFailed as exc: + if i == (n_retries - 1): + raise exc + LOG.warn('tcpslice command failed - retrying...') + time.sleep(5) + else: + break + + ssh_channel = self.client.open_session() + ssh_channel.exec_command('sudo cat ' + merged_cap_file) + self.addCleanup(ssh_channel.close) + return ssh_channel.makefile()