Tempest tests for glance import workflow
This adds an initial test for glance image interoperable import that uses the glance-direct method. It will skip if the server does not support glance-direct. Adding feature flag to enable the import tests as devstack on stable branches cannot support image import feature. Change-Id: I09e7fb4e7758edd5256ae70ceeea6f143466c3e3
This commit is contained in:
parent
50ec7d74c3
commit
7cff130795
@ -180,6 +180,7 @@
|
||||
USE_PYTHON3: true
|
||||
FORCE_CONFIG_DRIVE: true
|
||||
ENABLE_VOLUME_MULTIATTACH: true
|
||||
GLANCE_USE_IMPORT_WORKFLOW: True
|
||||
devstack_services:
|
||||
s-account: false
|
||||
s-container: false
|
||||
@ -270,6 +271,7 @@
|
||||
USE_PYTHON3: true
|
||||
FORCE_CONFIG_DRIVE: true
|
||||
ENABLE_VOLUME_MULTIATTACH: true
|
||||
GLANCE_USE_IMPORT_WORKFLOW: True
|
||||
|
||||
- job:
|
||||
name: tempest-integrated-object-storage
|
||||
|
@ -0,0 +1,17 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Add glance image import APIs function to v2
|
||||
images_client library.
|
||||
|
||||
* stage_image_file
|
||||
* info_import
|
||||
* info_stores
|
||||
* image_import
|
||||
other:
|
||||
- |
|
||||
New configuration options
|
||||
``CONF.glance.image_feature_enabled.image_import`` has been introduced
|
||||
to enable the image import tests. If your glance deployement support
|
||||
image import functionality then you can enable the image import tests
|
||||
via this flag. Default value of this new config option is false.
|
@ -29,6 +29,64 @@ CONF = config.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImportImagesTest(base.BaseV2ImageTest):
|
||||
"""Here we test the import operations for image"""
|
||||
|
||||
@classmethod
|
||||
def skip_checks(cls):
|
||||
super(ImportImagesTest, 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)
|
||||
|
||||
@decorators.idempotent_id('32ca0c20-e16f-44ac-8590-07869c9b4cc2')
|
||||
def test_image_import(self):
|
||||
"""Here we test these functionalities
|
||||
|
||||
Create image, stage image data, import image and verify
|
||||
that import succeeded.
|
||||
"""
|
||||
|
||||
body = self.client.info_import()
|
||||
if 'glance-direct' not in body['import-methods']['value']:
|
||||
raise self.skipException('Server does not support '
|
||||
'glance-direct import method')
|
||||
|
||||
# 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]
|
||||
image = self.create_image(name=image_name,
|
||||
container_format=container_format,
|
||||
disk_format=disk_format,
|
||||
visibility='private',
|
||||
ramdisk_id=uuid)
|
||||
self.assertIn('name', image)
|
||||
self.assertEqual(image_name, image['name'])
|
||||
self.assertIn('visibility', image)
|
||||
self.assertEqual('private', image['visibility'])
|
||||
self.assertIn('status', image)
|
||||
self.assertEqual('queued', image['status'])
|
||||
|
||||
# Stage image data
|
||||
file_content = data_utils.random_bytes()
|
||||
image_file = six.BytesIO(file_content)
|
||||
self.client.stage_image_file(image['id'], image_file)
|
||||
|
||||
# Now try to get image details
|
||||
body = self.client.show_image(image['id'])
|
||||
self.assertEqual(image['id'], body['id'])
|
||||
self.assertEqual(image_name, body['name'])
|
||||
self.assertEqual(uuid, body['ramdisk_id'])
|
||||
self.assertEqual('uploading', body['status'])
|
||||
|
||||
# import image from staging to backend
|
||||
self.client.image_import(image['id'])
|
||||
self.client.wait_for_resource_activation(image['id'])
|
||||
|
||||
|
||||
class BasicOperationsImagesTest(base.BaseV2ImageTest):
|
||||
"""Here we test the basic operations of images"""
|
||||
|
||||
|
@ -658,6 +658,12 @@ ImageFeaturesGroup = [
|
||||
'are current one. In future, Tempest will '
|
||||
'test v2 APIs only so this config option '
|
||||
'will be removed.'),
|
||||
# Image import feature is setup in devstack victoria onwards.
|
||||
# Once all stable branches setup the same via glance standalone
|
||||
# mode or with uwsgi, we can remove this config option.
|
||||
cfg.BoolOpt('import_image',
|
||||
default=False,
|
||||
help="Is image import feature enabled"),
|
||||
]
|
||||
|
||||
network_group = cfg.OptGroup(name='network',
|
||||
|
@ -914,12 +914,44 @@ class RestClient(object):
|
||||
raise exceptions.TimeoutException(message)
|
||||
time.sleep(self.build_interval)
|
||||
|
||||
def wait_for_resource_activation(self, id):
|
||||
"""Waits for a resource to become active
|
||||
|
||||
This method will loop over is_resource_active until either
|
||||
is_resource_active returns True or the build timeout is reached. This
|
||||
depends on is_resource_active being implemented
|
||||
|
||||
:param str id: The id of the resource to check
|
||||
:raises TimeoutException: If the build_timeout has elapsed and the
|
||||
resource still hasn't been active
|
||||
"""
|
||||
start_time = int(time.time())
|
||||
while True:
|
||||
if self.is_resource_active(id):
|
||||
return
|
||||
if int(time.time()) - start_time >= self.build_timeout:
|
||||
message = ('Failed to reach active state %(resource_type)s '
|
||||
'%(id)s within the required time (%(timeout)s s).' %
|
||||
{'resource_type': self.resource_type, 'id': id,
|
||||
'timeout': self.build_timeout})
|
||||
caller = test_utils.find_test_caller()
|
||||
if caller:
|
||||
message = '(%s) %s' % (caller, message)
|
||||
raise exceptions.TimeoutException(message)
|
||||
time.sleep(self.build_interval)
|
||||
|
||||
def is_resource_deleted(self, id):
|
||||
"""Subclasses override with specific deletion detection."""
|
||||
message = ('"%s" does not implement is_resource_deleted'
|
||||
% self.__class__.__name__)
|
||||
raise NotImplementedError(message)
|
||||
|
||||
def is_resource_active(self, id):
|
||||
"""Subclasses override with specific active detection."""
|
||||
message = ('"%s" does not implement is_resource_active'
|
||||
% self.__class__.__name__)
|
||||
raise NotImplementedError(message)
|
||||
|
||||
@property
|
||||
def resource_type(self):
|
||||
"""Returns the primary type of resource this client works with."""
|
||||
|
@ -128,6 +128,15 @@ class ImagesClient(rest_client.RestClient):
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_resource_active(self, id):
|
||||
try:
|
||||
image = self.show_image(id)
|
||||
if image['status'] != 'active':
|
||||
return False
|
||||
except lib_exc.NotFound:
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def resource_type(self):
|
||||
"""Returns the primary type of resource this client works with."""
|
||||
@ -152,6 +161,80 @@ class ImagesClient(rest_client.RestClient):
|
||||
self.expected_success(204, resp.status)
|
||||
return rest_client.ResponseBody(resp, body)
|
||||
|
||||
def stage_image_file(self, image_id, data):
|
||||
"""Upload binary image data to staging area.
|
||||
|
||||
For a full list of available parameters, please refer to the official
|
||||
API reference (stage API:
|
||||
https://docs.openstack.org/api-ref/image/v2/#interoperable-image-import
|
||||
"""
|
||||
url = 'images/%s/stage' % image_id
|
||||
|
||||
# We are going to do chunked transfer, so split the input data
|
||||
# info fixed-sized chunks.
|
||||
headers = {'Content-Type': 'application/octet-stream'}
|
||||
data = iter(functools.partial(data.read, CHUNKSIZE), b'')
|
||||
|
||||
resp, body = self.request('PUT', url, headers=headers,
|
||||
body=data, chunked=True)
|
||||
self.expected_success(204, resp.status)
|
||||
return rest_client.ResponseBody(resp, body)
|
||||
|
||||
def info_import(self):
|
||||
"""Return information about server-supported import methods."""
|
||||
url = 'info/import'
|
||||
resp, body = self.get(url)
|
||||
|
||||
self.expected_success(200, resp.status)
|
||||
body = json.loads(body)
|
||||
return rest_client.ResponseBody(resp, body)
|
||||
|
||||
def info_stores(self):
|
||||
"""Return information about server-supported stores."""
|
||||
url = 'info/stores'
|
||||
resp, body = self.get(url)
|
||||
body = json.loads(body)
|
||||
return rest_client.ResponseBody(resp, body)
|
||||
|
||||
def image_import(self, image_id, method='glance-direct',
|
||||
all_stores_must_succeed=None, all_stores=True,
|
||||
stores=None):
|
||||
"""Import data from staging area to glance store.
|
||||
|
||||
For a full list of available parameters, please refer to the official
|
||||
API reference (stage API:
|
||||
https://docs.openstack.org/api-ref/image/v2/#interoperable-image-import
|
||||
|
||||
:param method: The import method (i.e. glance-direct) to use
|
||||
:param all_stores_must_succeed: Boolean indicating if all store imports
|
||||
must succeed for the import to be
|
||||
considered successful. Must be None if
|
||||
server does not support multistore.
|
||||
:param all_stores: Boolean indicating if image should be imported to
|
||||
all available stores (incompatible with stores)
|
||||
:param stores: A list of destination store names for the import. Must
|
||||
be None if server does not support multistore.
|
||||
"""
|
||||
url = 'images/%s/import' % image_id
|
||||
data = {
|
||||
"method": {
|
||||
"name": method
|
||||
},
|
||||
}
|
||||
if stores is not None:
|
||||
data["stores"] = stores
|
||||
else:
|
||||
data["all_stores"] = all_stores
|
||||
|
||||
if all_stores_must_succeed is not None:
|
||||
data['all_stores_must_succeed'] = all_stores_must_succeed
|
||||
data = json.dumps(data)
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
resp, _ = self.post(url, data, headers=headers)
|
||||
|
||||
self.expected_success(202, resp.status)
|
||||
return rest_client.ResponseBody(resp)
|
||||
|
||||
def show_image_file(self, image_id):
|
||||
"""Download binary image data.
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user