Add test_dvr_ovn.py

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
This commit is contained in:
Roman Safronov 2024-03-14 18:23:56 +02:00
parent ae81daf2a7
commit 69169975c0
5 changed files with 1111 additions and 31 deletions

View File

@ -23,7 +23,7 @@ from tempest.lib import exceptions
from whitebox_neutron_tempest_plugin.common import utils
CONF = config.CONF
WB_CONF = config.CONF.whitebox_neutron_plugin_options
LOG = log.getLogger(__name__)
@ -35,7 +35,21 @@ class TcpdumpCapture(fixtures.Fixture):
self.client = client
self.interfaces = [ifc.strip() for ifc in interfaces.split(',')]
self.filter_str = filter_str
self.timeout = CONF.whitebox_neutron_plugin_options.capture_timeout
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()
@ -51,10 +65,11 @@ class TcpdumpCapture(fixtures.Fixture):
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)
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)
@ -71,7 +86,8 @@ class TcpdumpCapture(fixtures.Fixture):
if self.capture_files:
if utils.host_responds_to_ping(self.client.host):
self.client.exec_command(
'sudo rm -f ' + ' '.join(self.capture_files))
'{} rm -f '.format(self.cmd_prefix) + ' '.join(
self.capture_files))
self.capture_files = None
def is_empty(self):
@ -101,10 +117,11 @@ class TcpdumpCapture(fixtures.Fixture):
merged_cap_file = self.capture_files[0]
else:
cap_file_candidates = []
print_pcap_file_cmd = 'sudo tcpdump -r {} | wc -l'
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(cap_file)).rstrip()):
print_pcap_file_cmd.format(
self.cmd_prefix, cap_file)).rstrip()):
# cap files that are not empty
cap_file_candidates.append(cap_file)
@ -115,13 +132,13 @@ class TcpdumpCapture(fixtures.Fixture):
merged_cap_file = cap_file_candidates[0]
else:
merged_cap_file = self.client.exec_command(
'sudo mktemp').rstrip()
'mktemp -u').rstrip()
n_retries = 5
for i in range(n_retries):
try:
self.client.exec_command(
'sudo tcpslice -w {} {}'.format(
merged_cap_file,
'{} tcpslice -w {} {}'.format(
self.cmd_prefix, merged_cap_file,
' '.join(cap_file_candidates)))
except exceptions.SSHExecCommandFailed as exc:
if i == (n_retries - 1):
@ -132,6 +149,6 @@ class TcpdumpCapture(fixtures.Fixture):
break
ssh_channel = self.client.open_session()
ssh_channel.exec_command('sudo cat ' + merged_cap_file)
ssh_channel.exec_command('cat ' + merged_cap_file)
self.addCleanup(ssh_channel.close)
return ssh_channel.makefile()

View File

@ -96,6 +96,9 @@ WhiteboxNeutronPluginOptions = [
default=False,
help='Specifies whether the OSP setup under test has been '
'configured with BGP functionality or not'),
cfg.StrOpt('bgp_agent_config',
default='/etc/ovn-bgp-agent/bgp-agent.conf',
help='Path to ovn-bgp-agent config file'),
cfg.IntOpt('sriov_pfs_per_host',
default=1,
help='Number of available PF (Physical Function) ports per'
@ -116,12 +119,19 @@ WhiteboxNeutronPluginOptions = [
cfg.StrOpt('ml2_plugin_config',
default='/etc/neutron/plugins/ml2/ml2_conf.ini',
help='Path to ml2 plugin config.'),
cfg.StrOpt('ext_bridge',
default='br-ex',
help="OpenvSwitch bridge dedicated for external network."),
cfg.StrOpt('node_integration_bridge',
default='br-int',
help="OpenvSwitch bridge dedicated for OVN's use."),
cfg.StrOpt('ext_bridge',
default='{"default": "br-ex", "alt": "ospbr"}',
help="Bridge dedicated for external network. Dict with values "
"for default node and alternative node (if exist)."),
cfg.StrOpt('node_ext_interface',
default=None,
help='Physical interface of a node that is connected to the'
'external network. In case the value is set to None the '
'interface name will be discovered from a list of '
'interfaces under bridge connected to external network'),
cfg.IntOpt('ovn_max_controller_gw_ports_per_router',
default=1,
help='The number of network nodes used '

View File

@ -14,6 +14,7 @@
# under the License.
import base64
from functools import partial
import json
from multiprocessing import Process
import os
import random
@ -65,6 +66,7 @@ class BaseTempestWhiteboxTestCase(base.BaseTempestTestCase):
sriov_agents = [
agent for agent in agents if 'sriov' in agent['binary']]
cls.has_sriov_support = True if sriov_agents else False
cls.ext_bridge = json.loads(WB_CONF.ext_bridge)
# deployer tool dependent variables
if WB_CONF.openstack_type == 'devstack':
cls.master_node_client = cls.get_node_client('localhost')
@ -113,6 +115,16 @@ class BaseTempestWhiteboxTestCase(base.BaseTempestTestCase):
subnet['ip_version'] == constants.IP_VERSION_4):
return subnet['gateway_ip']
def get_external_bridge(self, client):
commands = [
"sudo ovs-vsctl list-br",
"sudo ip -o link show type bridge | cut -d ' ' -f 2 | tr -d ':'"]
for cmd in commands:
result = client.exec_command(cmd).strip().split()
if self.ext_bridge['default'] in result:
return self.ext_bridge['default']
return self.ext_bridge['alt']
@staticmethod
def get_node_client(
host, username=WB_CONF.overcloud_ssh_user, pkey=None,
@ -123,16 +135,6 @@ class BaseTempestWhiteboxTestCase(base.BaseTempestTestCase):
return ssh.Client(host=host, username=username,
key_filename=key_filename)
def find_different_compute_host(self, exclude_hosts):
for node in self.nodes:
if not node['is_compute']:
continue
if node['is_compute'] and not node['name'] in exclude_hosts:
return node['name']
raise self.skipException(
"Not able to find a different compute than: {}".format(
exclude_hosts))
def get_local_ssh_client(self, network):
return ssh.Client(
host=self._get_local_ip_from_network(
@ -152,6 +154,16 @@ class BaseTempestWhiteboxTestCase(base.BaseTempestTestCase):
if node['name'] == node_name:
return node['client']
def find_different_compute_host(self, exclude_hosts):
for node in self.nodes:
if not node['is_compute']:
continue
if node['is_compute'] and not node['name'] in exclude_hosts:
return node['name']
raise self.skipException(
"Not able to find a different compute than: {}".format(
exclude_hosts))
@staticmethod
def _get_local_ip_from_network(network):
host_ip_addresses = [ifaddresses(iface)[AF_INET][0]['addr']
@ -342,7 +354,7 @@ class BaseTempestWhiteboxTestCase(base.BaseTempestTestCase):
@classmethod
def check_service_setting(
cls, host, service='neutron', config_files=None,
section='DEFAULT', param='', value='True',
section='DEFAULT', param='', value='true',
msg='Required config value is missing', skip_if_fails=True):
"""Check if a service on a node has a setting with a value in config
@ -354,7 +366,7 @@ class BaseTempestWhiteboxTestCase(base.BaseTempestTestCase):
2 config files with same sections.
:param section(str): Section in the config file.
:param param(str): Parameter in section to check.
:param value(str): Expected value.
:param value(str): Expected value, case insensitive.
:param msg(str): Message to print in case of expected value not found
:param skip_if_fails(bool): skip if the check fails - if it fails and
skip_if_fails is False, return False.
@ -374,7 +386,7 @@ class BaseTempestWhiteboxTestCase(base.BaseTempestTestCase):
LOG.debug("Command = '{}'".format(cmd))
result = host['client'].exec_command(cmd)
LOG.debug("Result = '{}'".format(result))
if value in result:
if value.lower() in result.lower():
return True
else:
continue
@ -886,9 +898,31 @@ class TrafficFlowTest(BaseTempestWhiteboxTestCase):
cls.discover_nodes()
def _start_captures(self, interface, filters):
def get_interface(client):
# (rsafrono) discover interfaces, useful when capturing on
# nodes and different types of nodes have different interfaces,
# e.g. on podified environment ocp and compute have different
# names of interfaces connected to external network
bridge = self.get_external_bridge(client)
filters = [r"| grep 'eth\|enp\|ens' | grep -v veth ",
"| cut -f2 -d ' ' | tr -d ':'"]
commands = [
"sudo ovs-vsctl list-ports " + bridge + filters[0],
"sudo ip -o link show master " + bridge + filters[0] +
filters[1]]
for cmd in commands:
interfaces = client.exec_command(
cmd + " || true").strip().split()
if interfaces:
return ','.join(interfaces)
for node in self.nodes:
if interface:
node_interface = interface
else:
node_interface = get_interface(node['client'])
node['capture'] = capture.TcpdumpCapture(
node['client'], interface, filters)
node['client'], node_interface, filters)
self.useFixture(node['capture'])
time.sleep(2)

File diff suppressed because it is too large Load Diff

View File

@ -330,7 +330,7 @@ class ProviderRoutedNetworkOVNConfigTest(ProviderRoutedNetworkBaseTest,
to its availability zone.
"""
ext_bridge = WB_CONF.ext_bridge
ext_bridge = self.ext_bridge['default']
for _, details in self.resources.items():
for host in details['hosts']:
mapping = self.get_host_ovn_bridge_mapping(host)