
Moved downstream tests with some changes - Changed setup() to work properly even on environments where routers can be scheduled on compute nodes. - Adjusted paths and references to fit the whitebox plugin - Adjusted uuids to a new unique ones - Added skip for a single-node environment - Added necessary config options and base functions Also: - Extended tcpdump capture code to support toolbox on coreos. - Added external bridge and tcpdump capture interface name discovering to support environments where different nodes have different interface names. Note: some of tests still need to be adapted for podified environment so for now they will be skipped. Change-Id: I1497862f35ac3c8182a668db87bc608193c6727f
155 lines
5.8 KiB
Python
155 lines
5.8 KiB
Python
# 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
|
|
|
|
WB_CONF = config.CONF.whitebox_neutron_plugin_options
|
|
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 = WB_CONF.capture_timeout
|
|
result = self.client.exec_command(
|
|
'which toolbox || echo missing')
|
|
if 'missing' in result:
|
|
cmd_prefix = "sudo timeout {}"
|
|
self.path_prefix = ''
|
|
else:
|
|
# (rsafrono) on coreos ocp nodes tcpdump is executed via
|
|
# toolbox. the toolbox can ask for update, therefore we need
|
|
# the 'yes no' to skip updating since it can take some time
|
|
cmd_prefix = "yes no | timeout {} toolbox"
|
|
# the toolbox runs in a container.
|
|
# host file system is mounted there to /host
|
|
self.path_prefix = '/host'
|
|
self.cmd_prefix = cmd_prefix.format(self.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(
|
|
'mktemp -u').rstrip()
|
|
cmd = '{} tcpdump -s0 -Uni {} {} -w {}{}'.format(
|
|
self.cmd_prefix, interface, self.filter_str,
|
|
self.path_prefix, 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(
|
|
'{} rm -f '.format(self.cmd_prefix) + ' '.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 = '{} tcpdump -r {} | wc -l'
|
|
for cap_file in self.capture_files:
|
|
if 0 < int(self.client.exec_command(
|
|
print_pcap_file_cmd.format(
|
|
self.cmd_prefix, 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(
|
|
'mktemp -u').rstrip()
|
|
n_retries = 5
|
|
for i in range(n_retries):
|
|
try:
|
|
self.client.exec_command(
|
|
'{} tcpslice -w {} {}'.format(
|
|
self.cmd_prefix, 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('cat ' + merged_cap_file)
|
|
self.addCleanup(ssh_channel.close)
|
|
return ssh_channel.makefile()
|