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:
Abhishek Kekane 2020-07-16 10:30:13 +00:00 committed by Ghanshyam Mann
parent 50ec7d74c3
commit 7cff130795
6 changed files with 198 additions and 0 deletions

View File

@ -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

View File

@ -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.

View File

@ -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"""

View File

@ -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',

View File

@ -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."""

View File

@ -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.