Merge "Add MTU tests"
This commit is contained in:
commit
ae81daf2a7
@ -38,6 +38,12 @@ customize_public_network_and_subnet(){
|
||||
# that only a single DHCP server is available on the external network.
|
||||
openstack network set --share $PUBLIC_NETWORK_NAME
|
||||
openstack subnet set --dhcp $PUBLIC_SUBNET_NAME
|
||||
|
||||
# Decrease external network MTU to allow tests from test_mtu.py to run
|
||||
# (rsafrono) tests from test_mtu.py can not be run due to
|
||||
# https://bugs.launchpad.net/neutron/+bug/2060828
|
||||
# The line should be uncommented once the issue is fixed
|
||||
# openstack network set --mtu 1330 $PUBLIC_NETWORK_NAME
|
||||
fi
|
||||
}
|
||||
|
||||
|
@ -119,6 +119,9 @@ WhiteboxNeutronPluginOptions = [
|
||||
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.IntOpt('ovn_max_controller_gw_ports_per_router',
|
||||
default=1,
|
||||
help='The number of network nodes used '
|
||||
|
369
whitebox_neutron_tempest_plugin/tests/scenario/test_mtu.py
Normal file
369
whitebox_neutron_tempest_plugin/tests/scenario/test_mtu.py
Normal file
@ -0,0 +1,369 @@
|
||||
# Copyright 2024 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
|
||||
|
||||
from neutron_lib import constants as lib_constants
|
||||
from neutron_tempest_plugin.common import ssh
|
||||
from neutron_tempest_plugin import config
|
||||
from oslo_log import log
|
||||
from tempest.lib import decorators
|
||||
|
||||
from whitebox_neutron_tempest_plugin.common import constants
|
||||
from whitebox_neutron_tempest_plugin.common import tcpdump_capture as capture
|
||||
from whitebox_neutron_tempest_plugin.common import utils
|
||||
from whitebox_neutron_tempest_plugin.tests.scenario import base
|
||||
|
||||
CONF = config.CONF
|
||||
WB_CONF = config.CONF.whitebox_neutron_plugin_options
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class GatewayMtuTest(base.TrafficFlowTest):
|
||||
credentials = ['primary', 'admin']
|
||||
|
||||
@classmethod
|
||||
def skip_checks(cls):
|
||||
super(GatewayMtuTest, cls).skip_checks()
|
||||
if not CONF.network.public_network_id:
|
||||
raise cls.skipException(
|
||||
"The public_network_id option must be specified. ")
|
||||
|
||||
@classmethod
|
||||
def resource_setup(cls):
|
||||
super(GatewayMtuTest, cls).resource_setup()
|
||||
if CONF.neutron_plugin_options.default_image_is_advanced:
|
||||
cls.flavor_ref = CONF.compute.flavor_ref
|
||||
cls.image_ref = CONF.compute.image_ref
|
||||
cls.username = CONF.validation.image_ssh_user
|
||||
else:
|
||||
cls.flavor_ref = \
|
||||
CONF.neutron_plugin_options.advanced_image_flavor_ref
|
||||
cls.image_ref = CONF.neutron_plugin_options.advanced_image_ref
|
||||
cls.username = CONF.neutron_plugin_options.advanced_image_ssh_user
|
||||
|
||||
if (not cls.flavor_ref) or (not cls.image_ref):
|
||||
raise cls.skipException(
|
||||
'No advanced image/flavor available for these tests')
|
||||
|
||||
cls.keypair = cls.create_keypair()
|
||||
cls.secgroup = cls.create_security_group()
|
||||
cls.create_loginable_secgroup_rule(secgroup_id=cls.secgroup['id'])
|
||||
cls.create_pingable_secgroup_rule(secgroup_id=cls.secgroup['id'])
|
||||
for protocol in ['udp', 'tcp']:
|
||||
cls.os_primary.network_client.create_security_group_rule(
|
||||
security_group_id=cls.secgroup['id'],
|
||||
protocol=protocol,
|
||||
direction=lib_constants.INGRESS_DIRECTION,
|
||||
port_range_min=constants.NCAT_PORT,
|
||||
port_range_max=constants.NCAT_PORT)
|
||||
|
||||
def _verify_capture_for_icmp_unreacheable(
|
||||
self, protocol, payload_size, src_client, dst_client,
|
||||
must_contain=False):
|
||||
"""Check that ICMP 'fragmentation needed' message is sent when needed
|
||||
|
||||
:param protocol(str): Allowed values 'udp', 'tcp'.
|
||||
:param payload_size(int): Size in bytes.
|
||||
:param src_client(Client): SSH client to the origin of the traffic.
|
||||
:param dst_client(Client): SSH client to the destination host.
|
||||
:param must_contain(bool): Specifies if we expect to get ICMP
|
||||
'fragmentation needed' message in the traffic capture.
|
||||
:return: MTU size of next hop or None if the capture must not
|
||||
contain ICMP message.
|
||||
"""
|
||||
udp = '-u' if protocol == 'udp' else ''
|
||||
utils.create_payload_file(src_client, payload_size)
|
||||
interface = utils.get_default_interface(src_client)
|
||||
filters = 'icmp[icmptype] == 3 and icmp[icmpcode] == 4'
|
||||
|
||||
src_server_capture = capture.TcpdumpCapture(
|
||||
src_client, interface, filters)
|
||||
self.useFixture(src_server_capture)
|
||||
time.sleep(10)
|
||||
|
||||
if not must_contain:
|
||||
output_file = utils.run_ncat_server(dst_client, udp)
|
||||
utils.run_ncat_client(src_client, dst_client.host, udp, payload_size)
|
||||
time.sleep(5)
|
||||
src_server_capture.stop()
|
||||
|
||||
if must_contain:
|
||||
self.assertFalse(
|
||||
src_server_capture.is_empty(),
|
||||
"No ICMP 'fragmentation needed' message")
|
||||
return src_server_capture.get_next_hop_mtu()
|
||||
else:
|
||||
self.assertTrue(
|
||||
src_server_capture.is_empty(),
|
||||
"Unexpected ICMP 'fragmentation needed' message found")
|
||||
output_file_size = int(dst_client.exec_command(
|
||||
'stat -c "%s" ' + output_file).rstrip())
|
||||
self.assertEqual(
|
||||
output_file_size, payload_size,
|
||||
"Delivered data has size {} bytes while expected {}.".format(
|
||||
output_file_size, payload_size))
|
||||
|
||||
def _validate_environment_config(self):
|
||||
msg = "ovn_emit_need_to_frag is not set to 'true' in config file"
|
||||
for node in self.nodes:
|
||||
result = node['client'].exec_command(
|
||||
"sudo ovs-appctl -t ovs-vswitchd dpif/show-dp-features {} | "
|
||||
"grep 'Check pkt'".format(
|
||||
WB_CONF.node_integration_bridge))
|
||||
if 'Yes' not in result:
|
||||
raise self.skipException(
|
||||
"Path MTU discovery is not supported")
|
||||
if WB_CONF.openstack_type == 'devstack':
|
||||
if node['is_controller'] is False:
|
||||
continue
|
||||
else:
|
||||
self.check_service_setting(
|
||||
node, service='',
|
||||
config_files=[WB_CONF.ml2_plugin_config],
|
||||
section='ovn', param='ovn_emit_need_to_frag',
|
||||
msg=msg)
|
||||
if WB_CONF.openstack_type == 'podified':
|
||||
config_files = self.get_configs_of_service()
|
||||
self.check_service_setting(
|
||||
{'client': self.proxy_host_client},
|
||||
config_files=config_files, section='ovn',
|
||||
param='ovn_emit_need_to_frag', msg=msg)
|
||||
|
||||
def setup(self):
|
||||
self._validate_environment_config()
|
||||
|
||||
self.network = self.create_network()
|
||||
subnet = self.create_subnet(self.network)
|
||||
self.router = self.create_router_by_client()
|
||||
self.create_router_interface(self.router['id'], subnet['id'])
|
||||
|
||||
self.external_network = self.client.show_network(
|
||||
CONF.network.public_network_id)['network']
|
||||
ext_vm = self._create_server(
|
||||
network=self.external_network,
|
||||
create_floating_ip=False)
|
||||
ext_vm_ssh_client = ssh.Client(
|
||||
ext_vm['port']['fixed_ips'][0]['ip_address'], self.username,
|
||||
pkey=self.keypair['private_key'])
|
||||
self.local_client = ext_vm_ssh_client
|
||||
|
||||
# We'll use the default self.server as a proxy for no-FIP scenario
|
||||
self.server = self._create_server()
|
||||
server_ssh_client = ssh.Client(
|
||||
self.server['fip']['floating_ip_address'],
|
||||
self.username, pkey=self.keypair['private_key'])
|
||||
self.test_server = self._create_server(create_floating_ip=False)
|
||||
test_server_ip = self.test_server['port']['fixed_ips'][0]['ip_address']
|
||||
self.test_server_client = ssh.Client(
|
||||
test_server_ip, self.username,
|
||||
pkey=self.keypair['private_key'], proxy_client=server_ssh_client)
|
||||
self.validate_current_mtus()
|
||||
|
||||
def validate_current_mtus(self):
|
||||
self.internal_mtu = self.network['mtu']
|
||||
self.external_mtu = self.external_network['mtu']
|
||||
if int(self.external_mtu) > int(self.internal_mtu):
|
||||
raise self.skipException(
|
||||
'Internal network MTU is smaller than external.')
|
||||
|
||||
def validate_next_hop_mtu(self, starting_payload_size):
|
||||
next_hop_mtu = self._verify_capture_for_icmp_unreacheable(
|
||||
protocol='udp',
|
||||
must_contain=True,
|
||||
src_client=self.test_server_client,
|
||||
dst_client=self.local_client,
|
||||
payload_size=starting_payload_size)
|
||||
payload_size = (int(next_hop_mtu) - constants.IP_HEADER_LENGTH -
|
||||
constants.UDP_HEADER_LENGTH)
|
||||
self.assertLess(
|
||||
payload_size, starting_payload_size,
|
||||
"Received next hop MTU %s is bigger than expected" % next_hop_mtu)
|
||||
self._verify_capture_for_icmp_unreacheable(
|
||||
protocol='udp',
|
||||
must_contain=False,
|
||||
src_client=self.test_server_client,
|
||||
dst_client=self.local_client,
|
||||
payload_size=payload_size)
|
||||
|
||||
def check_pmtud_basic(self):
|
||||
self.setup()
|
||||
maximal_payload_for_internal_net = (self.internal_mtu -
|
||||
constants.IP_HEADER_LENGTH -
|
||||
constants.UDP_HEADER_LENGTH)
|
||||
self.validate_next_hop_mtu(
|
||||
starting_payload_size=maximal_payload_for_internal_net)
|
||||
self.create_floatingip(port=self.test_server['port'])
|
||||
self.validate_next_hop_mtu(
|
||||
starting_payload_size=maximal_payload_for_internal_net)
|
||||
|
||||
|
||||
class GatewayMtuTestIcmp(GatewayMtuTest):
|
||||
|
||||
def make_sure_routing_cache_is_clear(self, ssh_client, dst):
|
||||
utils.flush_routing_cache(ssh_client)
|
||||
result = ssh_client.exec_command("ip route get %s" % dst)
|
||||
if 'mtu' in result:
|
||||
raise self.skipException(
|
||||
'Routing cache already contains mtu records')
|
||||
|
||||
def validate_next_hop_mtu(self, starting_payload_size):
|
||||
next_hop_mtu = self._verify_capture_for_icmp_unreacheable(
|
||||
must_contain=True,
|
||||
src_client=self.test_server_client,
|
||||
dst_client=self.local_client,
|
||||
payload_size=starting_payload_size)
|
||||
payload_size = (int(next_hop_mtu) - constants.IP_HEADER_LENGTH -
|
||||
constants.UDP_HEADER_LENGTH)
|
||||
self.assertLess(
|
||||
payload_size, starting_payload_size,
|
||||
"Received next hop MTU %s is bigger than expected" % next_hop_mtu)
|
||||
self._verify_capture_for_icmp_unreacheable(
|
||||
must_contain=False,
|
||||
src_client=self.test_server_client,
|
||||
dst_client=self.local_client,
|
||||
payload_size=starting_payload_size)
|
||||
|
||||
def _verify_capture_for_icmp_unreacheable(
|
||||
self, payload_size, src_client, dst_client, must_contain=False):
|
||||
"""Check that ICMP 'fragmentation needed' message is sent when needed
|
||||
|
||||
:param payload_size(int): Size in bytes.
|
||||
:param src_client(Client): SSH client to the origin of the traffic.
|
||||
:param dst_client(Client): SSH client to the destination
|
||||
:param must_contain(bool): Specifies if we expect to get ICMP
|
||||
'fragmentation needed' message in the traffic capture.
|
||||
:return: MTU size of next hop or None if the capture must not
|
||||
contain ICMP message.
|
||||
"""
|
||||
interface = utils.get_default_interface(src_client)
|
||||
filters = 'icmp[icmptype] == 3 and icmp[icmpcode] == 4'
|
||||
should_succeed = not must_contain
|
||||
if must_contain:
|
||||
self.make_sure_routing_cache_is_clear(src_client, dst_client.host)
|
||||
time.sleep(2)
|
||||
ping_count = 1
|
||||
else:
|
||||
ping_count = 10
|
||||
|
||||
src_server_capture = capture.TcpdumpCapture(
|
||||
src_client, interface, filters)
|
||||
self.useFixture(src_server_capture)
|
||||
time.sleep(10)
|
||||
self.check_remote_connectivity(
|
||||
source=src_client, dest=dst_client.host, mtu=payload_size,
|
||||
should_succeed=should_succeed, ping_count=ping_count, timeout=20,
|
||||
forbid_packet_loss=True)
|
||||
time.sleep(5)
|
||||
src_server_capture.stop()
|
||||
|
||||
if must_contain:
|
||||
self.assertFalse(
|
||||
src_server_capture.is_empty(),
|
||||
"No ICMP 'fragmentation needed' message")
|
||||
return src_server_capture.get_next_hop_mtu()
|
||||
else:
|
||||
self.assertTrue(
|
||||
src_server_capture.is_empty(),
|
||||
"Unexpected ICMP 'fragmentation needed' message found")
|
||||
return None
|
||||
|
||||
@decorators.idempotent_id('10d73cf0-7506-4899-864c-cebe43099be3')
|
||||
def test_northbound_pmtud_icmp(self):
|
||||
self.check_pmtud_basic()
|
||||
|
||||
|
||||
class GatewayMtuTestOvn(GatewayMtuTest, base.BaseTempestTestCaseOvn):
|
||||
credentials = ['primary', 'admin']
|
||||
|
||||
def _validate_ovn_db_mtu_settings(self):
|
||||
router_port = self.os_admin.network_client.list_ports(
|
||||
device_id=self.router['id'],
|
||||
device_owner=lib_constants.DEVICE_OWNER_ROUTER_GW)['ports'][0]
|
||||
ovn_gateway_mtu = self.get_router_port_gateway_mtu(router_port['id'])
|
||||
self.assertEqual(
|
||||
self.external_mtu, ovn_gateway_mtu,
|
||||
"MTU is not set properly in OVN DB")
|
||||
|
||||
def _restore_external_network_mtu(self):
|
||||
self.os_admin.network_client.update_network(
|
||||
CONF.network.public_network_id, mtu=self.original_mtu)
|
||||
|
||||
@decorators.idempotent_id('7f7470ff-31b4-4ad8-bfa7-82dcca174744')
|
||||
def test_south_to_north_pmtud_udp_basic(self):
|
||||
"""Verify that south->north path MTU discovery is working for UDP
|
||||
|
||||
Setup:
|
||||
Environment with public network MTU smaller than tenant networks.
|
||||
|
||||
Scenario:
|
||||
1. Create router connected to pre-existed external network.
|
||||
It is expected that the external network MTU is lower than
|
||||
internal network MTU (will be created in the next step)
|
||||
2. Create internal network, connect it to the router and spawn
|
||||
2 instances, one will be used as a proxy in order to be able
|
||||
to access the test server even during no-FIP scenario.
|
||||
3. Launch traffic capture on the test server and send a UDP
|
||||
packet matching the internal network MTU to the host
|
||||
connected to the external network (local address from
|
||||
external network is used).
|
||||
4. Validate that the router returned ICMP 'fragmentation
|
||||
needed' message and send a new UDP packet matching MTU
|
||||
in this message.
|
||||
5. Validate that the smaller file was delivered to the external
|
||||
address and the router did not send any ICMP message.
|
||||
6. Add a FIP to the test server and repeat steps 3-5.
|
||||
"""
|
||||
self.check_pmtud_basic()
|
||||
|
||||
@decorators.idempotent_id('cab28be4-7d11-485b-b99e-ea511fe2a675')
|
||||
def test_south_to_north_pmtud_udp_change_mtu(self):
|
||||
"""Verify that south->north path MTU discovery is working for UDP
|
||||
|
||||
Setup:
|
||||
Environment with public network MTU smaller than tenant networks.
|
||||
|
||||
Scenario:
|
||||
1. Create router connected to pre-existed external network.
|
||||
It is expected that the external network MTU is lower than
|
||||
internal network MTU (will be created in the next step)
|
||||
2. Create internal network, connect it to the router and spawn
|
||||
2 instances, one will be used as a proxy in order to be able
|
||||
to access the test server even during no-FIP scenario.
|
||||
3. Verify that OVN DB MTU entries are configured correctly.
|
||||
4. Change MTU of the external network and verify that OVN DB
|
||||
MTU settings upated accordingly.
|
||||
5. Verify that south->north path MTU discovery is working for UDP
|
||||
using new the MTU (see details in test_south_to_north_pmtud_udp
|
||||
description, steps 3-6)
|
||||
6. After the test the original MTU of the external network is
|
||||
restored.
|
||||
"""
|
||||
self.setup()
|
||||
self._validate_ovn_db_mtu_settings()
|
||||
self.original_mtu = self.external_network['mtu']
|
||||
self.addCleanup(self._restore_external_network_mtu)
|
||||
new_mtu = int(self.external_mtu) - 30
|
||||
self.external_network = self.os_admin.network_client.update_network(
|
||||
CONF.network.public_network_id, mtu=new_mtu)['network']
|
||||
self.validate_current_mtus()
|
||||
self._validate_ovn_db_mtu_settings()
|
||||
minimal_payload_causing_fragmentation = (
|
||||
self.external_mtu - constants.ETHERNET_HEADER_LENGTH + 1)
|
||||
self.validate_next_hop_mtu(
|
||||
starting_payload_size=minimal_payload_causing_fragmentation)
|
||||
self.create_floatingip(port=self.test_server['port'])
|
||||
self.validate_next_hop_mtu(
|
||||
starting_payload_size=minimal_payload_causing_fragmentation)
|
@ -319,6 +319,8 @@
|
||||
/$NEUTRON_CORE_PLUGIN_CONF:
|
||||
ml2:
|
||||
type_drivers: local,flat,vlan,geneve
|
||||
ovn:
|
||||
ovn_emit_need_to_frag: true
|
||||
test-config:
|
||||
$TEMPEST_CONFIG:
|
||||
network-feature-enabled:
|
||||
|
Loading…
Reference in New Issue
Block a user