lkuchlan | 3bd6e27 | 2018-01-25 11:16:10 +0200 | [diff] [blame] | 1 | # Copyright 2018 Red Hat, Inc. |
| 2 | # All Rights Reserved. |
| 3 | # |
| 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may |
| 5 | # not use this file except in compliance with the License. You may obtain |
| 6 | # a copy of the License at |
| 7 | # |
| 8 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | # |
| 10 | # Unless required by applicable law or agreed to in writing, software |
| 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 13 | # License for the specific language governing permissions and limitations |
| 14 | # under the License. |
| 15 | |
songwenping | 8c3dac1 | 2021-02-22 09:12:37 +0800 | [diff] [blame] | 16 | import io |
Ghanshyam Mann | 4346a82 | 2020-07-29 13:45:04 -0500 | [diff] [blame] | 17 | |
lkuchlan | 3bd6e27 | 2018-01-25 11:16:10 +0200 | [diff] [blame] | 18 | from tempest.api.image import base |
Ghanshyam Mann | 4346a82 | 2020-07-29 13:45:04 -0500 | [diff] [blame] | 19 | from tempest.common import waiters |
| 20 | from tempest import config |
lkuchlan | 3bd6e27 | 2018-01-25 11:16:10 +0200 | [diff] [blame] | 21 | from tempest.lib.common.utils import data_utils |
| 22 | from tempest.lib import decorators |
Ghanshyam Mann | bd97ae9 | 2023-05-19 14:08:03 -0500 | [diff] [blame] | 23 | from tempest.lib import exceptions as lib_exc |
lkuchlan | 3bd6e27 | 2018-01-25 11:16:10 +0200 | [diff] [blame] | 24 | |
Ghanshyam Mann | 4346a82 | 2020-07-29 13:45:04 -0500 | [diff] [blame] | 25 | CONF = config.CONF |
| 26 | |
lkuchlan | 3bd6e27 | 2018-01-25 11:16:10 +0200 | [diff] [blame] | 27 | |
| 28 | class BasicOperationsImagesAdminTest(base.BaseV2ImageAdminTest): |
zhufl | e68f435 | 2020-04-21 13:44:55 +0800 | [diff] [blame] | 29 | """"Test image operations about image owner""" |
lkuchlan | 3bd6e27 | 2018-01-25 11:16:10 +0200 | [diff] [blame] | 30 | |
| 31 | @decorators.related_bug('1420008') |
| 32 | @decorators.idempotent_id('646a6eaa-135f-4493-a0af-12583021224e') |
| 33 | def test_create_image_owner_param(self): |
zhufl | e68f435 | 2020-04-21 13:44:55 +0800 | [diff] [blame] | 34 | """Test creating image with specified owner""" |
lkuchlan | 3bd6e27 | 2018-01-25 11:16:10 +0200 | [diff] [blame] | 35 | # NOTE: Create image with owner different from tenant owner by |
| 36 | # using "owner" parameter requires an admin privileges. |
| 37 | random_id = data_utils.rand_uuid_hex() |
| 38 | image = self.admin_client.create_image( |
| 39 | container_format='bare', disk_format='raw', owner=random_id) |
| 40 | self.addCleanup(self.admin_client.delete_image, image['id']) |
| 41 | image_info = self.admin_client.show_image(image['id']) |
| 42 | self.assertEqual(random_id, image_info['owner']) |
lkuchlan | f8ff1ff | 2018-02-07 11:29:54 +0200 | [diff] [blame] | 43 | |
| 44 | @decorators.related_bug('1420008') |
| 45 | @decorators.idempotent_id('525ba546-10ef-4aad-bba1-1858095ce553') |
| 46 | def test_update_image_owner_param(self): |
zhufl | e68f435 | 2020-04-21 13:44:55 +0800 | [diff] [blame] | 47 | """Test updating image owner""" |
lkuchlan | f8ff1ff | 2018-02-07 11:29:54 +0200 | [diff] [blame] | 48 | random_id_1 = data_utils.rand_uuid_hex() |
| 49 | image = self.admin_client.create_image( |
| 50 | container_format='bare', disk_format='raw', owner=random_id_1) |
| 51 | self.addCleanup(self.admin_client.delete_image, image['id']) |
| 52 | created_image_info = self.admin_client.show_image(image['id']) |
| 53 | |
| 54 | random_id_2 = data_utils.rand_uuid_hex() |
| 55 | self.admin_client.update_image( |
| 56 | image['id'], [dict(replace="/owner", value=random_id_2)]) |
| 57 | updated_image_info = self.admin_client.show_image(image['id']) |
| 58 | |
| 59 | self.assertEqual(random_id_2, updated_image_info['owner']) |
| 60 | self.assertNotEqual(created_image_info['owner'], |
| 61 | updated_image_info['owner']) |
Ghanshyam Mann | 4346a82 | 2020-07-29 13:45:04 -0500 | [diff] [blame] | 62 | |
Maxim Sava | 9db106a | 2023-06-18 15:49:03 +0300 | [diff] [blame] | 63 | @decorators.idempotent_id('f6ab4aa0-035e-4664-9f2d-c57c6df50605') |
| 64 | def test_list_public_image(self): |
| 65 | """Test create image as admin and list public image as none admin""" |
Martin Kopec | 213d0a4 | 2023-11-30 10:28:14 +0100 | [diff] [blame] | 66 | name = data_utils.rand_name( |
| 67 | prefix=CONF.resource_name_prefix, |
| 68 | name=self.__class__.__name__ + '-Image') |
Maxim Sava | 9db106a | 2023-06-18 15:49:03 +0300 | [diff] [blame] | 69 | image = self.admin_client.create_image( |
| 70 | name=name, |
| 71 | container_format='bare', |
| 72 | visibility='public', |
| 73 | disk_format='raw') |
| 74 | waiters.wait_for_image_status(self.admin_client, image['id'], 'queued') |
| 75 | created_image = self.admin_client.show_image(image['id']) |
| 76 | self.assertEqual(image['id'], created_image['id']) |
| 77 | self.addCleanup(self.admin_client.delete_image, image['id']) |
| 78 | |
| 79 | images_list = self.client.list_images()['images'] |
| 80 | fetched_images_id = [img['id'] for img in images_list] |
| 81 | self.assertIn(image['id'], fetched_images_id) |
| 82 | |
Ghanshyam Mann | 4346a82 | 2020-07-29 13:45:04 -0500 | [diff] [blame] | 83 | |
| 84 | class ImportCopyImagesTest(base.BaseV2ImageAdminTest): |
| 85 | """Test the import copy-image operations""" |
| 86 | |
| 87 | @classmethod |
| 88 | def skip_checks(cls): |
| 89 | super(ImportCopyImagesTest, cls).skip_checks() |
| 90 | if not CONF.image_feature_enabled.import_image: |
| 91 | skip_msg = ( |
| 92 | "%s skipped as image import is not available" % cls.__name__) |
| 93 | raise cls.skipException(skip_msg) |
| 94 | |
| 95 | @decorators.idempotent_id('9b3b644e-03d1-11eb-a036-fa163e2eaf49') |
| 96 | def test_image_copy_image_import(self): |
| 97 | """Test 'copy-image' import functionalities |
| 98 | |
| 99 | Create image, import image with copy-image method and |
| 100 | verify that import succeeded. |
| 101 | """ |
| 102 | available_stores = self.get_available_stores() |
| 103 | available_import_methods = self.client.info_import()[ |
| 104 | 'import-methods']['value'] |
| 105 | # NOTE(gmann): Skip if copy-image import method and multistore |
| 106 | # are not available. |
| 107 | if ('copy-image' not in available_import_methods or |
| 108 | not available_stores): |
| 109 | raise self.skipException('Either copy-image import method or ' |
| 110 | 'multistore is not available') |
| 111 | uuid = data_utils.rand_uuid() |
Martin Kopec | 213d0a4 | 2023-11-30 10:28:14 +0100 | [diff] [blame] | 112 | image_name = data_utils.rand_name( |
| 113 | prefix=CONF.resource_name_prefix, name='copy-image') |
Ghanshyam Mann | 4346a82 | 2020-07-29 13:45:04 -0500 | [diff] [blame] | 114 | container_format = CONF.image.container_formats[0] |
| 115 | disk_format = CONF.image.disk_formats[0] |
| 116 | image = self.create_image(name=image_name, |
| 117 | container_format=container_format, |
| 118 | disk_format=disk_format, |
| 119 | visibility='private', |
| 120 | ramdisk_id=uuid) |
| 121 | self.assertEqual('queued', image['status']) |
| 122 | |
| 123 | file_content = data_utils.random_bytes() |
songwenping | 8c3dac1 | 2021-02-22 09:12:37 +0800 | [diff] [blame] | 124 | image_file = io.BytesIO(file_content) |
Ghanshyam Mann | 4346a82 | 2020-07-29 13:45:04 -0500 | [diff] [blame] | 125 | self.client.store_image_file(image['id'], image_file) |
| 126 | |
| 127 | body = self.client.show_image(image['id']) |
| 128 | self.assertEqual(image['id'], body['id']) |
| 129 | self.assertEqual(len(file_content), body.get('size')) |
| 130 | self.assertEqual('active', body['status']) |
| 131 | |
| 132 | # Copy image to all the stores. In case of all_stores request |
| 133 | # glance will skip the stores where image is already available. |
| 134 | self.admin_client.image_import(image['id'], method='copy-image', |
| 135 | all_stores=True, |
| 136 | all_stores_must_succeed=False) |
| 137 | |
| 138 | # Wait for copy to finished on all stores. |
| 139 | failed_stores = waiters.wait_for_image_copied_to_stores( |
| 140 | self.client, image['id']) |
| 141 | # Assert if copy is failed on any store. |
| 142 | self.assertEqual(0, len(failed_stores), |
| 143 | "Failed to copy the following stores: %s" % |
| 144 | str(failed_stores)) |
Ghanshyam Mann | bd97ae9 | 2023-05-19 14:08:03 -0500 | [diff] [blame] | 145 | |
| 146 | |
| 147 | class ImageLocationsAdminTest(base.BaseV2ImageAdminTest): |
| 148 | |
| 149 | @classmethod |
| 150 | def skip_checks(cls): |
| 151 | super(ImageLocationsAdminTest, cls).skip_checks() |
| 152 | if not CONF.image_feature_enabled.manage_locations: |
| 153 | skip_msg = ( |
| 154 | "%s skipped as show_multiple_locations is not available" % ( |
| 155 | cls.__name__)) |
| 156 | raise cls.skipException(skip_msg) |
| 157 | |
| 158 | @decorators.idempotent_id('8a648de4-b745-4c28-a7b5-20de1c3da4d2') |
| 159 | def test_delete_locations(self): |
| 160 | image = self.check_set_multiple_locations() |
| 161 | expected_remaining_loc = image['locations'][1] |
| 162 | |
| 163 | self.admin_client.update_image(image['id'], [ |
| 164 | dict(remove='/locations/0')]) |
| 165 | |
| 166 | # The image should now have only the one location we did not delete |
| 167 | image = self.client.show_image(image['id']) |
| 168 | self.assertEqual(1, len(image['locations']), |
| 169 | 'Image should have one location but has %i' % ( |
| 170 | len(image['locations']))) |
| 171 | self.assertEqual(expected_remaining_loc['url'], |
| 172 | image['locations'][0]['url']) |
| 173 | |
| 174 | # The direct_url should now be the last remaining location |
| 175 | if 'direct_url' in image: |
| 176 | self.assertEqual(image['direct_url'], image['locations'][0]['url']) |
| 177 | |
| 178 | # Removing the last location should be disallowed |
| 179 | self.assertRaises(lib_exc.Forbidden, |
| 180 | self.admin_client.update_image, image['id'], [ |
| 181 | dict(remove='/locations/0')]) |
Maxim Sava | bd9cbd3 | 2023-10-17 13:13:33 +0300 | [diff] [blame] | 182 | |
| 183 | |
| 184 | class MultiStoresImagesTest(base.BaseV2ImageAdminTest, base.BaseV2ImageTest): |
| 185 | """Test importing and deleting image in multiple stores""" |
| 186 | @classmethod |
| 187 | def skip_checks(cls): |
| 188 | super(MultiStoresImagesTest, cls).skip_checks() |
| 189 | if not CONF.image_feature_enabled.import_image: |
| 190 | skip_msg = ( |
| 191 | "%s skipped as image import is not available" % cls.__name__) |
| 192 | raise cls.skipException(skip_msg) |
| 193 | |
| 194 | @classmethod |
| 195 | def resource_setup(cls): |
| 196 | super(MultiStoresImagesTest, cls).resource_setup() |
| 197 | cls.available_import_methods = \ |
| 198 | cls.client.info_import()['import-methods']['value'] |
| 199 | if not cls.available_import_methods: |
| 200 | raise cls.skipException('Server does not support ' |
| 201 | 'any import method') |
| 202 | |
| 203 | # NOTE(pdeore): Skip if glance-direct import method and mutlistore |
| 204 | # are not enabled/configured, or only one store is configured in |
| 205 | # multiple stores setup. |
| 206 | cls.available_stores = cls.get_available_stores() |
| 207 | if ('glance-direct' not in cls.available_import_methods or |
| 208 | not len(cls.available_stores) > 1): |
| 209 | raise cls.skipException( |
| 210 | 'Either glance-direct import method not present in %s or ' |
| 211 | 'None or only one store is ' |
| 212 | 'configured %s' % (cls.available_import_methods, |
| 213 | cls.available_stores)) |
| 214 | |
| 215 | @decorators.idempotent_id('1ecec683-41d4-4470-a0df-54969ec74514') |
| 216 | def test_delete_image_from_specific_store(self): |
| 217 | """Test delete image from specific store""" |
| 218 | # Import image to available stores |
| 219 | image, stores = self.create_and_stage_image(all_stores=True) |
| 220 | self.client.image_import(image['id'], |
| 221 | method='glance-direct', |
| 222 | all_stores=True) |
| 223 | self.addCleanup(self.admin_client.delete_image, image['id']) |
| 224 | waiters.wait_for_image_imported_to_stores( |
| 225 | self.client, |
| 226 | image['id'], stores) |
| 227 | observed_image = self.client.show_image(image['id']) |
| 228 | |
| 229 | # Image will be deleted from first store |
| 230 | first_image_store_deleted = (observed_image['stores'].split(","))[0] |
| 231 | self.admin_client.delete_image_from_store( |
| 232 | observed_image['id'], first_image_store_deleted) |
| 233 | waiters.wait_for_image_deleted_from_store( |
| 234 | self.admin_client, |
| 235 | observed_image, |
| 236 | stores, |
| 237 | first_image_store_deleted) |