Merge "Add delete image from specific store API"
This commit is contained in:
commit
01c2e2ff7e
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Add delete image from specific store API to image V2 client
|
@ -12,6 +12,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import io
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from tempest import config
|
from tempest import config
|
||||||
@ -95,6 +96,36 @@ class BaseV2ImageTest(BaseImageTest):
|
|||||||
namespace_name)
|
namespace_name)
|
||||||
return namespace
|
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
|
@classmethod
|
||||||
def get_available_stores(cls):
|
def get_available_stores(cls):
|
||||||
stores = []
|
stores = []
|
||||||
|
@ -179,3 +179,59 @@ class ImageLocationsAdminTest(base.BaseV2ImageAdminTest):
|
|||||||
self.assertRaises(lib_exc.Forbidden,
|
self.assertRaises(lib_exc.Forbidden,
|
||||||
self.admin_client.update_image, image['id'], [
|
self.admin_client.update_image, image['id'], [
|
||||||
dict(remove='/locations/0')])
|
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)
|
||||||
|
@ -344,37 +344,6 @@ class MultiStoresImportImagesTest(base.BaseV2ImageTest):
|
|||||||
'configured %s' % (cls.available_import_methods,
|
'configured %s' % (cls.available_import_methods,
|
||||||
cls.available_stores))
|
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')
|
@decorators.idempotent_id('bf04ff00-3182-47cb-833a-f1c6767b47fd')
|
||||||
def test_glance_direct_import_image_to_all_stores(self):
|
def test_glance_direct_import_image_to_all_stores(self):
|
||||||
"""Test image is imported in all available stores
|
"""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
|
Create image, import image to all available stores using glance-direct
|
||||||
import method and verify that import succeeded.
|
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(
|
self.client.image_import(
|
||||||
image['id'], method='glance-direct', all_stores=True)
|
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
|
Create image, import image to specified store(s) using glance-direct
|
||||||
import method and verify that import succeeded.
|
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',
|
self.client.image_import(image['id'], method='glance-direct',
|
||||||
stores=stores)
|
stores=stores)
|
||||||
|
|
||||||
|
@ -311,6 +311,36 @@ def wait_for_image_copied_to_stores(client, image_id):
|
|||||||
raise lib_exc.TimeoutException(message)
|
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,
|
def wait_for_volume_resource_status(client, resource_id, status,
|
||||||
server_id=None, servers_client=None):
|
server_id=None, servers_client=None):
|
||||||
"""Waits for a volume resource to reach a given status.
|
"""Waits for a volume resource to reach a given status.
|
||||||
|
@ -292,3 +292,15 @@ class ImagesClient(rest_client.RestClient):
|
|||||||
resp, _ = self.delete(url)
|
resp, _ = self.delete(url)
|
||||||
self.expected_success(204, resp.status)
|
self.expected_success(204, resp.status)
|
||||||
return rest_client.ResponseBody(resp)
|
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)
|
||||||
|
@ -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"
|
FAKE_TAG_NAME = "fake tag"
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -294,3 +324,12 @@ class TestImagesClient(base.BaseServiceTest):
|
|||||||
self.FAKE_SHOW_IMAGE_TASKS,
|
self.FAKE_SHOW_IMAGE_TASKS,
|
||||||
True,
|
True,
|
||||||
image_id="e485aab9-0907-4973-921c-bb6da8a8fcf8")
|
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)
|
||||||
|
Loading…
Reference in New Issue
Block a user