| # Copyright 2013 IBM Corp. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may |
| # not use this file except in compliance with the License. You may obtain |
| # a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations |
| # under the License. |
| |
| import io |
| import time |
| |
| from tempest import config |
| from tempest.lib.common.utils import data_utils |
| from tempest.lib.common.utils import test_utils |
| from tempest.lib import exceptions |
| import tempest.test |
| |
| CONF = config.CONF |
| BAD_REQUEST_RETRIES = 3 |
| |
| |
| class BaseImageTest(tempest.test.BaseTestCase): |
| """Base test class for Image API tests.""" |
| |
| credentials = ['primary'] |
| |
| @classmethod |
| def skip_checks(cls): |
| super(BaseImageTest, cls).skip_checks() |
| if not CONF.service_available.glance: |
| skip_msg = ("%s skipped as glance is not available" % cls.__name__) |
| raise cls.skipException(skip_msg) |
| |
| @classmethod |
| def setup_credentials(cls): |
| cls.set_network_resources() |
| super(BaseImageTest, cls).setup_credentials() |
| |
| @classmethod |
| def resource_setup(cls): |
| super(BaseImageTest, cls).resource_setup() |
| cls.created_images = [] |
| |
| @classmethod |
| def create_image(cls, data=None, **kwargs): |
| """Wrapper that returns a test image.""" |
| |
| if 'name' not in kwargs: |
| name = data_utils.rand_name( |
| prefix=CONF.resource_name_prefix, |
| name=cls.__name__ + "-image") |
| kwargs['name'] = name |
| |
| image = cls.client.create_image(**kwargs) |
| cls.created_images.append(image['id']) |
| cls.addClassResourceCleanup(cls.client.wait_for_resource_deletion, |
| image['id']) |
| cls.addClassResourceCleanup(test_utils.call_and_ignore_notfound_exc, |
| cls.client.delete_image, image['id']) |
| return image |
| |
| |
| class BaseV2ImageTest(BaseImageTest): |
| |
| @classmethod |
| def skip_checks(cls): |
| super(BaseV2ImageTest, cls).skip_checks() |
| if not CONF.image_feature_enabled.api_v2: |
| msg = "Glance API v2 not supported" |
| raise cls.skipException(msg) |
| |
| @classmethod |
| def setup_clients(cls): |
| super(BaseV2ImageTest, cls).setup_clients() |
| cls.client = cls.os_primary.image_client_v2 |
| cls.schemas_client = cls.os_primary.schemas_client |
| cls.versions_client = cls.os_primary.image_versions_client |
| |
| def create_namespace(self, namespace_name=None, visibility='public', |
| description='Tempest', protected=False, |
| **kwargs): |
| if not namespace_name: |
| namespace_name = data_utils.rand_name( |
| prefix=CONF.resource_name_prefix, name='test-ns') |
| kwargs.setdefault('display_name', namespace_name) |
| namespace = self.namespaces_client.create_namespace( |
| namespace=namespace_name, visibility=visibility, |
| description=description, protected=protected, **kwargs) |
| self.addCleanup(self.namespaces_client.delete_namespace, |
| namespace_name) |
| 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] |
| image = self.create_image(name=image_name, |
| container_format=container_format, |
| disk_format='raw', |
| 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 |
| def get_available_stores(cls): |
| stores = [] |
| try: |
| stores = cls.client.info_stores()['stores'] |
| except exceptions.NotFound: |
| pass |
| return stores |
| |
| def _update_image_with_retries(self, image, patch): |
| # NOTE(danms): If glance was unable to fetch the remote image via |
| # HTTP, it will return BadRequest. Because this can be transient in |
| # CI, we try this a few times before we agree that it has failed |
| # for a reason worthy of failing the test. |
| for i in range(BAD_REQUEST_RETRIES): |
| try: |
| self.client.update_image(image, patch) |
| break |
| except exceptions.BadRequest: |
| if i + 1 == BAD_REQUEST_RETRIES: |
| raise |
| else: |
| time.sleep(1) |
| |
| def check_set_location(self): |
| image = self.client.create_image(container_format='bare', |
| disk_format='raw') |
| |
| # Locations should be empty when there is no data |
| self.assertEqual('queued', image['status']) |
| self.assertEqual([], image['locations']) |
| |
| # Add a new location |
| new_loc = {'metadata': {'foo': 'bar'}, |
| 'url': CONF.image.http_image} |
| self._update_image_with_retries(image['id'], [ |
| dict(add='/locations/-', value=new_loc)]) |
| |
| # The image should now be active, with one location that looks |
| # like we expect |
| image = self.client.show_image(image['id']) |
| self.assertEqual(1, len(image['locations']), |
| 'Image should have one location but has %i' % ( |
| len(image['locations']))) |
| self.assertEqual(new_loc['url'], image['locations'][0]['url']) |
| self.assertEqual('bar', image['locations'][0]['metadata'].get('foo')) |
| if 'direct_url' in image: |
| self.assertEqual(image['direct_url'], image['locations'][0]['url']) |
| |
| # If we added the location directly, the image goes straight |
| # to active and no hashing is done |
| self.assertEqual('active', image['status']) |
| self.assertIsNone(None, image['os_hash_algo']) |
| self.assertIsNone(None, image['os_hash_value']) |
| |
| return image |
| |
| def check_set_multiple_locations(self): |
| image = self.check_set_location() |
| |
| new_loc = {'metadata': {'speed': '88mph'}, |
| 'url': '%s#new' % CONF.image.http_image} |
| self._update_image_with_retries(image['id'], |
| [dict(add='/locations/-', |
| value=new_loc)]) |
| |
| # The image should now have two locations and the last one |
| # (locations are ordered) should have the new URL. |
| image = self.client.show_image(image['id']) |
| self.assertEqual(2, len(image['locations']), |
| 'Image should have two locations but has %i' % ( |
| len(image['locations']))) |
| self.assertEqual(new_loc['url'], image['locations'][1]['url']) |
| |
| # The image should still be active and still have no hashes |
| self.assertEqual('active', image['status']) |
| self.assertIsNone(None, image['os_hash_algo']) |
| self.assertIsNone(None, image['os_hash_value']) |
| |
| # The direct_url should still match the first location |
| if 'direct_url' in image: |
| self.assertEqual(image['direct_url'], image['locations'][0]['url']) |
| |
| return image |
| |
| |
| class BaseV2MemberImageTest(BaseV2ImageTest): |
| |
| credentials = ['primary', 'alt'] |
| |
| @classmethod |
| def setup_clients(cls): |
| super(BaseV2MemberImageTest, cls).setup_clients() |
| cls.image_member_client = cls.os_primary.image_member_client_v2 |
| cls.alt_image_member_client = cls.os_alt.image_member_client_v2 |
| cls.alt_img_client = cls.os_alt.image_client_v2 |
| |
| @classmethod |
| def resource_setup(cls): |
| super(BaseV2MemberImageTest, cls).resource_setup() |
| cls.alt_tenant_id = cls.alt_image_member_client.tenant_id |
| |
| def _list_image_ids_as_alt(self): |
| image_list = self.alt_img_client.list_images()['images'] |
| image_ids = map(lambda x: x['id'], image_list) |
| return image_ids |
| |
| def _create_image(self): |
| name = data_utils.rand_name( |
| prefix=CONF.resource_name_prefix, |
| name=self.__class__.__name__ + '-image') |
| image = self.client.create_image(name=name, |
| container_format='bare', |
| disk_format='raw') |
| self.addCleanup(self.client.delete_image, image['id']) |
| return image['id'] |
| |
| |
| class BaseV2ImageAdminTest(BaseV2ImageTest): |
| |
| credentials = ['admin', 'primary'] |
| |
| @classmethod |
| def setup_clients(cls): |
| super(BaseV2ImageAdminTest, cls).setup_clients() |
| cls.admin_client = cls.os_admin.image_client_v2 |
| cls.namespaces_client = cls.os_admin.namespaces_client |
| cls.resource_types_client = cls.os_admin.resource_types_client |
| cls.namespace_properties_client =\ |
| cls.os_admin.namespace_properties_client |
| cls.namespace_objects_client = cls.os_admin.namespace_objects_client |
| cls.namespace_tags_client = cls.os_admin.namespace_tags_client |