From cdc2d6ad32f8597ffe3e78de43a575ec4c4ba400 Mon Sep 17 00:00:00 2001 From: Roman Safronov Date: Wed, 8 Nov 2023 16:23:19 +0100 Subject: [PATCH] Add test_vrrp Add existing tests to increase plugin functionality Also add code to customize advanced image to make it suitable for all tests of the plugin. Change-Id: I65d53544e85850c3b0a9929198fab14bddcca636 --- devstack/plugin.sh | 45 ++- .../tests/scenario/test_vrrp.py | 269 ++++++++++++++++++ 2 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 whitebox_neutron_tempest_plugin/tests/scenario/test_vrrp.py diff --git a/devstack/plugin.sh b/devstack/plugin.sh index 3a0226f..6619471 100644 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -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 diff --git a/whitebox_neutron_tempest_plugin/tests/scenario/test_vrrp.py b/whitebox_neutron_tempest_plugin/tests/scenario/test_vrrp.py new file mode 100644 index 0000000..e0622d5 --- /dev/null +++ b/whitebox_neutron_tempest_plugin/tests/scenario/test_vrrp.py @@ -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)