Merge "Add delete image from specific store API"

This commit is contained in:
Zuul 2024-01-09 21:34:38 +00:00 committed by Gerrit Code Review
commit 01c2e2ff7e
7 changed files with 174 additions and 33 deletions

View File

@ -0,0 +1,4 @@
---
features:
- |
Add delete image from specific store API to image V2 client

View File

@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import io
import time
from tempest import config
@ -95,6 +96,36 @@ class BaseV2ImageTest(BaseImageTest):
namespace_name)
return namespace
def create_and_stage_image(self, all_stores=False):
"""Create Image & stage image file for glance-direct import method."""
image_name = data_utils.rand_name('test-image')
container_format = CONF.image.container_formats[0]
disk_format = CONF.image.disk_formats[0]
image = self.create_image(name=image_name,
container_format=container_format,
disk_format=disk_format,
visibility='private')
self.assertEqual('queued', image['status'])
self.client.stage_image_file(
image['id'],
io.BytesIO(data_utils.random_bytes()))
# Check image status is 'uploading'
body = self.client.show_image(image['id'])
self.assertEqual(image['id'], body['id'])
self.assertEqual('uploading', body['status'])
if all_stores:
stores_list = ','.join([store['id']
for store in self.available_stores
if store.get('read-only') != 'true'])
else:
stores = [store['id'] for store in self.available_stores
if store.get('read-only') != 'true']
stores_list = stores[::max(1, len(stores) - 1)]
return body, stores_list
@classmethod
def get_available_stores(cls):
stores = []

View File

@ -179,3 +179,59 @@ class ImageLocationsAdminTest(base.BaseV2ImageAdminTest):
self.assertRaises(lib_exc.Forbidden,
self.admin_client.update_image, image['id'], [
dict(remove='/locations/0')])
class MultiStoresImagesTest(base.BaseV2ImageAdminTest, base.BaseV2ImageTest):
"""Test importing and deleting image in multiple stores"""
@classmethod
def skip_checks(cls):
super(MultiStoresImagesTest, cls).skip_checks()
if not CONF.image_feature_enabled.import_image:
skip_msg = (
"%s skipped as image import is not available" % cls.__name__)
raise cls.skipException(skip_msg)
@classmethod
def resource_setup(cls):
super(MultiStoresImagesTest, cls).resource_setup()
cls.available_import_methods = \
cls.client.info_import()['import-methods']['value']
if not cls.available_import_methods:
raise cls.skipException('Server does not support '
'any import method')
# NOTE(pdeore): Skip if glance-direct import method and mutlistore
# are not enabled/configured, or only one store is configured in
# multiple stores setup.
cls.available_stores = cls.get_available_stores()
if ('glance-direct' not in cls.available_import_methods or
not len(cls.available_stores) > 1):
raise cls.skipException(
'Either glance-direct import method not present in %s or '
'None or only one store is '
'configured %s' % (cls.available_import_methods,
cls.available_stores))
@decorators.idempotent_id('1ecec683-41d4-4470-a0df-54969ec74514')
def test_delete_image_from_specific_store(self):
"""Test delete image from specific store"""
# Import image to available stores
image, stores = self.create_and_stage_image(all_stores=True)
self.client.image_import(image['id'],
method='glance-direct',
all_stores=True)
self.addCleanup(self.admin_client.delete_image, image['id'])
waiters.wait_for_image_imported_to_stores(
self.client,
image['id'], stores)
observed_image = self.client.show_image(image['id'])
# Image will be deleted from first store
first_image_store_deleted = (observed_image['stores'].split(","))[0]
self.admin_client.delete_image_from_store(
observed_image['id'], first_image_store_deleted)
waiters.wait_for_image_deleted_from_store(
self.admin_client,
observed_image,
stores,
first_image_store_deleted)

View File

@ -344,37 +344,6 @@ class MultiStoresImportImagesTest(base.BaseV2ImageTest):
'configured %s' % (cls.available_import_methods,
cls.available_stores))
def _create_and_stage_image(self, all_stores=False):
"""Create Image & stage image file for glance-direct import method."""
image_name = data_utils.rand_name(
prefix=CONF.resource_name_prefix, name='test-image')
container_format = CONF.image.container_formats[0]
disk_format = CONF.image.disk_formats[0]
image = self.create_image(name=image_name,
container_format=container_format,
disk_format=disk_format,
visibility='private')
self.assertEqual('queued', image['status'])
self.client.stage_image_file(
image['id'],
io.BytesIO(data_utils.random_bytes()))
# Check image status is 'uploading'
body = self.client.show_image(image['id'])
self.assertEqual(image['id'], body['id'])
self.assertEqual('uploading', body['status'])
if all_stores:
stores_list = ','.join([store['id']
for store in self.available_stores
if store.get('read-only') != 'true'])
else:
stores = [store['id'] for store in self.available_stores
if store.get('read-only') != 'true']
stores_list = stores[::max(1, len(stores) - 1)]
return body, stores_list
@decorators.idempotent_id('bf04ff00-3182-47cb-833a-f1c6767b47fd')
def test_glance_direct_import_image_to_all_stores(self):
"""Test image is imported in all available stores
@ -382,7 +351,7 @@ class MultiStoresImportImagesTest(base.BaseV2ImageTest):
Create image, import image to all available stores using glance-direct
import method and verify that import succeeded.
"""
image, stores = self._create_and_stage_image(all_stores=True)
image, stores = self.create_and_stage_image(all_stores=True)
self.client.image_import(
image['id'], method='glance-direct', all_stores=True)
@ -397,7 +366,7 @@ class MultiStoresImportImagesTest(base.BaseV2ImageTest):
Create image, import image to specified store(s) using glance-direct
import method and verify that import succeeded.
"""
image, stores = self._create_and_stage_image()
image, stores = self.create_and_stage_image()
self.client.image_import(image['id'], method='glance-direct',
stores=stores)

View File

@ -311,6 +311,36 @@ def wait_for_image_copied_to_stores(client, image_id):
raise lib_exc.TimeoutException(message)
def wait_for_image_deleted_from_store(client, image, available_stores,
image_store_deleted):
"""Waits for an image to be deleted from specific store.
API will not allow deletion of the last location for an image.
This return image if image deleted from store.
"""
# Check if image have last store location
if len(available_stores) == 1:
exc_cls = lib_exc.OtherRestClientException
message = ('Delete from last store location not allowed'
% (image, image_store_deleted))
raise exc_cls(message)
start = int(time.time())
while int(time.time()) - start < client.build_timeout:
image = client.show_image(image['id'])
image_stores = image['stores'].split(",")
if image_store_deleted not in image_stores:
return
time.sleep(client.build_interval)
message = ('Failed to delete %s from requested store location: %s '
'within the required time: (%s s)' %
(image, image_store_deleted, client.build_timeout))
caller = test_utils.find_test_caller()
if caller:
message = '(%s) %s' % (caller, message)
raise exc_cls(message)
def wait_for_volume_resource_status(client, resource_id, status,
server_id=None, servers_client=None):
"""Waits for a volume resource to reach a given status.

View File

@ -292,3 +292,15 @@ class ImagesClient(rest_client.RestClient):
resp, _ = self.delete(url)
self.expected_success(204, resp.status)
return rest_client.ResponseBody(resp)
def delete_image_from_store(self, image_id, store_name):
"""Delete image from store
For a full list of available parameters,
please refer to the official API reference:
https://docs.openstack.org/api-ref/image/v2/#delete-image-from-store
"""
url = 'stores/%s/%s' % (store_name, image_id)
resp, _ = self.delete(url)
self.expected_success(204, resp.status)
return rest_client.ResponseBody(resp)

View File

@ -146,6 +146,36 @@ class TestImagesClient(base.BaseServiceTest):
]
}
FAKE_DELETE_IMAGE_FROM_STORE = {
"id": "e485aab9-0907-4973-921c-bb6da8a8fcf8",
"name": u"\u2740(*\xb4\u25e2`*)\u2740",
"status": "active",
"visibility": "public",
"size": 2254249,
"checksum": "2cec138d7dae2aa59038ef8c9aec2390",
"tags": [
"fedora",
"beefy"
],
"created_at": "2012-08-10T19:23:50Z",
"updated_at": "2012-08-12T11:11:33Z",
"self": "/v2/images/da3b75d9-3f4a-40e7-8a2c-bfab23927dea",
"file": "/v2/images/da3b75d9-3f4a-40e7-8a2c-bfab23927"
"dea/file",
"schema": "/v2/schemas/image",
"owner": None,
"min_ram": None,
"min_disk": None,
"disk_format": None,
"virtual_size": None,
"container_format": None,
"os_hash_algo": "sha512",
"os_hash_value": "ef7d1ed957ffafefb324d50ebc6685ed03d0e645d",
"os_hidden": False,
"protected": False,
"stores": ["store-1", "store-2"],
}
FAKE_TAG_NAME = "fake tag"
def setUp(self):
@ -294,3 +324,12 @@ class TestImagesClient(base.BaseServiceTest):
self.FAKE_SHOW_IMAGE_TASKS,
True,
image_id="e485aab9-0907-4973-921c-bb6da8a8fcf8")
def test_delete_image_from_store(self):
self.check_service_client_function(
self.client.delete_image_from_store,
'tempest.lib.common.rest_client.RestClient.delete',
{},
image_id=self.FAKE_DELETE_IMAGE_FROM_STORE["id"],
store_name=self.FAKE_DELETE_IMAGE_FROM_STORE["stores"][0],
status=204)