Merge "Add test_vrrp"
This commit is contained in:
commit
dc5aa0a129
@ -1,3 +1,44 @@
|
||||
if [[ "$1" == "stack" ]] && [[ "$2" == "install" ]]; then
|
||||
echo "tempest ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/99_tempest
|
||||
customize_advanced_image(){
|
||||
# Here we customize an advanced image to make it suitable for the plugin tests.
|
||||
# Note: the advanced image was downloaded and set by neutron_tempest_plugin.
|
||||
# However we can't rely on neutron_tempest_plugin capabilities for customizing
|
||||
# the image since it expects a debian/ubuntu based image which does not fit well
|
||||
# to this plugin tests.
|
||||
# This code modifies the downloaded image by adding packages required by this
|
||||
# plugin, uploads the image to glance and if all passed successfully it updates
|
||||
# tempest.conf with the new image reference instead of the original one.
|
||||
sudo dnf install guestfs-tools -y
|
||||
for image_url in ${IMAGE_URLS//,/ }; do
|
||||
if [[ $image_url =~ $ADVANCED_IMAGE_NAME ]]; then
|
||||
image_file=$(basename $image_url)
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -n "$image_file" ] && [ -s "$TOP_DIR/files/$image_file" ]; then
|
||||
cp -f $TOP_DIR/files/$image_file /tmp
|
||||
image_file_custom=/tmp/$image_file
|
||||
timeout 150 virt-customize -a $image_file_custom --install nmap,python3,keepalived,iperf3 --selinux-relabel
|
||||
if [ "$?" == "0" ]; then
|
||||
source $TOP_DIR/openrc admin
|
||||
old_image_id=$(openstack image show $ADVANCED_IMAGE_NAME -c id -f value)
|
||||
new_image_id=$(openstack image create --disk-format qcow2 --container-format bare --public $ADVANCED_IMAGE_NAME --file $image_file_custom -c id -f value)
|
||||
if [ -n "$new_image_id" ]; then
|
||||
iniset $TEMPEST_CONFIG neutron_plugin_options advanced_image_ref $new_image_id
|
||||
openstack image delete $old_image_id
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ "$1" == "stack" ]]; then
|
||||
case "$2" in
|
||||
install)
|
||||
if [[ "$INSTALL_TEMPEST" == "True" ]]; then
|
||||
echo "tempest ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/99_tempest
|
||||
fi
|
||||
;;
|
||||
test-config)
|
||||
customize_advanced_image
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
269
whitebox_neutron_tempest_plugin/tests/scenario/test_vrrp.py
Normal file
269
whitebox_neutron_tempest_plugin/tests/scenario/test_vrrp.py
Normal file
@ -0,0 +1,269 @@
|
||||
# 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.
|
||||
|
||||
from neutron_lib import constants
|
||||
from oslo_log import log
|
||||
from tempest.common import compute
|
||||
from tempest.common import utils
|
||||
from tempest.common import waiters
|
||||
from tempest.lib.common.utils import data_utils
|
||||
from tempest.lib import decorators
|
||||
|
||||
from neutron_tempest_plugin.common import ssh
|
||||
from neutron_tempest_plugin.common import utils as common_utils
|
||||
from neutron_tempest_plugin import config
|
||||
from neutron_tempest_plugin import exceptions
|
||||
from neutron_tempest_plugin.scenario import base
|
||||
|
||||
|
||||
CONF = config.CONF
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
keepalived_config_template = """vrrp_instance VIP_1 {
|
||||
state MASTER
|
||||
interface %s
|
||||
virtual_router_id 51
|
||||
priority 150
|
||||
advert_int 1
|
||||
authentication {
|
||||
auth_type PASS
|
||||
auth_pass secretpass
|
||||
}
|
||||
virtual_ipaddress {
|
||||
%s
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def get_keepalived_config(interface, vip_ip):
|
||||
return keepalived_config_template % (interface, vip_ip)
|
||||
|
||||
|
||||
class VrrpTest(base.BaseTempestTestCase):
|
||||
credentials = ['primary', 'admin']
|
||||
|
||||
@classmethod
|
||||
def skip_checks(cls):
|
||||
super(VrrpTest, cls).skip_checks()
|
||||
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')
|
||||
|
||||
@classmethod
|
||||
@utils.requires_ext(extension="router", service="network")
|
||||
def resource_setup(cls):
|
||||
super(VrrpTest, cls).resource_setup()
|
||||
# Create keypair with admin privileges
|
||||
cls.keypair = cls.create_keypair()
|
||||
# Create security group with admin privileges
|
||||
cls.secgroup = cls.create_security_group(
|
||||
name=data_utils.rand_name('secgroup'))
|
||||
# Execute funcs to achieve ssh, ICMP and VRRP capabilities
|
||||
cls.create_loginable_secgroup_rule(secgroup_id=cls.secgroup['id'])
|
||||
cls.create_pingable_secgroup_rule(secgroup_id=cls.secgroup['id'])
|
||||
cls.create_security_group_rule(security_group_id=cls.secgroup['id'],
|
||||
protocol=constants.PROTO_NAME_VRRP,
|
||||
direction=constants.INGRESS_DIRECTION)
|
||||
|
||||
def _create_server(self, port, name=None, scheduler_hints=None):
|
||||
if not name:
|
||||
name = data_utils.rand_name('vm')
|
||||
params = {
|
||||
'flavor_ref': self.flavor_ref,
|
||||
'image_ref': self.image_ref,
|
||||
'key_name': self.keypair['name'],
|
||||
'name': name
|
||||
}
|
||||
if (scheduler_hints and CONF.compute.min_compute_nodes > 1 and
|
||||
compute.is_scheduler_filter_enabled("DifferentHostFilter")):
|
||||
params['scheduler_hints'] = scheduler_hints
|
||||
vm = self.create_server(networks=[{'port': port['id']}], **params)
|
||||
vm['server']['name'] = name
|
||||
return vm
|
||||
|
||||
def _get_vm_host(self, server_id):
|
||||
server_details = self.os_admin.servers_client.show_server(server_id)
|
||||
return server_details['server']['OS-EXT-SRV-ATTR:host']
|
||||
|
||||
def _check_keepalived_on_server(self, ssh_client, server_id):
|
||||
try:
|
||||
ssh_client.execute_script('PATH=$PATH:/usr/sbin which keepalived')
|
||||
except exceptions.SSHScriptFailed:
|
||||
raise self.skipException(
|
||||
"keepalived is not available on server %s" % (server_id))
|
||||
|
||||
@staticmethod
|
||||
def _is_keepalived_service_active(ssh_client):
|
||||
output = ssh_client.exec_command("sudo systemctl is-active keepalived")
|
||||
return 'active' == output.splitlines()[0]
|
||||
|
||||
def _prepare_server(self, ssh_client, interface, vip_ip):
|
||||
config_text = get_keepalived_config(interface, vip_ip)
|
||||
config_file = 'keepalived.conf'
|
||||
ssh_client.execute_script(
|
||||
'echo "{0}" > /tmp/{1};'
|
||||
'sudo mv -Z /tmp/{1} /etc/keepalived/'.format(
|
||||
config_text, config_file))
|
||||
ssh_client.exec_command("sudo systemctl restart keepalived")
|
||||
# make sure keepalived is active
|
||||
common_utils.wait_until_true(
|
||||
lambda: self._is_keepalived_service_active(ssh_client=ssh_client),
|
||||
timeout=20,
|
||||
exception=RuntimeError("Timed out waiting for keepalived active"))
|
||||
|
||||
@staticmethod
|
||||
def _get_vm_id_by_name(name, vms):
|
||||
for vm in vms:
|
||||
if vm['server']['name'] == name:
|
||||
return vm['server']['id']
|
||||
return None
|
||||
|
||||
def _get_client(self, ip_address, proxy_client=None):
|
||||
return ssh.Client(ip_address,
|
||||
self.username,
|
||||
pkey=self.keypair['private_key'],
|
||||
proxy_client=proxy_client)
|
||||
|
||||
@decorators.idempotent_id('f88ca220-eea2-48d2-9cac-3f382908cb37')
|
||||
def test_vrrp_vip_failover(self):
|
||||
"""This test verifies traffic flow during VRRP VIP failover
|
||||
|
||||
The aim of the test is to validate that in case master VM
|
||||
becomes not available the traffic to the VIP is directed to the
|
||||
second VM.
|
||||
|
||||
Recommended topology:
|
||||
Controller node plus at least 2 compute nodes.
|
||||
|
||||
Scenario:
|
||||
- Create a port for VRRP VIP and ports for VMs with
|
||||
allowed address pair configured to the VIP IP address
|
||||
- Attach a FIP to each one of these ports, including the VIP. We will
|
||||
differentiate between private VIP and public (FIP) VIP
|
||||
- Create two VMs on different compute nodes
|
||||
- Setup VRRP between the VMs using keepalived
|
||||
- Create a proxy VM with a normal port and a FIP. This VM is neither
|
||||
part of the VRRP VIP configuration, nor keepalived is installed on it
|
||||
- Test traffic to the public VIP (login via ssh)
|
||||
- Test traffic to the private VIP through the proxy VM
|
||||
- Kill active VM
|
||||
- Test traffic to the public VIP. Traffic should now flow to the
|
||||
second VM
|
||||
- Test traffic to the private VIP through the proxy VM. Traffic should
|
||||
now flow to the second VM
|
||||
"""
|
||||
network = self.create_network()
|
||||
subnet = self.create_subnet(network, cidr="192.168.100.0/24")
|
||||
router = self.create_router_by_client()
|
||||
self.create_router_interface(router['id'], subnet['id'])
|
||||
ports = {'vip': {}, 'vm1': {}, 'vm2': {}}
|
||||
|
||||
ports['vip']['port'] = self.create_port(
|
||||
network=network)
|
||||
vip_ip = ports['vip']['port']['fixed_ips'][0]['ip_address']
|
||||
|
||||
vm_names = ['vm1', 'vm2']
|
||||
for vm in vm_names:
|
||||
ports[vm]['port'] = self.create_port(
|
||||
network=network, security_groups=[self.secgroup['id']],
|
||||
allowed_address_pairs=[{"ip_address": vip_ip}])
|
||||
|
||||
for key in ports.keys():
|
||||
ports[key]['fip'] = self.create_floatingip(
|
||||
port=ports[key]['port'])
|
||||
|
||||
vms = []
|
||||
vm1 = self._create_server(port=ports['vm1']['port'], name='vm1')
|
||||
vm2 = self._create_server(
|
||||
port=ports['vm2']['port'], name='vm2',
|
||||
scheduler_hints={'different_host': vm1['server']['id']})
|
||||
vms = [vm1, vm2]
|
||||
|
||||
if (self._get_vm_host(vm1['server']['id']) ==
|
||||
self._get_vm_host(vm2['server']['id']) and
|
||||
CONF.compute.min_compute_nodes > 1):
|
||||
raise self.skipException(
|
||||
"VMs are running on the same host."
|
||||
"Make sure you have DifferentHostFilter enabled in nova.conf "
|
||||
"in order to cover multi-node scenario properly.")
|
||||
|
||||
for vm in vm_names:
|
||||
ports[vm]['client'] = ssh.Client(
|
||||
ports[vm]['fip']['floating_ip_address'],
|
||||
self.username,
|
||||
pkey=self.keypair['private_key'])
|
||||
interface = ports[vm]['client'].exec_command(
|
||||
"PATH=$PATH:/usr/sbin ip route get default 8.8.8.8 | "
|
||||
"head -1 | cut -d ' ' -f 5").rstrip()
|
||||
self._check_keepalived_on_server(ports[vm]['client'], vm)
|
||||
self._prepare_server(ports[vm]['client'], interface, vip_ip)
|
||||
|
||||
# create proxy vm
|
||||
port_vm_proxy = self.create_port(network=network,
|
||||
security_groups=[self.secgroup['id']])
|
||||
self._create_server(port=port_vm_proxy, name='vm_proxy')
|
||||
fip_vm_proxy = self.create_floatingip(port=port_vm_proxy)
|
||||
proxy_client = ssh.Client(fip_vm_proxy['floating_ip_address'],
|
||||
self.username,
|
||||
pkey=self.keypair['private_key'])
|
||||
|
||||
# verify public VIP connectivity
|
||||
ports['vip']['client'] = self._get_client(
|
||||
ports['vip']['fip']['floating_ip_address'])
|
||||
master_host = ports['vip']['client'].exec_command(
|
||||
'hostname').rstrip()
|
||||
LOG.debug('(obtained from public VIP) master_host = ' + master_host)
|
||||
# verify private VIP connectivity
|
||||
private_vip_client = self._get_client(
|
||||
vip_ip, proxy_client=proxy_client)
|
||||
master_host_private = private_vip_client.exec_command(
|
||||
'hostname').rstrip()
|
||||
LOG.debug('(obtained from private VIP) master_host = ' +
|
||||
master_host_private)
|
||||
self.assertEqual(master_host, master_host_private)
|
||||
|
||||
LOG.debug('Stopping master host')
|
||||
master_host_id = self._get_vm_id_by_name(master_host, vms)
|
||||
self.os_primary.servers_client.stop_server(master_host_id)
|
||||
waiters.wait_for_server_status(self.os_primary.servers_client,
|
||||
master_host_id, 'SHUTOFF')
|
||||
|
||||
# verify public VIP connectivity
|
||||
ports['vip']['client'] = self._get_client(
|
||||
ports['vip']['fip']['floating_ip_address'])
|
||||
new_master_host = ports['vip']['client'].exec_command(
|
||||
'hostname').rstrip()
|
||||
LOG.debug('(obtained from public VIP) new_master_host = ' +
|
||||
new_master_host)
|
||||
self.assertNotEqual(master_host, new_master_host)
|
||||
# verify private VIP connectivity
|
||||
private_vip_client = self._get_client(
|
||||
vip_ip, proxy_client=proxy_client)
|
||||
new_master_host_private = private_vip_client.exec_command(
|
||||
'hostname').rstrip()
|
||||
LOG.debug('(obtained from private VIP) new_master_host = ' +
|
||||
new_master_host_private)
|
||||
self.assertEqual(new_master_host, new_master_host_private)
|
Loading…
Reference in New Issue
Block a user