Merge "Update server details before getting fixed IP"
diff --git a/releasenotes/notes/tempest-zed-release-335293c4a7f5a4b1.yaml b/releasenotes/notes/tempest-zed-release-335293c4a7f5a4b1.yaml
new file mode 100644
index 0000000..841aa5d
--- /dev/null
+++ b/releasenotes/notes/tempest-zed-release-335293c4a7f5a4b1.yaml
@@ -0,0 +1,17 @@
+---
+prelude: |
+    This release is to tag Tempest for OpenStack Zed release.
+    This release marks the start of Zed release support in Tempest.
+    After this release, Tempest will support below OpenStack Releases:
+
+    * Zed
+    * Yoga
+    * Xena
+    * Wallaby
+
+    Current development of Tempest is for OpenStack 2023.1 development
+    cycle. Every Tempest commit is also tested against master during
+    the 2023.1 cycle. However, this does not necessarily mean that using
+    Tempest as of this tag will work against a 2023.1 (or future release)
+    cloud.
+    To be on safe side, use this tag to test the OpenStack Zed release.
diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst
index 9b5aad3..b36be01 100644
--- a/releasenotes/source/index.rst
+++ b/releasenotes/source/index.rst
@@ -6,6 +6,7 @@
    :maxdepth: 1
 
    unreleased
+   v32.0.0
    v31.1.0
    v31.0.0
    v30.0.0
diff --git a/releasenotes/source/v32.0.0.rst b/releasenotes/source/v32.0.0.rst
new file mode 100644
index 0000000..e4c2cea
--- /dev/null
+++ b/releasenotes/source/v32.0.0.rst
@@ -0,0 +1,5 @@
+=====================
+v32.0.0 Release Notes
+=====================
+.. release-notes:: 32.0.0 Release Notes
+   :version: 32.0.0
diff --git a/tempest/api/compute/admin/test_servers_on_multinodes.py b/tempest/api/compute/admin/test_servers_on_multinodes.py
index f440428..9082306 100644
--- a/tempest/api/compute/admin/test_servers_on_multinodes.py
+++ b/tempest/api/compute/admin/test_servers_on_multinodes.py
@@ -16,6 +16,7 @@
 
 from tempest.api.compute import base
 from tempest.common import compute
+from tempest.common import waiters
 from tempest import config
 from tempest.lib import decorators
 
@@ -125,3 +126,47 @@
         hostnames = list(hosts.values())
         self.assertEqual(hostnames[0], hostnames[1],
                          'Servers are on the different hosts: %s' % hosts)
+
+
+class UnshelveToHostMultiNodesTest(base.BaseV2ComputeAdminTest):
+    """Test to unshelve server in between hosts."""
+    min_microversion = '2.91'
+    max_microversion = 'latest'
+
+    @classmethod
+    def skip_checks(cls):
+        super(UnshelveToHostMultiNodesTest, cls).skip_checks()
+
+        if CONF.compute.min_compute_nodes < 2:
+            raise cls.skipException(
+                "Less than 2 compute nodes, skipping multi-nodes test.")
+
+    def _shelve_offload_then_unshelve_to_host(self, server, host):
+        compute.shelve_server(self.servers_client, server['id'],
+                              force_shelve_offload=True)
+
+        self.os_admin.servers_client.unshelve_server(
+            server['id'],
+            body={'unshelve': {'host': host}}
+            )
+        waiters.wait_for_server_status(self.servers_client, server['id'],
+                                       'ACTIVE')
+
+    @decorators.idempotent_id('b5cc0889-50c2-46a0-b8ff-b5fb4c3a6e20')
+    def test_unshelve_to_specific_host(self):
+        """Test unshelve to a specific host, new behavior introduced in
+        microversion 2.91.
+        1. Shelve offload server.
+        2. Request unshelve to original host and verify server land on it.
+        3. Shelve offload server again.
+        4. Request unshelve to the other host and verify server land on it.
+        """
+        server = self.create_test_server(wait_until='ACTIVE')
+        host = self.get_host_for_server(server['id'])
+        otherhost = self.get_host_other_than(server['id'])
+
+        self._shelve_offload_then_unshelve_to_host(server, host)
+        self.assertEqual(host, self.get_host_for_server(server['id']))
+
+        self._shelve_offload_then_unshelve_to_host(server, otherhost)
+        self.assertEqual(otherhost, self.get_host_for_server(server['id']))
diff --git a/tempest/api/image/v2/test_images.py b/tempest/api/image/v2/test_images.py
index 96031ac..7e647dd 100644
--- a/tempest/api/image/v2/test_images.py
+++ b/tempest/api/image/v2/test_images.py
@@ -49,12 +49,12 @@
             raise cls.skipException('Server does not support '
                                     'any import method')
 
-    def _create_image(self):
+    def _create_image(self, disk_format=None, container_format=None):
         # Create image
         uuid = '00000000-1111-2222-3333-444455556666'
         image_name = data_utils.rand_name('image')
-        container_format = CONF.image.container_formats[0]
-        disk_format = CONF.image.disk_formats[0]
+        container_format = container_format or CONF.image.container_formats[0]
+        disk_format = disk_format or CONF.image.disk_formats[0]
         image = self.create_image(name=image_name,
                                   container_format=container_format,
                                   disk_format=disk_format,
@@ -134,6 +134,141 @@
                                  import_params={'uri': image_uri})
         waiters.wait_for_image_imported_to_stores(self.client, image['id'])
 
+    @decorators.idempotent_id('8876c818-c40e-4b90-9742-31d231616305')
+    def test_image_glance_download_import_success(self):
+        # We use glance-direct initially, then glance-download for test
+        self._require_import_method('glance-direct')
+        self._require_import_method('glance-download')
+
+        # Create an image via the normal import process to be our source
+        src = self._stage_and_check()
+        self.client.image_import(src, method='glance-direct')
+        waiters.wait_for_image_imported_to_stores(self.client, src)
+
+        # Add some properties to it that will be copied by the default
+        # config (and one that won't)
+        self.client.update_image(src, [
+            {'add': '/hw_cpu_cores', 'value': '5'},
+            {'add': '/trait:STORAGE_DISK_SSD', 'value': 'required'},
+            {'add': '/os_distro', 'value': 'rhel'},
+            {'add': '/speed', 'value': '88mph'},
+        ])
+
+        # Make sure our properties stuck on the source image
+        src_image = self.client.show_image(src)
+        self.assertEqual('5', src_image['hw_cpu_cores'])
+        self.assertEqual('required', src_image['trait:STORAGE_DISK_SSD'])
+        self.assertEqual('rhel', src_image['os_distro'])
+        self.assertEqual('88mph', src_image['speed'])
+
+        # Create a new image which we will fill from another glance image
+        dst = self._create_image(container_format='ovf',
+                                 disk_format='iso')['id']
+
+        # Set some values that will conflict to make sure we get the
+        # new ones and confirm they stuck before the import.
+        self.client.update_image(dst, [
+            {'add': '/hw_cpu_cores', 'value': '1'},
+            {'add': '/os_distro', 'value': 'windows'},
+        ])
+        dst_image = self.client.show_image(dst)
+        self.assertEqual('1', dst_image['hw_cpu_cores'])
+        self.assertEqual('windows', dst_image['os_distro'])
+
+        params = {
+            'glance_image_id': src,
+            'glance_region': self.client.region,
+            'glance_service_interface': 'public',
+        }
+        self.client.image_import(dst, method='glance-download',
+                                 import_params=params)
+        waiters.wait_for_image_tasks_status(self.client, dst, 'success')
+
+        # Make sure the new image has all the keys imported from the
+        # original image that we expect
+        dst_image = self.client.show_image(dst)
+        self.assertEqual(src_image['disk_format'], dst_image['disk_format'])
+        self.assertEqual(src_image['container_format'],
+                         dst_image['container_format'])
+        self.assertEqual('5', dst_image['hw_cpu_cores'])
+        self.assertEqual('required', dst_image['trait:STORAGE_DISK_SSD'])
+        self.assertEqual('rhel', dst_image['os_distro'])
+        self.assertNotIn('speed', dst_image)
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('36d4b546-64a2-4bb9-bdd0-ba676aa48f2c')
+    def test_image_glance_download_import_bad_uuid(self):
+        self._require_import_method('glance-download')
+        image_id = self._create_image()['id']
+        params = {
+            'glance_image_id': 'foo',
+            'glance_region': self.client.region,
+            'glance_service_interface': 'public',
+        }
+
+        # A non-UUID-like image id should make us fail immediately
+        e = self.assertRaises(lib_exc.BadRequest,
+                              self.client.image_import,
+                              image_id, method='glance-download',
+                              import_params=params)
+        self.assertIn('image id does not look like a UUID', str(e))
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('77644240-dbbe-4744-ae28-09b2ac12e218')
+    def test_image_glance_download_import_bad_endpoint(self):
+        self._require_import_method('glance-download')
+        image_id = self._create_image()['id']
+
+        # Set some properties before the import to make sure they are
+        # undisturbed
+        self.client.update_image(image_id, [
+            {'add': '/hw_cpu_cores', 'value': '1'},
+            {'add': '/os_distro', 'value': 'windows'},
+        ])
+        image = self.client.show_image(image_id)
+        self.assertEqual('1', image['hw_cpu_cores'])
+        self.assertEqual('windows', image['os_distro'])
+
+        params = {
+            'glance_image_id': '36d4b546-64a2-4bb9-bdd0-ba676aa48f2c',
+            'glance_region': 'not a region',
+            'glance_service_interface': 'not an interface',
+        }
+
+        # A bad region or interface will cause us to fail when we
+        # contact the remote glance.
+        self.client.image_import(image_id, method='glance-download',
+                                 import_params=params)
+        waiters.wait_for_image_tasks_status(self.client, image_id, 'failure')
+
+        # Make sure we reverted the image status to queued on failure, and that
+        # our extra properties are still in place.
+        image = self.client.show_image(image_id)
+        self.assertEqual('queued', image['status'])
+        self.assertEqual('1', image['hw_cpu_cores'])
+        self.assertEqual('windows', image['os_distro'])
+
+    @decorators.attr(type=['negative'])
+    @decorators.idempotent_id('c7edec8e-24b5-416a-9d42-b3e773bab62c')
+    def test_image_glance_download_import_bad_missing_image(self):
+        self._require_import_method('glance-download')
+        image_id = self._create_image()['id']
+        params = {
+            'glance_image_id': '36d4b546-64a2-4bb9-bdd0-ba676aa48f2c',
+            'glance_region': self.client.region,
+            'glance_service_interface': 'public',
+        }
+
+        # A non-existent image will cause us to fail when we
+        # contact the remote glance.
+        self.client.image_import(image_id, method='glance-download',
+                                 import_params=params)
+        waiters.wait_for_image_tasks_status(self.client, image_id, 'failure')
+
+        # Make sure we reverted the image status to queued on failure
+        image = self.client.show_image(image_id)
+        self.assertEqual('queued', image['status'])
+
     @decorators.idempotent_id('e04761a1-22af-42c2-b8bc-a34a3f12b585')
     def test_remote_import(self):
         """Test image import against a different worker than stage.
diff --git a/tempest/api/object_storage/test_object_services.py b/tempest/api/object_storage/test_object_services.py
index a11bed8..7d5bd26 100644
--- a/tempest/api/object_storage/test_object_services.py
+++ b/tempest/api/object_storage/test_object_services.py
@@ -186,12 +186,10 @@
         """Test creating object with transfer_encoding"""
         object_name = data_utils.rand_name(name='TestObject')
         data = data_utils.random_bytes(1024)
-        headers = {'Transfer-Encoding': 'chunked'}
         resp, _ = self.object_client.create_object(
             self.container_name,
             object_name,
             data=data_utils.chunkify(data, 512),
-            headers=headers,
             chunked=True)
 
         self.assertHeaders(resp, 'Object', 'PUT')
diff --git a/tempest/clients.py b/tempest/clients.py
index b7fa54a..a65c43b 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -118,7 +118,6 @@
             enable_instance_password=eip)
         self.server_groups_client = self.compute.ServerGroupsClient()
         self.limits_client = self.compute.LimitsClient()
-        self.compute_images_client = self.compute.ImagesClient()
         self.keypairs_client = self.compute.KeyPairsClient(
             ssh_key_type=CONF.validation.ssh_key_type)
         self.quotas_client = self.compute.QuotasClient()
@@ -158,6 +157,8 @@
             **params_volume)
         self.snapshots_extensions_client = self.compute.SnapshotsClient(
             **params_volume)
+        self.compute_images_client = self.compute.ImagesClient(
+            build_timeout=CONF.image.build_timeout)
 
     def _set_placement_clients(self):
         self.placement_client = self.placement.PlacementClient()
diff --git a/tempest/common/compute.py b/tempest/common/compute.py
index 3711834..00f133e 100644
--- a/tempest/common/compute.py
+++ b/tempest/common/compute.py
@@ -209,13 +209,6 @@
                                    kwargs.get('max_count', 0)) > 1)
 
     if CONF.validation.run_validation and validatable:
-        # As a first implementation, multiple pingable or sshable servers will
-        # not be supported
-        if multiple_create_request:
-            msg = ("Multiple pingable or sshable servers not supported at "
-                   "this stage.")
-            raise ValueError(msg)
-
         LOG.debug("Provisioning test server with validation resources %s",
                   validation_resources)
         if 'security_groups' in kwargs:
diff --git a/tempest/lib/common/dynamic_creds.py b/tempest/lib/common/dynamic_creds.py
index be8c0e8..d687eb5 100644
--- a/tempest/lib/common/dynamic_creds.py
+++ b/tempest/lib/common/dynamic_creds.py
@@ -559,23 +559,24 @@
             except lib_exc.NotFound:
                 LOG.warning("user with name: %s not found for delete",
                             creds.username)
-            # NOTE(zhufl): Only when neutron's security_group ext is
-            # enabled, cleanup_default_secgroup will not raise error. But
-            # here cannot use test_utils.is_extension_enabled for it will cause
-            # "circular dependency". So here just use try...except to
-            # ensure tenant deletion without big changes.
-            try:
-                if self.neutron_available:
-                    self.cleanup_default_secgroup(
-                        self.security_groups_admin_client, creds.tenant_id)
-            except lib_exc.NotFound:
-                LOG.warning("failed to cleanup tenant %s's secgroup",
-                            creds.tenant_name)
-            try:
-                self.creds_client.delete_project(creds.tenant_id)
-            except lib_exc.NotFound:
-                LOG.warning("tenant with name: %s not found for delete",
-                            creds.tenant_name)
+            if creds.tenant_id:
+                # NOTE(zhufl): Only when neutron's security_group ext is
+                # enabled, cleanup_default_secgroup will not raise error. But
+                # here cannot use test_utils.is_extension_enabled for it will
+                # cause "circular dependency". So here just use try...except to
+                # ensure tenant deletion without big changes.
+                try:
+                    if self.neutron_available:
+                        self.cleanup_default_secgroup(
+                            self.security_groups_admin_client, creds.tenant_id)
+                except lib_exc.NotFound:
+                    LOG.warning("failed to cleanup tenant %s's secgroup",
+                                creds.tenant_name)
+                try:
+                    self.creds_client.delete_project(creds.tenant_id)
+                except lib_exc.NotFound:
+                    LOG.warning("tenant with name: %s not found for delete",
+                                creds.tenant_name)
 
             # if cred is domain scoped, delete ephemeral domain
             # do not delete default domain
diff --git a/zuul.d/project.yaml b/zuul.d/project.yaml
index 3cc3fda..1432180 100644
--- a/zuul.d/project.yaml
+++ b/zuul.d/project.yaml
@@ -34,6 +34,8 @@
         - glance-multistore-cinder-import:
             voting: false
             irrelevant-files: *tempest-irrelevant-files
+        - tempest-full-zed:
+            irrelevant-files: *tempest-irrelevant-files
         - tempest-full-yoga:
             irrelevant-files: *tempest-irrelevant-files
         - tempest-full-xena:
@@ -168,9 +170,11 @@
             irrelevant-files: *tempest-irrelevant-files
     periodic-stable:
       jobs:
+        - tempest-full-zed
         - tempest-full-yoga
         - tempest-full-xena
         - tempest-full-wallaby-py3
+        - tempest-slow-zed
         - tempest-slow-yoga
         - tempest-slow-xena
         - tempest-slow-wallaby
diff --git a/zuul.d/stable-jobs.yaml b/zuul.d/stable-jobs.yaml
index d1445c0..6d97fad 100644
--- a/zuul.d/stable-jobs.yaml
+++ b/zuul.d/stable-jobs.yaml
@@ -1,5 +1,10 @@
 # NOTE(gmann): This file includes all stable release jobs definition.
 - job:
+    name: tempest-full-zed
+    parent: tempest-full-py3
+    override-checkout: stable/zed
+
+- job:
     name: tempest-full-yoga
     parent: tempest-full-py3
     override-checkout: stable/yoga
@@ -15,6 +20,11 @@
     override-checkout: stable/wallaby
 
 - job:
+    name: tempest-slow-zed
+    parent: tempest-slow-py3
+    override-checkout: stable/zed
+
+- job:
     name: tempest-slow-yoga
     parent: tempest-slow-py3
     override-checkout: stable/yoga