diff --git a/.zuul.yaml b/.zuul.yaml index f7a22ba809..9c53ba9f0e 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 0000000000..b0180ccd9c --- /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 c4a3e0e93f..3e72b3432b 100644 --- a/tempest/api/image/v2/test_images.py +++ b/tempest/api/image/v2/test_images.py @@ -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""" diff --git a/tempest/config.py b/tempest/config.py index 11f9426f3b..cd5194c504 100644 --- a/tempest/config.py +++ b/tempest/config.py @@ -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', diff --git a/tempest/lib/common/rest_client.py b/tempest/lib/common/rest_client.py index 1d524f0037..0513e90133 100644 --- a/tempest/lib/common/rest_client.py +++ b/tempest/lib/common/rest_client.py @@ -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.""" diff --git a/tempest/lib/services/image/v2/images_client.py b/tempest/lib/services/image/v2/images_client.py index 90778da31a..b9c5776655 100644 --- a/tempest/lib/services/image/v2/images_client.py +++ b/tempest/lib/services/image/v2/images_client.py @@ -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.