Add scenario test: test instances with cinder volumes

This patch adds a scenario test instances with cinder volumes launches
on all available compute nodes, up to CONF.compute.min_compute_nodes.
Also 1 additional configuration is added: volume_types_for_data_volume.

Also function for create and add security group to server is moved
to tempest/scenario/manager.py to avoid code duplication.

Change-Id: I346a9505bc942e66aedad2029215617d0918a885
This commit is contained in:
jskunda 2023-10-18 13:49:02 +02:00 committed by Martin Kopec
parent 5c64e3913a
commit d744598179
5 changed files with 272 additions and 22 deletions

View File

@ -0,0 +1,8 @@
---
features:
- |
A new config option in the ``volume_feature_enabled`` section,
``volume_types_for_data_volume``, is added to allow the user to specify
which volume types can be used for data volumes in a new test
``test_instances_with_cinder_volumes_on_all_compute_nodes``. By default,
this option is set to None.

View File

@ -1111,7 +1111,11 @@ VolumeFeaturesGroup = [
default=True, default=True,
help='Does the cloud support extending the size of a volume ' help='Does the cloud support extending the size of a volume '
'which has snapshot? Some drivers do not support this ' 'which has snapshot? Some drivers do not support this '
'operation.') 'operation.'),
cfg.StrOpt('volume_types_for_data_volume',
default=None,
help='Volume types used for data volumes. Multiple volume '
'types can be assigned.'),
] ]

View File

@ -734,6 +734,31 @@ class ScenarioTest(tempest.test.BaseTestCase):
return rules return rules
def create_and_add_security_group_to_server(self, server):
"""Create a security group and add it to the server.
:param server: The server to add the security group to.
:return: The security group was added to the server.
"""
secgroup = self.create_security_group()
self.servers_client.add_security_group(server['id'],
name=secgroup['name'])
self.addCleanup(self.servers_client.remove_security_group,
server['id'], name=secgroup['name'])
def wait_for_secgroup_add():
body = (self.servers_client.show_server(server['id'])
['server'])
return {'name': secgroup['name']} in body['security_groups']
if not test_utils.call_until_true(wait_for_secgroup_add,
CONF.compute.build_timeout,
CONF.compute.build_interval):
msg = ('Timed out waiting for adding security group %s to server '
'%s' % (secgroup['id'], server['id']))
raise lib_exc.TimeoutException(msg)
def get_remote_client(self, ip_address, username=None, private_key=None, def get_remote_client(self, ip_address, username=None, private_key=None,
server=None): server=None):
"""Get a SSH client to a remote server """Get a SSH client to a remote server
@ -1103,6 +1128,15 @@ class ScenarioTest(tempest.test.BaseTestCase):
self.assertIsNone(floating_ip['port_id']) self.assertIsNone(floating_ip['port_id'])
return floating_ip return floating_ip
def create_file(self, ip_address, path, private_key=None, server=None,
username=None):
"""Create a file on a remote server"""
ssh_client = self.get_remote_client(ip_address,
private_key=private_key,
server=server,
username=username)
ssh_client.exec_command('sudo mkdir -p %s' % path)
def create_timestamp(self, ip_address, dev_name=None, mount_path='/mnt', def create_timestamp(self, ip_address, dev_name=None, mount_path='/mnt',
private_key=None, server=None, username=None, private_key=None, server=None, username=None,
fs='vfat'): fs='vfat'):

View File

@ -0,0 +1,225 @@
# Copyright 2024 Openstack Foundation
# 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 oslo_log import log as logging
from tempest.common import utils
from tempest.common import waiters
from tempest import config
from tempest.lib import decorators
from tempest.lib import exceptions
from tempest.scenario import manager
CONF = config.CONF
LOG = logging.getLogger(__name__)
class TestInstancesWithCinderVolumes(manager.ScenarioTest):
"""This is cinder volumes test.
Tests are below:
* test_instances_with_cinder_volumes_on_all_compute_nodes
"""
compute_min_microversion = '2.60'
@decorators.idempotent_id('d0e3c1a3-4b0a-4b0e-8b0a-4b0e8b0a4b0e')
@decorators.attr(type=['slow', 'multinode'])
@utils.services('compute', 'volume', 'image', 'network')
def test_instances_with_cinder_volumes_on_all_compute_nodes(self):
"""Test instances with cinder volumes launches on all compute nodes
Steps:
1. Create an image
2. Create a keypair
3. Create a bootable volume from the image and of the given volume
type
4. Boot an instance from the bootable volume on each available
compute node, up to CONF.compute.min_compute_nodes
5. Create a volume using each volume_types_for_data_volume on all
available compute nodes, up to CONF.compute.min_compute_nodes.
Total number of volumes is equal to
compute nodes * len(volume_types_for_data_volume)
6. Attach volumes to the instances
7. Assign floating IP to all instances
8. Configure security group for ssh access to all instances
9. Confirm ssh access to all instances
10. Run write test to all volumes through ssh connection per
instance
11. Clean up the sources, an instance, volumes, keypair and image
"""
boot_volume_type = (CONF.volume.volume_type or
self.create_volume_type()['name'])
# create an image
image = self.image_create()
# create keypair
keypair = self.create_keypair()
# check all available zones for booting instances
available_zone = \
self.os_admin.availability_zone_client.list_availability_zones(
detail=True)['availabilityZoneInfo']
hosts = []
for zone in available_zone:
if zone['zoneState']['available']:
for host in zone['hosts']:
if 'nova-compute' in zone['hosts'][host] and \
zone['hosts'][host]['nova-compute']['available'] and \
not host.endswith('-ironic'):
hosts.append({'zone': zone['zoneName'],
'host_name': host})
# fail if there is less hosts than minimal number of instances
if len(hosts) < CONF.compute.min_compute_nodes:
raise exceptions.InvalidConfiguration(
"Host list %s is shorter than min_compute_nodes. " % hosts)
# get volume types
volume_types = []
if CONF.volume_feature_enabled.volume_types_for_data_volume:
types = CONF.volume_feature_enabled.volume_types_for_data_volume
volume_types = types.split(',')
else:
# no user specified volume types, create 2 default ones
volume_types.append(self.create_volume_type()['name'])
volume_types.append(self.create_volume_type()['name'])
hosts_to_boot_servers = hosts[:CONF.compute.min_compute_nodes]
LOG.debug("List of hosts selected to boot servers %s: ",
hosts_to_boot_servers)
# create volumes so that we dont need to wait for them to be created
# and save them in a list
created_volumes = []
for host in hosts_to_boot_servers:
for volume_type in volume_types:
created_volumes.append(
self.create_volume(volume_type=volume_type,
wait_until=None)
)
bootable_volumes = []
for host in hosts_to_boot_servers:
# create boot volume from image and of the given volume type
bootable_volumes.append(
self.create_volume(
imageRef=image, volume_type=boot_volume_type,
wait_until=None)
)
# boot server
servers = []
for bootable_volume in bootable_volumes:
# wait for bootable volumes to become available
waiters.wait_for_volume_resource_status(
self.volumes_client, bootable_volume['id'], 'available')
# create an instance from bootable volume
server = self.boot_instance_from_resource(
source_id=bootable_volume['id'],
source_type='volume',
keypair=keypair,
wait_until=None
)
servers.append(server)
start = 0
end = len(volume_types)
for server in servers:
attached_volumes = []
# wait for server to become active
waiters.wait_for_server_status(self.servers_client,
server['id'], 'ACTIVE')
# attach volumes to the instances
for volume in created_volumes[start:end]:
# wait for volume to become available
waiters.wait_for_volume_resource_status(
self.volumes_client, volume['id'], 'available')
attached_volume = self.nova_volume_attach(server, volume)
attached_volumes.append(attached_volume)
LOG.debug("Attached volume %s to server %s",
attached_volume['id'], server['id'])
# assign floating ip
floating_ip = None
if (CONF.network_feature_enabled.floating_ips and
CONF.network.floating_network_name):
fip = self.create_floating_ip(server)
floating_ip = self.associate_floating_ip(
fip, server)
ssh_ip = floating_ip['floating_ip_address']
else:
ssh_ip = self.get_server_ip(server)
# create security group
self.create_and_add_security_group_to_server(server)
# confirm ssh access
self.linux_client = self.get_remote_client(
ssh_ip, private_key=keypair['private_key'],
server=server
)
# run write test on all volumes
for volume in attached_volumes:
waiters.wait_for_volume_resource_status(
self.volumes_client, volume['id'], 'in-use')
# get the mount path
mount_path = f"/mnt/{volume['attachments'][0]['device'][5:]}"
# create file for mounting on server
self.create_file(ssh_ip, mount_path,
private_key=keypair['private_key'],
server=server)
# dev name volume['attachments'][0]['device'][5:] is like
# /dev/vdb, we need to remove /dev/ -> first 5 chars
timestamp_before = self.create_timestamp(
ssh_ip, private_key=keypair['private_key'], server=server,
dev_name=volume['attachments'][0]['device'][5:],
mount_path=mount_path
)
timestamp_after = self.get_timestamp(
ssh_ip, private_key=keypair['private_key'], server=server,
dev_name=volume['attachments'][0]['device'][5:],
mount_path=mount_path
)
self.assertEqual(timestamp_before, timestamp_after)
# delete volume
self.nova_volume_detach(server, volume)
self.volumes_client.delete_volume(volume['id'])
if floating_ip:
# delete the floating IP, this should refresh the server
# addresses
self.disassociate_floating_ip(floating_ip)
waiters.wait_for_server_floating_ip(
self.servers_client, server, floating_ip,
wait_for_disassociate=True)
start += len(volume_types)
end += len(volume_types)

View File

@ -19,9 +19,7 @@ from tempest.common import custom_matchers
from tempest.common import utils from tempest.common import utils
from tempest.common import waiters from tempest.common import waiters
from tempest import config from tempest import config
from tempest.lib.common.utils import test_utils
from tempest.lib import decorators from tempest.lib import decorators
from tempest.lib import exceptions
from tempest.scenario import manager from tempest.scenario import manager
CONF = config.CONF CONF = config.CONF
@ -73,25 +71,6 @@ class TestMinimumBasicScenario(manager.ScenarioTest):
disks = self.linux_client.get_disks() disks = self.linux_client.get_disks()
self.assertEqual(1, disks.count(CONF.compute.volume_device_name)) self.assertEqual(1, disks.count(CONF.compute.volume_device_name))
def create_and_add_security_group_to_server(self, server):
secgroup = self.create_security_group()
self.servers_client.add_security_group(server['id'],
name=secgroup['name'])
self.addCleanup(self.servers_client.remove_security_group,
server['id'], name=secgroup['name'])
def wait_for_secgroup_add():
body = (self.servers_client.show_server(server['id'])
['server'])
return {'name': secgroup['name']} in body['security_groups']
if not test_utils.call_until_true(wait_for_secgroup_add,
CONF.compute.build_timeout,
CONF.compute.build_interval):
msg = ('Timed out waiting for adding security group %s to server '
'%s' % (secgroup['id'], server['id']))
raise exceptions.TimeoutException(msg)
@decorators.attr(type='slow') @decorators.attr(type='slow')
@decorators.idempotent_id('bdbb5441-9204-419d-a225-b4fdbfb1a1a8') @decorators.idempotent_id('bdbb5441-9204-419d-a225-b4fdbfb1a1a8')
@utils.services('compute', 'volume', 'image', 'network') @utils.services('compute', 'volume', 'image', 'network')