# Copyright 2016 Hewlett-Packard Development Company, L.P. # # 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 operator import tempfile import uuid from openstack.cloud import exc from openstack.cloud import meta from openstack import exceptions from openstack.image.v1 import image as image_v1 from openstack.image.v2 import image from openstack.tests import fakes from openstack.tests.unit import base IMPORT_METHODS = 'glance-direct,web-download' class BaseTestImage(base.TestCase): def setUp(self): super(BaseTestImage, self).setUp() self.image_id = str(uuid.uuid4()) self.image_name = self.getUniqueString('image') self.object_name = 'images/{name}'.format(name=self.image_name) self.imagefile = tempfile.NamedTemporaryFile(delete=False) data = b'\2\0' self.imagefile.write(data) self.imagefile.close() self.output = data self.fake_image_dict = fakes.make_fake_image( image_id=self.image_id, image_name=self.image_name, data=self.imagefile.name, ) self.fake_search_return = {'images': [self.fake_image_dict]} self.container_name = self.getUniqueString('container') def _compare_images(self, exp, real): self.assertDictEqual( image.Image(**exp).to_dict(computed=False), real.to_dict(computed=False), ) def _compare_images_v1(self, exp, real): self.assertDictEqual( image_v1.Image(**exp).to_dict(computed=False), real.to_dict(computed=False), ) class TestImage(BaseTestImage): def setUp(self): super(TestImage, self).setUp() self.use_glance() def test_config_v1(self): self.cloud.config.config['image_api_version'] = '1' # We override the scheme of the endpoint with the scheme of the service # because glance has a bug where it doesn't return https properly. self.assertEqual( 'https://image.example.com/v1/', self.cloud._image_client.get_endpoint(), ) self.assertEqual('1', self.cloud_config.get_api_version('image')) def test_config_v2(self): self.cloud.config.config['image_api_version'] = '2' # We override the scheme of the endpoint with the scheme of the service # because glance has a bug where it doesn't return https properly. self.assertEqual( 'https://image.example.com/v2/', self.cloud._image_client.get_endpoint(), ) self.assertEqual('2', self.cloud_config.get_api_version('image')) def test_download_image_no_output(self): self.assertRaises( exc.OpenStackCloudException, self.cloud.download_image, self.image_name, ) def test_download_image_two_outputs(self): fake_fd = io.BytesIO() self.assertRaises( exc.OpenStackCloudException, self.cloud.download_image, self.image_name, output_path='fake_path', output_file=fake_fd, ) def test_download_image_no_images_found(self): self.register_uris( [ dict( method='GET', uri='https://image.example.com/v2/images/{name}'.format( name=self.image_name ), status_code=404, ), dict( method='GET', uri='https://image.example.com/v2/images?name={name}'.format( # noqa: E501 name=self.image_name ), json=dict(images=[]), ), dict( method='GET', uri='https://image.example.com/v2/images?os_hidden=True', json=dict(images=[]), ), ] ) self.assertRaises( exc.OpenStackCloudResourceNotFound, self.cloud.download_image, self.image_name, output_path='fake_path', ) self.assert_calls() def _register_image_mocks(self): self.register_uris( [ dict( method='GET', uri='https://image.example.com/v2/images/{name}'.format( name=self.image_name ), status_code=404, ), dict( method='GET', uri='https://image.example.com/v2/images?name={name}'.format( # noqa: E501 name=self.image_name ), json=self.fake_search_return, ), dict( method='GET', uri='https://image.example.com/v2/images/{id}/file'.format( id=self.image_id ), content=self.output, headers={ 'Content-Type': 'application/octet-stream', 'Content-MD5': self.fake_image_dict['checksum'], }, ), ] ) def test_download_image_with_fd(self): self._register_image_mocks() output_file = io.BytesIO() self.cloud.download_image(self.image_name, output_file=output_file) output_file.seek(0) self.assertEqual(output_file.read(), self.output) self.assert_calls() def test_download_image_with_path(self): self._register_image_mocks() output_file = tempfile.NamedTemporaryFile() self.cloud.download_image( self.image_name, output_path=output_file.name ) output_file.seek(0) self.assertEqual(output_file.read(), self.output) self.assert_calls() def test_get_image_name(self, cloud=None): cloud = cloud or self.cloud self.register_uris( [ dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), json=self.fake_search_return, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), json=self.fake_search_return, ), ] ) self.assertEqual(self.image_name, cloud.get_image_name(self.image_id)) self.assertEqual( self.image_name, cloud.get_image_name(self.image_name) ) self.assert_calls() def test_get_image_by_id(self): self.register_uris( [ dict( method='GET', uri=self.get_mock_url( 'image', append=['images', self.image_id], base_url_append='v2', ), json=self.fake_image_dict, ) ] ) self._compare_images( self.fake_image_dict, self.cloud.get_image_by_id(self.image_id) ) self.assert_calls() def test_get_image_id(self, cloud=None): cloud = cloud or self.cloud self.register_uris( [ dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), json=self.fake_search_return, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), json=self.fake_search_return, ), ] ) self.assertEqual(self.image_id, cloud.get_image_id(self.image_id)) self.assertEqual(self.image_id, cloud.get_image_id(self.image_name)) self.assert_calls() def test_get_image_name_operator(self): # This should work the same as non-operator, just verifying it does. self.test_get_image_name(cloud=self.cloud) def test_get_image_id_operator(self): # This should work the same as the other test, just verifying it does. self.test_get_image_id(cloud=self.cloud) def test_empty_list_images(self): self.register_uris( [ dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), json={'images': []}, ) ] ) self.assertEqual([], self.cloud.list_images()) self.assert_calls() def test_list_images(self): self.register_uris( [ dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), json=self.fake_search_return, ) ] ) [ self._compare_images(a, b) for a, b in zip([self.fake_image_dict], self.cloud.list_images()) ] self.assert_calls() def test_list_images_show_all(self): self.register_uris( [ dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2', qs_elements=['member_status=all'], ), json=self.fake_search_return, ) ] ) [ self._compare_images(a, b) for a, b in zip( [self.fake_image_dict], self.cloud.list_images(show_all=True) ) ] self.assert_calls() def test_list_images_show_all_deleted(self): deleted_image = self.fake_image_dict.copy() deleted_image['status'] = 'deleted' self.register_uris( [ dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2', qs_elements=['member_status=all'], ), json={'images': [self.fake_image_dict, deleted_image]}, ) ] ) [ self._compare_images(a, b) for a, b in zip( [self.fake_image_dict], self.cloud.list_images(show_all=True) ) ] self.assert_calls() def test_list_images_no_filter_deleted(self): deleted_image = self.fake_image_dict.copy() deleted_image['status'] = 'deleted' self.register_uris( [ dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), json={'images': [self.fake_image_dict, deleted_image]}, ) ] ) [ self._compare_images(a, b) for a, b in zip( [self.fake_image_dict], self.cloud.list_images(filter_deleted=False), ) ] self.assert_calls() def test_list_images_filter_deleted(self): deleted_image = self.fake_image_dict.copy() deleted_image['status'] = 'deleted' self.register_uris( [ dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), json={'images': [self.fake_image_dict, deleted_image]}, ) ] ) [ self._compare_images(a, b) for a, b in zip([self.fake_image_dict], self.cloud.list_images()) ] self.assert_calls() def test_list_images_string_properties(self): image_dict = self.fake_image_dict.copy() image_dict['properties'] = 'list,of,properties' self.register_uris( [ dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), json={'images': [image_dict]}, ), ] ) images = self.cloud.list_images() [self._compare_images(a, b) for a, b in zip([image_dict], images)] self.assertEqual( images[0]['properties']['properties'], 'list,of,properties' ) self.assert_calls() def test_list_images_paginated(self): marker = str(uuid.uuid4()) self.register_uris( [ dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), json={ 'images': [self.fake_image_dict], 'next': '/v2/images?marker={marker}'.format( marker=marker ), }, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2', qs_elements=['marker={marker}'.format(marker=marker)], ), json=self.fake_search_return, ), ] ) [ self._compare_images(a, b) for a, b in zip([self.fake_image_dict], self.cloud.list_images()) ] self.assert_calls() def test_create_image_put_v2_no_import(self): self.cloud.image_api_use_tasks = False self.register_uris( [ dict( method='GET', uri=self.get_mock_url( 'image', append=['images', self.image_name], base_url_append='v2', ), status_code=404, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2', qs_elements=['name=' + self.image_name], ), validate=dict(), json={'images': []}, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2', qs_elements=['os_hidden=True'], ), json={'images': []}, ), dict( method='POST', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), json=self.fake_image_dict, validate=dict( json={ 'container_format': 'bare', 'disk_format': 'qcow2', 'name': self.image_name, 'owner_specified.openstack.md5': self.fake_image_dict[ # noqa: E501 'owner_specified.openstack.md5' ], 'owner_specified.openstack.object': self.object_name, # noqa: E501 'owner_specified.openstack.sha256': self.fake_image_dict[ # noqa: E501 'owner_specified.openstack.sha256' ], 'visibility': 'private', 'tags': ['tag1', 'tag2'], } ), ), dict( method='PUT', uri=self.get_mock_url( 'image', append=['images', self.image_id, 'file'], base_url_append='v2', ), request_headers={ 'Content-Type': 'application/octet-stream' }, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images', self.fake_image_dict['id']], base_url_append='v2', ), json=self.fake_image_dict, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), complete_qs=True, json=self.fake_search_return, ), ] ) self.cloud.create_image( self.image_name, self.imagefile.name, wait=True, timeout=1, tags=['tag1', 'tag2'], is_public=False, validate_checksum=True, ) self.assert_calls() self.assertEqual( self.adapter.request_history[7].text.read(), self.output ) def test_create_image_put_v2_import_supported(self): self.cloud.image_api_use_tasks = False self.register_uris( [ dict( method='GET', uri=self.get_mock_url( 'image', append=['images', self.image_name], base_url_append='v2', ), status_code=404, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2', qs_elements=['name=' + self.image_name], ), validate=dict(), json={'images': []}, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2', qs_elements=['os_hidden=True'], ), json={'images': []}, ), dict( method='POST', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), json=self.fake_image_dict, headers={ 'OpenStack-image-import-methods': IMPORT_METHODS, }, validate=dict( json={ 'container_format': 'bare', 'disk_format': 'qcow2', 'name': self.image_name, 'owner_specified.openstack.md5': self.fake_image_dict[ # noqa: E501 'owner_specified.openstack.md5' ], 'owner_specified.openstack.object': self.object_name, # noqa: E501 'owner_specified.openstack.sha256': self.fake_image_dict[ # noqa: E501 'owner_specified.openstack.sha256' ], 'visibility': 'private', 'tags': ['tag1', 'tag2'], } ), ), dict( method='PUT', uri=self.get_mock_url( 'image', append=['images', self.image_id, 'file'], base_url_append='v2', ), request_headers={ 'Content-Type': 'application/octet-stream' }, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images', self.fake_image_dict['id']], base_url_append='v2', ), json=self.fake_image_dict, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), complete_qs=True, json=self.fake_search_return, ), ] ) self.cloud.create_image( self.image_name, self.imagefile.name, wait=True, timeout=1, tags=['tag1', 'tag2'], is_public=False, validate_checksum=True, ) self.assert_calls() self.assertEqual( self.adapter.request_history[7].text.read(), self.output ) def test_create_image_use_import(self): self.cloud.image_api_use_tasks = False self.register_uris( [ dict( method='GET', uri=self.get_mock_url( 'image', append=['images', self.image_name], base_url_append='v2', ), status_code=404, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2', qs_elements=['name=' + self.image_name], ), validate=dict(), json={'images': []}, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2', qs_elements=['os_hidden=True'], ), json={'images': []}, ), dict( method='POST', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), json=self.fake_image_dict, headers={ 'OpenStack-image-import-methods': IMPORT_METHODS, }, validate=dict( json={ 'container_format': 'bare', 'disk_format': 'qcow2', 'name': self.image_name, 'owner_specified.openstack.md5': self.fake_image_dict[ # noqa: E501 'owner_specified.openstack.md5' ], 'owner_specified.openstack.object': self.object_name, # noqa: E501 'owner_specified.openstack.sha256': self.fake_image_dict[ # noqa: E501 'owner_specified.openstack.sha256' ], 'visibility': 'private', 'tags': ['tag1', 'tag2'], } ), ), dict( method='PUT', uri=self.get_mock_url( 'image', append=['images', self.image_id, 'stage'], base_url_append='v2', ), request_headers={ 'Content-Type': 'application/octet-stream' }, ), dict( method='POST', uri=self.get_mock_url( 'image', append=['images', self.image_id, 'import'], base_url_append='v2', ), json={'method': {'name': 'glance-direct'}}, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images', self.fake_image_dict['id']], base_url_append='v2', ), json=self.fake_image_dict, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), complete_qs=True, json=self.fake_search_return, ), ] ) self.cloud.create_image( self.image_name, self.imagefile.name, wait=True, timeout=1, tags=['tag1', 'tag2'], is_public=False, validate_checksum=True, use_import=True, ) self.assert_calls() self.assertEqual( self.adapter.request_history[7].text.read(), self.output ) def test_create_image_task(self): self.cloud.image_api_use_tasks = True endpoint = self.cloud._object_store_client.get_endpoint() task_id = str(uuid.uuid4()) args = dict( id=task_id, status='success', type='import', result={ 'image_id': self.image_id, }, ) image_no_checksums = self.fake_image_dict.copy() del image_no_checksums['owner_specified.openstack.md5'] del image_no_checksums['owner_specified.openstack.sha256'] del image_no_checksums['owner_specified.openstack.object'] self.register_uris( [ dict( method='GET', uri=self.get_mock_url( 'image', append=['images', self.image_name], base_url_append='v2', ), status_code=404, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2', qs_elements=['name=' + self.image_name], ), validate=dict(), json={'images': []}, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2', qs_elements=['os_hidden=True'], ), json={'images': []}, ), dict( method='HEAD', uri='{endpoint}/{container}'.format( endpoint=endpoint, container=self.container_name ), status_code=404, ), dict( method='PUT', uri='{endpoint}/{container}'.format( endpoint=endpoint, container=self.container_name ), status_code=201, headers={ 'Date': 'Fri, 16 Dec 2016 18:21:20 GMT', 'Content-Length': '0', 'Content-Type': 'text/html; charset=UTF-8', }, ), dict( method='HEAD', uri='{endpoint}/{container}'.format( endpoint=endpoint, container=self.container_name ), headers={ 'Content-Length': '0', 'X-Container-Object-Count': '0', 'Accept-Ranges': 'bytes', 'X-Storage-Policy': 'Policy-0', 'Date': 'Fri, 16 Dec 2016 18:29:05 GMT', 'X-Timestamp': '1481912480.41664', 'X-Trans-Id': 'tx60ec128d9dbf44b9add68-0058543271dfw1', 'X-Container-Bytes-Used': '0', 'Content-Type': 'text/plain; charset=utf-8', }, ), dict( method='GET', # This is explicitly not using get_mock_url because that # gets us a project-id oriented URL. uri='https://object-store.example.com/info', json=dict( swift={'max_file_size': 1000}, slo={'min_segment_size': 500}, ), ), dict( method='HEAD', uri='{endpoint}/{container}/{object}'.format( endpoint=endpoint, container=self.container_name, object=self.image_name, ), status_code=404, ), dict( method='PUT', uri='{endpoint}/{container}/{object}'.format( endpoint=endpoint, container=self.container_name, object=self.image_name, ), status_code=201, validate=dict( headers={ 'X-Object-Meta-x-sdk-md5': self.fake_image_dict[ 'owner_specified.openstack.md5' ], 'X-Object-Meta-x-sdk-sha256': self.fake_image_dict[ 'owner_specified.openstack.sha256' ], } ), ), dict( method='POST', uri=self.get_mock_url( 'image', append=['tasks'], base_url_append='v2' ), json={'id': task_id, 'status': 'processing'}, validate=dict( json=dict( type='import', input={ 'import_from': '{container}/{object}'.format( container=self.container_name, object=self.image_name, ), 'image_properties': {'name': self.image_name}, }, ) ), ), dict( method='GET', uri=self.get_mock_url( 'image', append=['tasks', task_id], base_url_append='v2', ), status_code=503, text='Random error', ), dict( method='GET', uri=self.get_mock_url( 'image', append=['tasks', task_id], base_url_append='v2', ), json=args, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images', self.image_id], base_url_append='v2', ), json=image_no_checksums, ), dict( method='PATCH', uri=self.get_mock_url( 'image', append=['images', self.image_id], base_url_append='v2', ), validate=dict( json=sorted( [ { 'op': 'add', 'value': '{container}/{object}'.format( container=self.container_name, object=self.image_name, ), 'path': '/owner_specified.openstack.object', # noqa: E501 }, { 'op': 'add', 'value': self.fake_image_dict[ 'owner_specified.openstack.md5' ], 'path': '/owner_specified.openstack.md5', }, { 'op': 'add', 'value': self.fake_image_dict[ 'owner_specified.openstack.sha256' ], 'path': '/owner_specified.openstack.sha256', # noqa: E501 }, ], key=operator.itemgetter('path'), ), headers={ 'Content-Type': 'application/openstack-images-v2.1-json-patch' # noqa: E501 }, ), json=self.fake_search_return, ), dict( method='HEAD', uri='{endpoint}/{container}/{object}'.format( endpoint=endpoint, container=self.container_name, object=self.image_name, ), headers={ 'X-Timestamp': '1429036140.50253', 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', 'Content-Length': '1290170880', 'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT', 'X-Object-Meta-X-Sdk-Sha256': self.fake_image_dict[ 'owner_specified.openstack.sha256' ], 'X-Object-Meta-X-Sdk-Md5': self.fake_image_dict[ 'owner_specified.openstack.md5' ], 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', 'Accept-Ranges': 'bytes', 'Content-Type': 'application/octet-stream', 'Etag': fakes.NO_MD5, }, ), dict( method='DELETE', uri='{endpoint}/{container}/{object}'.format( endpoint=endpoint, container=self.container_name, object=self.image_name, ), ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), complete_qs=True, json=self.fake_search_return, ), ] ) self.cloud.create_image( self.image_name, self.imagefile.name, wait=True, timeout=1, disk_format='vhd', container_format='ovf', is_public=False, validate_checksum=True, container=self.container_name, ) self.assert_calls() def test_delete_autocreated_no_tasks(self): self.use_keystone_v3() self.cloud.image_api_use_tasks = False deleted = self.cloud.delete_autocreated_image_objects( container=self.container_name ) self.assertFalse(deleted) self.assert_calls([]) def test_delete_image_task(self): self.cloud.image_api_use_tasks = True endpoint = self.cloud._object_store_client.get_endpoint() object_path = self.fake_image_dict['owner_specified.openstack.object'] image_no_checksums = self.fake_image_dict.copy() del image_no_checksums['owner_specified.openstack.md5'] del image_no_checksums['owner_specified.openstack.sha256'] del image_no_checksums['owner_specified.openstack.object'] self.register_uris( [ dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), json=self.fake_search_return, ), dict( method='DELETE', uri='https://image.example.com/v2/images/{id}'.format( id=self.image_id ), ), dict( method='HEAD', uri='{endpoint}/{object}'.format( endpoint=endpoint, object=object_path ), headers={ 'X-Timestamp': '1429036140.50253', 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', 'Content-Length': '1290170880', 'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT', 'X-Object-Meta-X-Sdk-Sha256': self.fake_image_dict[ 'owner_specified.openstack.sha256' ], 'X-Object-Meta-X-Sdk-Md5': self.fake_image_dict[ 'owner_specified.openstack.md5' ], 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', 'Accept-Ranges': 'bytes', 'Content-Type': 'application/octet-stream', 'Etag': fakes.NO_MD5, }, ), dict( method='DELETE', uri='{endpoint}/{object}'.format( endpoint=endpoint, object=object_path ), ), ] ) self.cloud.delete_image(self.image_id) self.assert_calls() def test_delete_autocreated_image_objects(self): self.use_keystone_v3() self.cloud.image_api_use_tasks = True endpoint = self.cloud._object_store_client.get_endpoint() other_image = self.getUniqueString('no-delete') self.register_uris( [ dict( method='GET', uri=self.get_mock_url( service_type='object-store', resource=self.container_name, qs_elements=['format=json'], ), json=[ { 'content_type': 'application/octet-stream', 'bytes': 1437258240, 'hash': '249219347276c331b87bf1ac2152d9af', 'last_modified': '2015-02-16T17:50:05.289600', 'name': other_image, }, { 'content_type': 'application/octet-stream', 'bytes': 1290170880, 'hash': fakes.NO_MD5, 'last_modified': '2015-04-14T18:29:00.502530', 'name': self.image_name, }, ], ), dict( method='HEAD', uri=self.get_mock_url( service_type='object-store', resource=self.container_name, append=[other_image], ), headers={ 'X-Timestamp': '1429036140.50253', 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', 'Content-Length': '1290170880', 'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT', 'X-Object-Meta-X-Shade-Sha256': 'does not matter', 'X-Object-Meta-X-Shade-Md5': 'does not matter', 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', 'Accept-Ranges': 'bytes', 'Content-Type': 'application/octet-stream', 'Etag': '249219347276c331b87bf1ac2152d9af', }, ), dict( method='HEAD', uri=self.get_mock_url( service_type='object-store', resource=self.container_name, append=[self.image_name], ), headers={ 'X-Timestamp': '1429036140.50253', 'X-Trans-Id': 'txbbb825960a3243b49a36f-005a0dadaedfw1', 'Content-Length': '1290170880', 'Last-Modified': 'Tue, 14 Apr 2015 18:29:01 GMT', 'X-Object-Meta-X-Shade-Sha256': fakes.NO_SHA256, 'X-Object-Meta-X-Shade-Md5': fakes.NO_MD5, 'Date': 'Thu, 16 Nov 2017 15:24:30 GMT', 'Accept-Ranges': 'bytes', 'Content-Type': 'application/octet-stream', ( 'X-Object-Meta-' + self.cloud._OBJECT_AUTOCREATE_KEY ): 'true', 'Etag': fakes.NO_MD5, 'X-Static-Large-Object': 'false', }, ), dict( method='DELETE', uri='{endpoint}/{container}/{object}'.format( endpoint=endpoint, container=self.container_name, object=self.image_name, ), ), ] ) deleted = self.cloud.delete_autocreated_image_objects( container=self.container_name ) self.assertTrue(deleted) self.assert_calls() def _image_dict(self, fake_image): return self.cloud._normalize_image(meta.obj_to_munch(fake_image)) def _call_create_image(self, name, **kwargs): imagefile = tempfile.NamedTemporaryFile(delete=False) imagefile.write(b'\0') imagefile.close() self.cloud.create_image( name, imagefile.name, wait=True, timeout=1, is_public=False, validate_checksum=True, **kwargs ) def test_create_image_put_v1(self): self.cloud.config.config['image_api_version'] = '1' args = { 'name': self.image_name, 'container_format': 'bare', 'disk_format': 'qcow2', 'properties': { 'owner_specified.openstack.md5': fakes.NO_MD5, 'owner_specified.openstack.sha256': fakes.NO_SHA256, 'owner_specified.openstack.object': 'images/{name}'.format( name=self.image_name ), 'is_public': False, }, } ret = args.copy() ret['id'] = self.image_id ret['status'] = 'success' self.register_uris( [ dict( method='GET', uri='https://image.example.com/v1/images/' + self.image_name, status_code=404, ), dict( method='GET', uri='https://image.example.com/v1/images/detail?name=' + self.image_name, json={'images': []}, ), dict( method='POST', uri='https://image.example.com/v1/images', json={'image': ret}, validate=dict(json=args), ), dict( method='PUT', uri='https://image.example.com/v1/images/{id}'.format( id=self.image_id ), json={'image': ret}, validate=dict( headers={ 'x-image-meta-checksum': fakes.NO_MD5, 'x-glance-registry-purge-props': 'false', } ), ), dict( method='GET', uri='https://image.example.com/v1/images/detail', json={'images': [ret]}, ), ] ) self._call_create_image(self.image_name) [ self._compare_images_v1(b, a) for a, b in zip(self.cloud.list_images(), [ret]) ] def test_create_image_put_v1_bad_delete(self): self.cloud.config.config['image_api_version'] = '1' args = { 'name': self.image_name, 'container_format': 'bare', 'disk_format': 'qcow2', 'properties': { 'owner_specified.openstack.md5': fakes.NO_MD5, 'owner_specified.openstack.sha256': fakes.NO_SHA256, 'owner_specified.openstack.object': 'images/{name}'.format( name=self.image_name ), 'is_public': False, }, 'validate_checksum': True, } ret = args.copy() ret['id'] = self.image_id ret['status'] = 'success' self.register_uris( [ dict( method='GET', uri='https://image.example.com/v1/images/' + self.image_name, status_code=404, ), dict( method='GET', uri='https://image.example.com/v1/images/detail?name=' + self.image_name, json={'images': []}, ), dict( method='POST', uri='https://image.example.com/v1/images', json={'image': ret}, validate=dict(json=args), ), dict( method='PUT', uri='https://image.example.com/v1/images/{id}'.format( id=self.image_id ), status_code=400, validate=dict( headers={ 'x-image-meta-checksum': fakes.NO_MD5, 'x-glance-registry-purge-props': 'false', } ), ), dict( method='DELETE', uri='https://image.example.com/v1/images/{id}'.format( id=self.image_id ), json={'images': [ret]}, ), ] ) self.assertRaises( exc.OpenStackCloudHTTPError, self._call_create_image, self.image_name, ) self.assert_calls() def test_update_image_no_patch(self): self.cloud.image_api_use_tasks = False args = { 'name': self.image_name, 'container_format': 'bare', 'disk_format': 'qcow2', 'owner_specified.openstack.md5': fakes.NO_MD5, 'owner_specified.openstack.sha256': fakes.NO_SHA256, 'owner_specified.openstack.object': 'images/{name}'.format( name=self.image_name ), 'visibility': 'private', } ret = args.copy() ret['id'] = self.image_id ret['status'] = 'success' self.cloud.update_image_properties( image=image.Image.existing(**ret), **{ 'owner_specified.openstack.object': 'images/{name}'.format( name=self.image_name ) } ) self.assert_calls() def test_create_image_put_v2_bad_delete(self): self.cloud.image_api_use_tasks = False args = { 'name': self.image_name, 'container_format': 'bare', 'disk_format': 'qcow2', 'owner_specified.openstack.md5': fakes.NO_MD5, 'owner_specified.openstack.sha256': fakes.NO_SHA256, 'owner_specified.openstack.object': 'images/{name}'.format( name=self.image_name ), 'visibility': 'private', } ret = args.copy() ret['id'] = self.image_id ret['status'] = 'success' self.register_uris( [ dict( method='GET', uri=self.get_mock_url( 'image', append=['images', self.image_name], base_url_append='v2', ), status_code=404, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2', qs_elements=['name=' + self.image_name], ), validate=dict(), json={'images': []}, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2', qs_elements=['os_hidden=True'], ), json={'images': []}, ), dict( method='POST', uri='https://image.example.com/v2/images', json=ret, validate=dict(json=args), ), dict( method='PUT', uri='https://image.example.com/v2/images/{id}/file'.format( id=self.image_id ), status_code=400, validate=dict( headers={ 'Content-Type': 'application/octet-stream', }, ), ), dict( method='DELETE', uri='https://image.example.com/v2/images/{id}'.format( id=self.image_id ), ), ] ) self.assertRaises( exc.OpenStackCloudHTTPError, self._call_create_image, self.image_name, ) self.assert_calls() def test_create_image_put_v2_wrong_checksum_delete(self): self.cloud.image_api_use_tasks = False fake_image = self.fake_image_dict fake_image['owner_specified.openstack.md5'] = 'a' fake_image['owner_specified.openstack.sha256'] = 'b' self.register_uris( [ dict( method='POST', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), json=self.fake_image_dict, validate=dict( json={ 'container_format': 'bare', 'disk_format': 'qcow2', 'name': self.image_name, 'owner_specified.openstack.md5': fake_image[ 'owner_specified.openstack.md5' ], 'owner_specified.openstack.object': self.object_name, # noqa: E501 'owner_specified.openstack.sha256': fake_image[ 'owner_specified.openstack.sha256' ], 'visibility': 'private', } ), ), dict( method='PUT', uri=self.get_mock_url( 'image', append=['images', self.image_id, 'file'], base_url_append='v2', ), request_headers={ 'Content-Type': 'application/octet-stream' }, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images', self.fake_image_dict['id']], base_url_append='v2', ), json=fake_image, ), dict( method='DELETE', uri='https://image.example.com/v2/images/{id}'.format( id=self.image_id ), ), ] ) self.assertRaises( exceptions.SDKException, self.cloud.create_image, self.image_name, self.imagefile.name, is_public=False, md5='a', sha256='b', allow_duplicates=True, validate_checksum=True, ) self.assert_calls() def test_create_image_put_bad_int(self): self.cloud.image_api_use_tasks = False self.assertRaises( exc.OpenStackCloudException, self._call_create_image, self.image_name, allow_duplicates=True, min_disk='fish', min_ram=0, ) self.assert_calls() def test_create_image_put_user_int(self): self.cloud.image_api_use_tasks = False args = { 'name': self.image_name, 'container_format': 'bare', 'disk_format': 'qcow2', 'owner_specified.openstack.md5': fakes.NO_MD5, 'owner_specified.openstack.sha256': fakes.NO_SHA256, 'owner_specified.openstack.object': 'images/{name}'.format( name=self.image_name ), 'int_v': '12345', 'visibility': 'private', 'min_disk': 0, 'min_ram': 0, } ret = args.copy() ret['id'] = self.image_id ret['status'] = 'success' self.register_uris( [ dict( method='GET', uri=self.get_mock_url( 'image', append=['images', self.image_name], base_url_append='v2', ), status_code=404, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2', qs_elements=['name=' + self.image_name], ), validate=dict(), json={'images': []}, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2', qs_elements=['os_hidden=True'], ), json={'images': []}, ), dict( method='POST', uri='https://image.example.com/v2/images', json=ret, validate=dict(json=args), ), dict( method='PUT', uri='https://image.example.com/v2/images/{id}/file'.format( id=self.image_id ), validate=dict( headers={ 'Content-Type': 'application/octet-stream', }, ), ), dict( method='GET', uri='https://image.example.com/v2/images/{id}'.format( id=self.image_id ), json=ret, ), dict( method='GET', uri='https://image.example.com/v2/images', complete_qs=True, json={'images': [ret]}, ), ] ) self._call_create_image( self.image_name, min_disk='0', min_ram=0, int_v=12345 ) self.assert_calls() def test_create_image_put_meta_int(self): self.cloud.image_api_use_tasks = False args = { 'name': self.image_name, 'container_format': 'bare', 'disk_format': 'qcow2', 'owner_specified.openstack.md5': fakes.NO_MD5, 'owner_specified.openstack.sha256': fakes.NO_SHA256, 'owner_specified.openstack.object': 'images/{name}'.format( name=self.image_name ), 'int_v': 12345, 'visibility': 'private', 'min_disk': 0, 'min_ram': 0, } ret = args.copy() ret['id'] = self.image_id ret['status'] = 'success' ret['checksum'] = fakes.NO_MD5 self.register_uris( [ dict( method='GET', uri=self.get_mock_url( 'image', append=['images', self.image_name], base_url_append='v2', ), status_code=404, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2', qs_elements=['name=' + self.image_name], ), validate=dict(), json={'images': []}, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2', qs_elements=['os_hidden=True'], ), json={'images': []}, ), dict( method='POST', uri='https://image.example.com/v2/images', json=ret, validate=dict(json=args), ), dict( method='PUT', uri='https://image.example.com/v2/images/{id}/file'.format( id=self.image_id ), validate=dict( headers={ 'Content-Type': 'application/octet-stream', }, ), ), dict( method='GET', uri='https://image.example.com/v2/images/{id}'.format( id=self.image_id ), json=ret, ), dict( method='GET', uri='https://image.example.com/v2/images', complete_qs=True, json={'images': [ret]}, ), ] ) self._call_create_image( self.image_name, min_disk='0', min_ram=0, meta={'int_v': 12345} ) self.assert_calls() def test_create_image_put_protected(self): self.cloud.image_api_use_tasks = False args = { 'name': self.image_name, 'container_format': 'bare', 'disk_format': 'qcow2', 'owner_specified.openstack.md5': fakes.NO_MD5, 'owner_specified.openstack.sha256': fakes.NO_SHA256, 'owner_specified.openstack.object': 'images/{name}'.format( name=self.image_name ), 'int_v': '12345', 'protected': False, 'visibility': 'private', 'min_disk': 0, 'min_ram': 0, } ret = args.copy() ret['id'] = self.image_id ret['status'] = 'success' ret['checksum'] = fakes.NO_MD5 self.register_uris( [ dict( method='GET', uri=self.get_mock_url( 'image', append=['images', self.image_name], base_url_append='v2', ), status_code=404, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2', qs_elements=['name=' + self.image_name], ), validate=dict(), json={'images': []}, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2', qs_elements=['os_hidden=True'], ), json={'images': []}, ), dict( method='POST', uri='https://image.example.com/v2/images', json=ret, validate=dict(json=args), ), dict( method='PUT', uri='https://image.example.com/v2/images/{id}/file'.format( id=self.image_id ), validate=dict( headers={ 'Content-Type': 'application/octet-stream', }, ), ), dict( method='GET', uri='https://image.example.com/v2/images/{id}'.format( id=self.image_id ), json=ret, ), dict( method='GET', uri='https://image.example.com/v2/images', complete_qs=True, json={'images': [ret]}, ), ] ) self._call_create_image( self.image_name, min_disk='0', min_ram=0, properties={'int_v': 12345}, is_protected=False, ) self.assert_calls() class TestImageSuburl(BaseTestImage): def setUp(self): super(TestImageSuburl, self).setUp() self.os_fixture.use_suburl() self.os_fixture.build_tokens() self.use_keystone_v3() self.use_glance( image_version_json='image-version-suburl.json', image_discovery_url='https://example.com/image', ) def test_list_images(self): self.register_uris( [ dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), json=self.fake_search_return, ) ] ) [ self._compare_images(b, a) for a, b in zip(self.cloud.list_images(), [self.fake_image_dict]) ] self.assert_calls() def test_list_images_paginated(self): marker = str(uuid.uuid4()) self.register_uris( [ dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), json={ 'images': [self.fake_image_dict], 'next': '/v2/images?marker={marker}'.format( marker=marker ), }, ), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2', qs_elements=['marker={marker}'.format(marker=marker)], ), json=self.fake_search_return, ), ] ) [ self._compare_images(b, a) for a, b in zip( self.cloud.list_images(), [self.fake_image_dict, self.fake_image_dict], ) ] self.assert_calls() class TestImageV1Only(base.TestCase): def setUp(self): super(TestImageV1Only, self).setUp() self.use_glance(image_version_json='image-version-v1.json') def test_config_v1(self): self.cloud.config.config['image_api_version'] = '1' # We override the scheme of the endpoint with the scheme of the service # because glance has a bug where it doesn't return https properly. self.assertEqual( 'https://image.example.com/v1/', self.cloud._image_client.get_endpoint(), ) self.assertTrue(self.cloud._is_client_version('image', 1)) def test_config_v2(self): self.cloud.config.config['image_api_version'] = '2' # We override the scheme of the endpoint with the scheme of the service # because glance has a bug where it doesn't return https properly. self.assertEqual( 'https://image.example.com/v1/', self.cloud._image_client.get_endpoint(), ) self.assertFalse(self.cloud._is_client_version('image', 2)) class TestImageV2Only(base.TestCase): def setUp(self): super(TestImageV2Only, self).setUp() self.use_glance(image_version_json='image-version-v2.json') def test_config_v1(self): self.cloud.config.config['image_api_version'] = '1' # We override the scheme of the endpoint with the scheme of the service # because glance has a bug where it doesn't return https properly. self.assertEqual( 'https://image.example.com/v2/', self.cloud._image_client.get_endpoint(), ) self.assertTrue(self.cloud._is_client_version('image', 2)) def test_config_v2(self): self.cloud.config.config['image_api_version'] = '2' # We override the scheme of the endpoint with the scheme of the service # because glance has a bug where it doesn't return https properly. self.assertEqual( 'https://image.example.com/v2/', self.cloud._image_client.get_endpoint(), ) self.assertTrue(self.cloud._is_client_version('image', 2)) class TestImageVolume(BaseTestImage): def setUp(self): super(TestImageVolume, self).setUp() self.volume_id = str(uuid.uuid4()) def test_create_image_volume(self): self.register_uris( [ self.get_cinder_discovery_mock_dict(), dict( method='POST', uri=self.get_mock_url( 'volumev3', append=['volumes', self.volume_id, 'action'], ), json={ 'os-volume_upload_image': {'image_id': self.image_id} }, validate=dict( json={ 'os-volume_upload_image': { 'container_format': 'bare', 'disk_format': 'qcow2', 'force': False, 'image_name': 'fake_image', } } ), ), # NOTE(notmorgan): Glance discovery happens here, insert the # glance discovery mock at this point, DO NOT use the # .use_glance() method, that is intended only for use in # .setUp self.get_glance_discovery_mock_dict(), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), json=self.fake_search_return, ), ] ) self.cloud.create_image( 'fake_image', self.imagefile.name, wait=True, timeout=1, volume={'id': self.volume_id}, ) self.assert_calls() def test_create_image_volume_duplicate(self): self.register_uris( [ self.get_cinder_discovery_mock_dict(), dict( method='POST', uri=self.get_mock_url( 'volumev3', append=['volumes', self.volume_id, 'action'], ), json={ 'os-volume_upload_image': {'image_id': self.image_id} }, validate=dict( json={ 'os-volume_upload_image': { 'container_format': 'bare', 'disk_format': 'qcow2', 'force': True, 'image_name': 'fake_image', } } ), ), # NOTE(notmorgan): Glance discovery happens here, insert the # glance discovery mock at this point, DO NOT use the # .use_glance() method, that is intended only for use in # .setUp self.get_glance_discovery_mock_dict(), dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), json=self.fake_search_return, ), ] ) self.cloud.create_image( 'fake_image', self.imagefile.name, wait=True, timeout=1, volume={'id': self.volume_id}, allow_duplicates=True, ) self.assert_calls() class TestImageBrokenDiscovery(base.TestCase): def setUp(self): super(TestImageBrokenDiscovery, self).setUp() self.use_glance(image_version_json='image-version-broken.json') def test_url_fix(self): # image-version-broken.json has both http urls and localhost as the # host. This is testing that what is discovered is https, because # that's what's in the catalog, and image.example.com for the same # reason. self.register_uris( [ dict( method='GET', uri=self.get_mock_url( 'image', append=['images'], base_url_append='v2' ), json={'images': []}, ) ] ) self.assertEqual([], self.cloud.list_images()) self.assertEqual( self.cloud._image_client.get_endpoint(), 'https://image.example.com/v2/', ) self.assert_calls()