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
diff --git a/.zuul.yaml b/.zuul.yaml
index f7a22ba..9c53ba9 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -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
diff --git a/releasenotes/notes/image_import_testing_support-22ba4bcb9f2fb848.yaml b/releasenotes/notes/image_import_testing_support-22ba4bcb9f2fb848.yaml
new file mode 100644
index 0000000..b0180cc
--- /dev/null
+++ b/releasenotes/notes/image_import_testing_support-22ba4bcb9f2fb848.yaml
@@ -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.
diff --git a/tempest/api/image/v2/test_images.py b/tempest/api/image/v2/test_images.py
index c4a3e0e..3e72b34 100644
--- a/tempest/api/image/v2/test_images.py
+++ b/tempest/api/image/v2/test_images.py
@@ -29,6 +29,64 @@
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"""
diff --git a/tempest/config.py b/tempest/config.py
index 11f9426..cd5194c 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -658,6 +658,12 @@
'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',
diff --git a/tempest/lib/common/rest_client.py b/tempest/lib/common/rest_client.py
index 1d524f0..0513e90 100644
--- a/tempest/lib/common/rest_client.py
+++ b/tempest/lib/common/rest_client.py
@@ -914,12 +914,44 @@
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."""
diff --git a/tempest/lib/services/image/v2/images_client.py b/tempest/lib/services/image/v2/images_client.py
index 90778da..b9c5776 100644
--- a/tempest/lib/services/image/v2/images_client.py
+++ b/tempest/lib/services/image/v2/images_client.py
@@ -128,6 +128,15 @@
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 @@
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.