Merge "Use 'tempest run --slowest' in integrated-compute (again)"
diff --git a/releasenotes/notes/add-enable-volume-image-dep-tests-option-150b929d18da233f.yaml b/releasenotes/notes/add-enable-volume-image-dep-tests-option-150b929d18da233f.yaml
new file mode 100644
index 0000000..e78201e
--- /dev/null
+++ b/releasenotes/notes/add-enable-volume-image-dep-tests-option-150b929d18da233f.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add new config option 'enable_volume_image_dep_tests' in section
+ [volume-feature-enabled] which should be used in
+ image<->volume<->snapshot dependency tests.
diff --git a/releasenotes/notes/add-placement-resource-provider-traits-api-calls-9f4b0455afec9afb.yaml b/releasenotes/notes/add-placement-resource-provider-traits-api-calls-9f4b0455afec9afb.yaml
new file mode 100644
index 0000000..1d1811c
--- /dev/null
+++ b/releasenotes/notes/add-placement-resource-provider-traits-api-calls-9f4b0455afec9afb.yaml
@@ -0,0 +1,4 @@
+---
+features:
+ - |
+ Adds API calls for traits in ResourceProvidersClient.
diff --git a/releasenotes/notes/add-placement-traits-api-calls-087061f5455f0b12.yaml b/releasenotes/notes/add-placement-traits-api-calls-087061f5455f0b12.yaml
new file mode 100644
index 0000000..77d0b38
--- /dev/null
+++ b/releasenotes/notes/add-placement-traits-api-calls-087061f5455f0b12.yaml
@@ -0,0 +1,4 @@
+---
+features:
+ - |
+ Adds API calls for traits in PlacementClient.
diff --git a/tempest/api/compute/servers/test_delete_server.py b/tempest/api/compute/servers/test_delete_server.py
index ee25a22..596d2bd 100644
--- a/tempest/api/compute/servers/test_delete_server.py
+++ b/tempest/api/compute/servers/test_delete_server.py
@@ -99,11 +99,14 @@
def test_delete_server_while_in_verify_resize_state(self):
"""Test deleting a server while it's VM state is VERIFY_RESIZE"""
server = self.create_test_server(wait_until='ACTIVE')
- self.client.resize_server(server['id'], self.flavor_ref_alt)
- waiters.wait_for_server_status(self.client, server['id'],
- 'VERIFY_RESIZE')
- self.client.delete_server(server['id'])
- waiters.wait_for_server_termination(self.client, server['id'])
+ body = self.client.resize_server(server['id'], self.flavor_ref_alt)
+ request_id = body.response['x-openstack-request-id']
+ waiters.wait_for_server_status(
+ self.client, server['id'], 'VERIFY_RESIZE', request_id=request_id)
+ body = self.client.delete_server(server['id'])
+ request_id = body.response['x-openstack-request-id']
+ waiters.wait_for_server_termination(
+ self.client, server['id'], request_id=request_id)
@decorators.idempotent_id('d0f3f0d6-d9b6-4a32-8da4-23015dcab23c')
@utils.services('volume')
diff --git a/tempest/api/image/v2/test_images_dependency.py b/tempest/api/image/v2/test_images_dependency.py
new file mode 100644
index 0000000..326045b
--- /dev/null
+++ b/tempest/api/image/v2/test_images_dependency.py
@@ -0,0 +1,103 @@
+# 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.
+
+import io
+
+from oslo_log import log as logging
+
+from tempest.api.compute import base as compute_base
+from tempest.api.image import base as image_base
+from tempest.common import utils
+from tempest.common import waiters
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+from tempest.scenario import manager
+
+CONF = config.CONF
+LOG = logging.getLogger(__name__)
+
+
+class ImageDependencyTests(image_base.BaseV2ImageTest,
+ compute_base.BaseV2ComputeTest,
+ manager.ScenarioTest):
+ """Test image, instance, and snapshot dependency.
+
+ The tests create image and remove the base image that other snapshots
+ were depend on.In OpenStack, images and snapshots should be separate,
+ but in some configurations like Glance with Ceph storage,
+ there were cases where images couldn't be removed.
+ This was fixed in glance store for RBD backend.
+
+ * Dependency scenarios:
+ - image > instance -> snapshot dependency
+
+ NOTE: volume -> image dependencies tests are in cinder-tempest-plugin
+ """
+
+ @classmethod
+ def skip_checks(cls):
+ super(ImageDependencyTests, cls).skip_checks()
+ if not CONF.volume_feature_enabled.enable_volume_image_dep_tests:
+ skip_msg = (
+ "%s Volume/image dependency tests "
+ "not enabled" % (cls.__name__))
+ raise cls.skipException(skip_msg)
+
+ def _create_instance_snapshot(self):
+ """Create instance from image and then snapshot the instance."""
+ # Create image and store data to image
+ image_name = data_utils.rand_name(
+ prefix=CONF.resource_name_prefix,
+ name='image-dependency-test')
+ image = self.create_image(name=image_name,
+ container_format='bare',
+ disk_format='raw',
+ visibility='private')
+ file_content = data_utils.random_bytes()
+ image_file = io.BytesIO(file_content)
+ self.client.store_image_file(image['id'], image_file)
+ waiters.wait_for_image_status(
+ self.client, image['id'], 'active')
+ # Create instance
+ instance = self.create_test_server(
+ name='instance-depend-image',
+ image_id=image['id'],
+ wait_until='ACTIVE')
+ LOG.info("Instance from image is created %s", instance)
+ instance_observed = \
+ self.servers_client.show_server(instance['id'])['server']
+ # Create instance snapshot
+ snapshot_instance = self.create_server_snapshot(
+ server=instance_observed)
+ LOG.info("Instance snapshot is created %s", snapshot_instance)
+ return image['id'], snapshot_instance['id']
+
+ @decorators.idempotent_id('d19b0731-e98e-4103-8b0e-02f651b8f586')
+ @utils.services('compute')
+ def test_nova_image_snapshot_dependency(self):
+ """Test with image > instance > snapshot dependency.
+
+ Create instance snapshot and check if we able to delete base
+ image
+
+ """
+ base_image_id, snapshot_image_id = self._create_instance_snapshot()
+ self.client.delete_image(base_image_id)
+ self.client.wait_for_resource_deletion(base_image_id)
+ images_list = self.client.list_images()['images']
+ fetched_images_id = [img['id'] for img in images_list]
+ self.assertNotIn(base_image_id, fetched_images_id)
+ self.assertIn(snapshot_image_id, fetched_images_id)
diff --git a/tempest/common/utils/linux/remote_client.py b/tempest/common/utils/linux/remote_client.py
index 0d93430..dd18190 100644
--- a/tempest/common/utils/linux/remote_client.py
+++ b/tempest/common/utils/linux/remote_client.py
@@ -182,6 +182,9 @@
def umount(self, mount_path='/mnt'):
self.exec_command('sudo umount %s' % mount_path)
+ def mkdir(self, dir_path):
+ self.exec_command('sudo mkdir -p %s' % dir_path)
+
def make_fs(self, dev_name, fs='ext4'):
cmd_mkfs = 'sudo mkfs -t %s /dev/%s' % (fs, dev_name)
try:
diff --git a/tempest/common/waiters.py b/tempest/common/waiters.py
index d65b491..e249f35 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -103,7 +103,8 @@
old_task_state = task_state
-def wait_for_server_termination(client, server_id, ignore_error=False):
+def wait_for_server_termination(client, server_id, ignore_error=False,
+ request_id=None):
"""Waits for server to reach termination."""
try:
body = client.show_server(server_id)['server']
@@ -126,9 +127,13 @@
'/'.join((server_status, str(task_state))),
time.time() - start_time)
if server_status == 'ERROR' and not ignore_error:
- raise lib_exc.DeleteErrorException(
- "Server %s failed to delete and is in ERROR status" %
- server_id)
+ details = ("Server %s failed to delete and is in ERROR status." %
+ server_id)
+ if 'fault' in body:
+ details += ' Fault: %s.' % body['fault']
+ if request_id:
+ details += ' Server delete request ID: %s.' % request_id
+ raise lib_exc.DeleteErrorException(details, server_id=server_id)
if server_status == 'SOFT_DELETED':
# Soft-deleted instances need to be forcibly deleted to
diff --git a/tempest/config.py b/tempest/config.py
index fc50db5..4b5330a 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -1071,7 +1071,10 @@
default=None,
help='Volume types used for data volumes. Multiple volume '
'types can be assigned.'),
-
+ cfg.BoolOpt('enable_volume_image_dep_tests',
+ default=True,
+ help='Run tests for dependencies between images, volumes'
+ 'and instance snapshots')
]
@@ -1176,7 +1179,7 @@
default='icmp',
choices=('icmp', 'tcp', 'udp'),
help='The protocol used in security groups tests to check '
- 'connectivity.'),
+ 'connectivity.')
]
diff --git a/tempest/lib/services/placement/placement_client.py b/tempest/lib/services/placement/placement_client.py
index 216ac08..f272cbf 100644
--- a/tempest/lib/services/placement/placement_client.py
+++ b/tempest/lib/services/placement/placement_client.py
@@ -49,3 +49,39 @@
self.expected_success(200, resp.status)
body = json.loads(body)
return rest_client.ResponseBody(resp, body)
+
+ def list_traits(self, **params):
+ """API ref https://docs.openstack.org/api-ref/placement/#traits
+ """
+ url = "/traits"
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def show_trait(self, name, **params):
+ url = "/traits"
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
+ url = f"{url}/{name}"
+ resp, _ = self.get(url)
+ self.expected_success(204, resp.status)
+ return resp.status
+
+ def create_trait(self, name, **params):
+ url = f"/traits/{name}"
+ json_body = json.dumps(params)
+ resp, _ = self.put(url, body=json_body)
+ return resp.status
+
+ def delete_trait(self, name):
+ url = f"/traits/{name}"
+ resp, _ = self.delete(url)
+ self.expected_success(204, resp.status)
+ return resp.status
diff --git a/tempest/lib/services/placement/resource_providers_client.py b/tempest/lib/services/placement/resource_providers_client.py
index 3214053..a336500 100644
--- a/tempest/lib/services/placement/resource_providers_client.py
+++ b/tempest/lib/services/placement/resource_providers_client.py
@@ -121,3 +121,29 @@
resp, body = self.delete(url)
self.expected_success(204, resp.status)
return rest_client.ResponseBody(resp, body)
+
+ def list_resource_provider_traits(self, rp_uuid, **kwargs):
+ """https://docs.openstack.org/api-ref/placement/#resource-provider-traits
+ """
+ url = f"/resource_providers/{rp_uuid}/traits"
+ if kwargs:
+ url += '?%s' % urllib.urlencode(kwargs)
+ resp, body = self.get(url)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def update_resource_provider_traits(self, rp_uuid, **kwargs):
+ url = f"/resource_providers/{rp_uuid}/traits"
+ data = json.dumps(kwargs)
+ resp, body = self.put(url, data)
+ self.expected_success(200, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
+
+ def delete_resource_provider_traits(self, rp_uuid):
+ url = f"/resource_providers/{rp_uuid}/traits"
+ resp, body = self.delete(url)
+ self.expected_success(204, resp.status)
+ body = json.loads(body)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index d51e7e5..714a7c7 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -1229,16 +1229,18 @@
# dev_name to mount_path.
target_dir = '/tmp'
if dev_name is not None:
+ mount_path = os.path.join(mount_path, dev_name)
ssh_client.make_fs(dev_name, fs=fs)
- ssh_client.exec_command('sudo mount /dev/%s %s' % (dev_name,
- mount_path))
+ ssh_client.mkdir(mount_path)
+ ssh_client.mount(dev_name, mount_path)
target_dir = mount_path
+
cmd_timestamp = 'sudo sh -c "date > %s/timestamp; sync"' % target_dir
ssh_client.exec_command(cmd_timestamp)
timestamp = ssh_client.exec_command('sudo cat %s/timestamp'
% target_dir)
if dev_name is not None:
- ssh_client.exec_command('sudo umount %s' % mount_path)
+ ssh_client.umount(mount_path)
return timestamp
def get_timestamp(self, ip_address, dev_name=None, mount_path='/mnt',
@@ -1266,12 +1268,14 @@
# dev_name to mount_path.
target_dir = '/tmp'
if dev_name is not None:
+ mount_path = os.path.join(mount_path, dev_name)
+ ssh_client.mkdir(mount_path)
ssh_client.mount(dev_name, mount_path)
target_dir = mount_path
timestamp = ssh_client.exec_command('sudo cat %s/timestamp'
% target_dir)
if dev_name is not None:
- ssh_client.exec_command('sudo umount %s' % mount_path)
+ ssh_client.umount(mount_path)
return timestamp
def get_server_ip(self, server, **kwargs):
diff --git a/tempest/scenario/test_instances_with_cinder_volumes.py b/tempest/scenario/test_instances_with_cinder_volumes.py
index 5f33b49..b9ac2c8 100644
--- a/tempest/scenario/test_instances_with_cinder_volumes.py
+++ b/tempest/scenario/test_instances_with_cinder_volumes.py
@@ -181,31 +181,24 @@
server=server
)
+ server_name = server['name'].split('-')[-1]
+
# 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
+ dev_name = volume['attachments'][0]['device'][5:]
+
+ mount_path = f"/mnt/{server_name}"
+
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
+ dev_name=dev_name, 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
+ dev_name=dev_name, mount_path=mount_path,
)
self.assertEqual(timestamp_before, timestamp_after)
diff --git a/tempest/tests/lib/services/placement/test_placement_client.py b/tempest/tests/lib/services/placement/test_placement_client.py
index 1396a85..bb57bb0 100644
--- a/tempest/tests/lib/services/placement/test_placement_client.py
+++ b/tempest/tests/lib/services/placement/test_placement_client.py
@@ -87,3 +87,77 @@
def test_list_allocations_with_bytes_body(self):
self._test_list_allocations(bytes_body=True)
+
+ FAKE_ALL_TRAITS = {
+ "traits": [
+ "CUSTOM_HW_FPGA_CLASS1",
+ "CUSTOM_HW_FPGA_CLASS2",
+ "CUSTOM_HW_FPGA_CLASS3"
+ ]
+ }
+
+ FAKE_ASSOCIATED_TRAITS = {
+ "traits": [
+ "CUSTOM_HW_FPGA_CLASS1",
+ "CUSTOM_HW_FPGA_CLASS2"
+ ]
+ }
+
+ def test_list_traits(self):
+ self.check_service_client_function(
+ self.client.list_traits,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_ALL_TRAITS)
+
+ self.check_service_client_function(
+ self.client.list_traits,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_ASSOCIATED_TRAITS,
+ **{
+ "associated": "true"
+ })
+
+ self.check_service_client_function(
+ self.client.list_traits,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_ALL_TRAITS,
+ **{
+ "associated": "true",
+ "name": "startswith:CUSTOM_HW_FPGPA"
+ })
+
+ def test_show_traits(self):
+ self.check_service_client_function(
+ self.client.show_trait,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ 204, status=204,
+ name="CUSTOM_HW_FPGA_CLASS1")
+
+ self.check_service_client_function(
+ self.client.show_trait,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ 404, status=404,
+ # trait with this name does not exists
+ name="CUSTOM_HW_FPGA_CLASS4")
+
+ def test_create_traits(self):
+ self.check_service_client_function(
+ self.client.create_trait,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ 204, status=204,
+ # try to create trait with existing name
+ name="CUSTOM_HW_FPGA_CLASS1")
+
+ self.check_service_client_function(
+ self.client.create_trait,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ 201, status=201,
+ # create new trait
+ name="CUSTOM_HW_FPGA_CLASS4")
+
+ def test_delete_traits(self):
+ self.check_service_client_function(
+ self.client.delete_trait,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ 204, status=204,
+ name="CUSTOM_HW_FPGA_CLASS1")
diff --git a/tempest/tests/lib/services/placement/test_resource_providers_client.py b/tempest/tests/lib/services/placement/test_resource_providers_client.py
index 2871395..399f323 100644
--- a/tempest/tests/lib/services/placement/test_resource_providers_client.py
+++ b/tempest/tests/lib/services/placement/test_resource_providers_client.py
@@ -204,3 +204,40 @@
def test_show_resource_provider_usages_with_with_bytes_body(self):
self._test_list_resource_provider_inventories(bytes_body=True)
+
+ FAKE_ALL_RESOURCE_PROVIDER_TRAITS = {
+ "resource_provider_generation": 0,
+ "traits": [
+ "CUSTOM_HW_FPGA_CLASS1",
+ "CUSTOM_HW_FPGA_CLASS2"
+ ]
+ }
+ FAKE_NEW_RESOURCE_PROVIDER_TRAITS = {
+ "resource_provider_generation": 1,
+ "traits": [
+ "CUSTOM_HW_FPGA_CLASS1",
+ "CUSTOM_HW_FPGA_CLASS2"
+ ]
+ }
+
+ def test_list_resource_provider_traits(self):
+ self.check_service_client_function(
+ self.client.list_resource_provider_traits,
+ 'tempest.lib.common.rest_client.RestClient.get',
+ self.FAKE_ALL_RESOURCE_PROVIDER_TRAITS,
+ rp_uuid=self.FAKE_RESOURCE_PROVIDER_UUID)
+
+ def test_update_resource_provider_traits(self):
+ self.check_service_client_function(
+ self.client.update_resource_provider_traits,
+ 'tempest.lib.common.rest_client.RestClient.put',
+ self.FAKE_NEW_RESOURCE_PROVIDER_TRAITS,
+ rp_uuid=self.FAKE_RESOURCE_PROVIDER_UUID,
+ **self.FAKE_NEW_RESOURCE_PROVIDER_TRAITS)
+
+ def test_delete_resource_provider_traits(self):
+ self.check_service_client_function(
+ self.client.delete_resource_provider_traits,
+ 'tempest.lib.common.rest_client.RestClient.delete',
+ self.FAKE_ALL_RESOURCE_PROVIDER_TRAITS, status=204,
+ rp_uuid=self.FAKE_RESOURCE_PROVIDER_UUID)