
In the somewhat pathological case of an image having a property named properties already that contains a non-dict value, image normalizaiton becomes quite unhappy. Fix it. Change-Id: I2ce4dd4e3b7a4a9b46ff2bf699d2c8517e1ce7ce Story: 2001073 Task: 4697
798 lines
32 KiB
Python
798 lines
32 KiB
Python
# 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 operator
|
|
import tempfile
|
|
import uuid
|
|
|
|
import mock
|
|
import munch
|
|
import os_client_config as occ
|
|
import six
|
|
|
|
import shade
|
|
from shade import exc
|
|
from shade import meta
|
|
from shade.tests import fakes
|
|
from shade.tests.unit import base
|
|
|
|
|
|
CINDER_URL = 'https://volume.example.com/v2/1c36b64c840a42cd9e9b931a369337f0'
|
|
|
|
|
|
class BaseTestImage(base.RequestsMockTestCase):
|
|
|
|
def setUp(self):
|
|
super(BaseTestImage, self).setUp()
|
|
self.image_id = str(uuid.uuid4())
|
|
self.imagefile = tempfile.NamedTemporaryFile(delete=False)
|
|
self.imagefile.write(b'\0')
|
|
self.imagefile.close()
|
|
self.fake_image_dict = fakes.make_fake_image(image_id=self.image_id)
|
|
self.fake_search_return = {'images': [self.fake_image_dict]}
|
|
self.output = uuid.uuid4().bytes
|
|
|
|
|
|
class TestImage(BaseTestImage):
|
|
|
|
def setUp(self):
|
|
super(TestImage, self).setUp()
|
|
self.use_glance()
|
|
|
|
def test_config_v1(self):
|
|
self.cloud.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.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, 'fake_image')
|
|
|
|
def test_download_image_two_outputs(self):
|
|
fake_fd = six.BytesIO()
|
|
self.assertRaises(exc.OpenStackCloudException,
|
|
self.cloud.download_image, 'fake_image',
|
|
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',
|
|
json=dict(images=[]))])
|
|
self.assertRaises(exc.OpenStackCloudResourceNotFound,
|
|
self.cloud.download_image, 'fake_image',
|
|
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',
|
|
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'})
|
|
])
|
|
|
|
def test_download_image_with_fd(self):
|
|
self._register_image_mocks()
|
|
output_file = six.BytesIO()
|
|
self.cloud.download_image('fake_image', 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('fake_image', output_path=output_file.name)
|
|
output_file.seek(0)
|
|
self.assertEqual(output_file.read(), self.output)
|
|
self.assert_calls()
|
|
|
|
def test_empty_list_images(self):
|
|
self.register_uris([
|
|
dict(method='GET', uri='https://image.example.com/v2/images',
|
|
json={'images': []})
|
|
])
|
|
self.assertEqual([], self.cloud.list_images())
|
|
self.assert_calls()
|
|
|
|
def test_list_images(self):
|
|
self.register_uris([
|
|
dict(method='GET', uri='https://image.example.com/v2/images',
|
|
json=self.fake_search_return)
|
|
])
|
|
self.assertEqual(
|
|
self.cloud._normalize_images([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='https://image.example.com/v2/images',
|
|
json={'images': [image_dict]}),
|
|
])
|
|
images = self.cloud.list_images()
|
|
self.assertEqual(
|
|
self.cloud._normalize_images([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='https://image.example.com/v2/images',
|
|
json={'images': [self.fake_image_dict],
|
|
'next': '/v2/images?marker={marker}'.format(
|
|
marker=marker)}),
|
|
dict(method='GET',
|
|
uri=('https://image.example.com/v2/images?'
|
|
'marker={marker}'.format(marker=marker)),
|
|
json=self.fake_search_return)
|
|
])
|
|
self.assertEqual(
|
|
self.cloud._normalize_images([
|
|
self.fake_image_dict, self.fake_image_dict]),
|
|
self.cloud.list_images())
|
|
self.assert_calls()
|
|
|
|
def test_create_image_put_v2(self):
|
|
self.cloud.image_api_use_tasks = False
|
|
|
|
self.register_uris([
|
|
dict(method='GET', uri='https://image.example.com/v2/images',
|
|
json={'images': []}),
|
|
dict(method='POST', uri='https://image.example.com/v2/images',
|
|
json=self.fake_image_dict,
|
|
validate=dict(
|
|
json={u'container_format': u'bare',
|
|
u'disk_format': u'qcow2',
|
|
u'name': u'fake_image',
|
|
u'owner_specified.shade.md5': fakes.NO_MD5,
|
|
u'owner_specified.shade.object': u'images/fake_image', # noqa
|
|
u'owner_specified.shade.sha256': fakes.NO_SHA256,
|
|
u'visibility': u'private'})
|
|
),
|
|
dict(method='PUT',
|
|
uri='https://image.example.com/v2/images/{id}/file'.format(
|
|
id=self.image_id),
|
|
request_headers={'Content-Type': 'application/octet-stream'}),
|
|
dict(method='GET', uri='https://image.example.com/v2/images',
|
|
json=self.fake_search_return)
|
|
])
|
|
|
|
self.cloud.create_image(
|
|
'fake_image', self.imagefile.name, wait=True, timeout=1,
|
|
is_public=False)
|
|
|
|
self.assert_calls()
|
|
self.assertEqual(self.adapter.request_history[5].text.read(), b'\x00')
|
|
|
|
def test_create_image_task(self):
|
|
self.cloud.image_api_use_tasks = True
|
|
image_name = 'name-99'
|
|
container_name = 'image_upload_v2_test_container'
|
|
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.shade.md5'])
|
|
del(image_no_checksums['owner_specified.shade.sha256'])
|
|
del(image_no_checksums['owner_specified.shade.object'])
|
|
|
|
self.register_uris([
|
|
dict(method='GET', uri='https://image.example.com/v2/images',
|
|
json={'images': []}),
|
|
dict(method='GET', 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}'.format(
|
|
endpoint=endpoint, container=container_name),
|
|
status_code=404),
|
|
dict(method='PUT',
|
|
uri='{endpoint}/{container}'.format(
|
|
endpoint=endpoint, container=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=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', # noqa
|
|
'X-Container-Bytes-Used': '0',
|
|
'Content-Type': 'text/plain; charset=utf-8'}),
|
|
dict(method='HEAD',
|
|
uri='{endpoint}/{container}/{object}'.format(
|
|
endpoint=endpoint, container=container_name,
|
|
object=image_name),
|
|
status_code=404),
|
|
dict(method='PUT',
|
|
uri='{endpoint}/{container}/{object}'.format(
|
|
endpoint=endpoint, container=container_name,
|
|
object=image_name),
|
|
status_code=201,
|
|
validate=dict(
|
|
headers={'x-object-meta-x-shade-md5': fakes.NO_MD5,
|
|
'x-object-meta-x-shade-sha256': fakes.NO_SHA256})
|
|
),
|
|
dict(method='GET', uri='https://image.example.com/v2/images',
|
|
json={'images': []}),
|
|
dict(method='POST', uri='https://image.example.com/v2/tasks',
|
|
json=args,
|
|
validate=dict(
|
|
json=dict(
|
|
type='import', input={
|
|
'import_from': '{container}/{object}'.format(
|
|
container=container_name, object=image_name),
|
|
'image_properties': {'name': image_name}}))
|
|
),
|
|
dict(method='GET',
|
|
uri='https://image.example.com/v2/tasks/{id}'.format(
|
|
id=task_id),
|
|
status_code=503, text='Random error'),
|
|
dict(method='GET',
|
|
uri='https://image.example.com/v2/tasks/{id}'.format(
|
|
id=task_id),
|
|
json={'images': args}),
|
|
dict(method='GET', uri='https://image.example.com/v2/images',
|
|
json={'images': [image_no_checksums]}),
|
|
dict(method='PATCH',
|
|
uri='https://image.example.com/v2/images/{id}'.format(
|
|
id=self.image_id),
|
|
validate=dict(
|
|
json=sorted([{u'op': u'add',
|
|
u'value': '{container}/{object}'.format(
|
|
container=container_name,
|
|
object=image_name),
|
|
u'path': u'/owner_specified.shade.object'},
|
|
{u'op': u'add', u'value': fakes.NO_MD5,
|
|
u'path': u'/owner_specified.shade.md5'},
|
|
{u'op': u'add', u'value': fakes.NO_SHA256,
|
|
u'path': u'/owner_specified.shade.sha256'}],
|
|
key=operator.itemgetter('value')),
|
|
headers={
|
|
'Content-Type':
|
|
'application/openstack-images-v2.1-json-patch'})
|
|
),
|
|
dict(method='GET', uri='https://image.example.com/v2/images',
|
|
json=self.fake_search_return)
|
|
])
|
|
|
|
self.cloud.create_image(
|
|
image_name, self.imagefile.name, wait=True, timeout=1,
|
|
is_public=False, container=container_name)
|
|
|
|
self.assert_calls()
|
|
|
|
def _image_dict(self, fake_image):
|
|
return self.cloud._normalize_image(meta.obj_to_munch(fake_image))
|
|
|
|
def _munch_images(self, fake_image):
|
|
return self.cloud._normalize_images([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, **kwargs)
|
|
|
|
@mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version')
|
|
@mock.patch.object(shade.OpenStackCloud, '_image_client')
|
|
def test_create_image_put_v1(self, mock_image_client, mock_api_version):
|
|
mock_api_version.return_value = '1'
|
|
mock_image_client.get.return_value = []
|
|
self.assertEqual([], self.cloud.list_images())
|
|
|
|
args = {'name': '42 name',
|
|
'container_format': 'bare', 'disk_format': 'qcow2',
|
|
'properties': {
|
|
'owner_specified.shade.md5': mock.ANY,
|
|
'owner_specified.shade.sha256': mock.ANY,
|
|
'owner_specified.shade.object': 'images/42 name',
|
|
'is_public': False}}
|
|
ret = munch.Munch(args.copy())
|
|
ret['id'] = '42'
|
|
ret['status'] = 'success'
|
|
mock_image_client.get.side_effect = [
|
|
[],
|
|
[ret],
|
|
[ret],
|
|
]
|
|
mock_image_client.post.return_value = ret
|
|
mock_image_client.put.return_value = ret
|
|
self._call_create_image('42 name')
|
|
mock_image_client.post.assert_called_with('/images', json=args)
|
|
mock_image_client.put.assert_called_with(
|
|
'/images/42', data=mock.ANY,
|
|
headers={
|
|
'x-image-meta-checksum': mock.ANY,
|
|
'x-glance-registry-purge-props': 'false'
|
|
})
|
|
mock_image_client.get.assert_called_with('/images/detail')
|
|
self.assertEqual(
|
|
self._munch_images(ret), self.cloud.list_images())
|
|
|
|
@mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version')
|
|
@mock.patch.object(shade.OpenStackCloud, '_image_client')
|
|
def test_create_image_put_v1_bad_delete(
|
|
self, mock_image_client, mock_api_version):
|
|
mock_api_version.return_value = '1'
|
|
mock_image_client.get.return_value = []
|
|
self.assertEqual([], self.cloud.list_images())
|
|
|
|
args = {'name': '42 name',
|
|
'container_format': 'bare', 'disk_format': 'qcow2',
|
|
'properties': {
|
|
'owner_specified.shade.md5': mock.ANY,
|
|
'owner_specified.shade.sha256': mock.ANY,
|
|
'owner_specified.shade.object': 'images/42 name',
|
|
'is_public': False}}
|
|
ret = munch.Munch(args.copy())
|
|
ret['id'] = '42'
|
|
ret['status'] = 'success'
|
|
mock_image_client.get.side_effect = [
|
|
[],
|
|
[ret],
|
|
]
|
|
mock_image_client.post.return_value = ret
|
|
mock_image_client.put.side_effect = exc.OpenStackCloudHTTPError(
|
|
"Some error", {})
|
|
self.assertRaises(
|
|
exc.OpenStackCloudHTTPError,
|
|
self._call_create_image,
|
|
'42 name')
|
|
mock_image_client.post.assert_called_with('/images', json=args)
|
|
mock_image_client.put.assert_called_with(
|
|
'/images/42', data=mock.ANY,
|
|
headers={
|
|
'x-image-meta-checksum': mock.ANY,
|
|
'x-glance-registry-purge-props': 'false'
|
|
})
|
|
mock_image_client.delete.assert_called_with('/images/42')
|
|
|
|
@mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version')
|
|
@mock.patch.object(shade.OpenStackCloud, '_image_client')
|
|
def test_update_image_no_patch(self, mock_image_client, mock_api_version):
|
|
mock_api_version.return_value = '2'
|
|
self.cloud.image_api_use_tasks = False
|
|
|
|
mock_image_client.get.return_value = []
|
|
self.assertEqual([], self.cloud.list_images())
|
|
|
|
args = {'name': '42 name',
|
|
'container_format': 'bare', 'disk_format': 'qcow2',
|
|
'owner_specified.shade.md5': mock.ANY,
|
|
'owner_specified.shade.sha256': mock.ANY,
|
|
'owner_specified.shade.object': 'images/42 name',
|
|
'visibility': 'private',
|
|
'min_disk': 0, 'min_ram': 0}
|
|
ret = munch.Munch(args.copy())
|
|
ret['id'] = '42'
|
|
ret['status'] = 'success'
|
|
mock_image_client.get.side_effect = [
|
|
[],
|
|
[ret],
|
|
[ret],
|
|
]
|
|
self.cloud.update_image_properties(
|
|
image=self._image_dict(ret),
|
|
**{'owner_specified.shade.object': 'images/42 name'})
|
|
mock_image_client.get.assert_called_with('/images')
|
|
mock_image_client.patch.assert_not_called()
|
|
|
|
@mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version')
|
|
@mock.patch.object(shade.OpenStackCloud, '_image_client')
|
|
def test_create_image_put_v2_bad_delete(
|
|
self, mock_image_client, mock_api_version):
|
|
mock_api_version.return_value = '2'
|
|
self.cloud.image_api_use_tasks = False
|
|
|
|
mock_image_client.get.return_value = []
|
|
self.assertEqual([], self.cloud.list_images())
|
|
|
|
args = {'name': '42 name',
|
|
'container_format': 'bare', 'disk_format': 'qcow2',
|
|
'owner_specified.shade.md5': mock.ANY,
|
|
'owner_specified.shade.sha256': mock.ANY,
|
|
'owner_specified.shade.object': 'images/42 name',
|
|
'visibility': 'private',
|
|
'min_disk': 0, 'min_ram': 0}
|
|
ret = munch.Munch(args.copy())
|
|
ret['id'] = '42'
|
|
ret['status'] = 'success'
|
|
mock_image_client.get.side_effect = [
|
|
[],
|
|
[ret],
|
|
[ret],
|
|
]
|
|
mock_image_client.post.return_value = ret
|
|
mock_image_client.put.side_effect = exc.OpenStackCloudHTTPError(
|
|
"Some error", {})
|
|
self.assertRaises(
|
|
exc.OpenStackCloudHTTPError,
|
|
self._call_create_image,
|
|
'42 name', min_disk='0', min_ram=0)
|
|
mock_image_client.post.assert_called_with('/images', json=args)
|
|
mock_image_client.put.assert_called_with(
|
|
'/images/42/file',
|
|
headers={'Content-Type': 'application/octet-stream'},
|
|
data=mock.ANY)
|
|
mock_image_client.delete.assert_called_with('/images/42')
|
|
|
|
@mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version')
|
|
@mock.patch.object(shade.OpenStackCloud, '_image_client')
|
|
def test_create_image_put_bad_int(
|
|
self, mock_image_client, mock_api_version):
|
|
mock_api_version.return_value = '2'
|
|
self.cloud.image_api_use_tasks = False
|
|
|
|
self.assertRaises(
|
|
exc.OpenStackCloudException,
|
|
self._call_create_image, '42 name', min_disk='fish', min_ram=0)
|
|
mock_image_client.post.assert_not_called()
|
|
|
|
@mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version')
|
|
@mock.patch.object(shade.OpenStackCloud, '_image_client')
|
|
def test_create_image_put_user_int(
|
|
self, mock_image_client, mock_api_version):
|
|
mock_api_version.return_value = '2'
|
|
self.cloud.image_api_use_tasks = False
|
|
|
|
args = {'name': '42 name',
|
|
'container_format': 'bare', 'disk_format': u'qcow2',
|
|
'owner_specified.shade.md5': mock.ANY,
|
|
'owner_specified.shade.sha256': mock.ANY,
|
|
'owner_specified.shade.object': 'images/42 name',
|
|
'int_v': '12345',
|
|
'visibility': 'private',
|
|
'min_disk': 0, 'min_ram': 0}
|
|
ret = munch.Munch(args.copy())
|
|
ret['id'] = '42'
|
|
ret['status'] = 'success'
|
|
mock_image_client.get.side_effect = [
|
|
[],
|
|
[ret],
|
|
[ret]
|
|
]
|
|
mock_image_client.post.return_value = ret
|
|
self._call_create_image(
|
|
'42 name', min_disk='0', min_ram=0, int_v=12345)
|
|
mock_image_client.post.assert_called_with('/images', json=args)
|
|
mock_image_client.put.assert_called_with(
|
|
'/images/42/file',
|
|
headers={'Content-Type': 'application/octet-stream'},
|
|
data=mock.ANY)
|
|
mock_image_client.get.assert_called_with('/images')
|
|
self.assertEqual(
|
|
self._munch_images(ret), self.cloud.list_images())
|
|
|
|
@mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version')
|
|
@mock.patch.object(shade.OpenStackCloud, '_image_client')
|
|
def test_create_image_put_meta_int(
|
|
self, mock_image_client, mock_api_version):
|
|
mock_api_version.return_value = '2'
|
|
self.cloud.image_api_use_tasks = False
|
|
|
|
mock_image_client.get.return_value = []
|
|
self.assertEqual([], self.cloud.list_images())
|
|
|
|
self._call_create_image(
|
|
'42 name', min_disk='0', min_ram=0, meta={'int_v': 12345})
|
|
args = {'name': '42 name',
|
|
'container_format': 'bare', 'disk_format': u'qcow2',
|
|
'owner_specified.shade.md5': mock.ANY,
|
|
'owner_specified.shade.sha256': mock.ANY,
|
|
'owner_specified.shade.object': 'images/42 name',
|
|
'int_v': 12345,
|
|
'visibility': 'private',
|
|
'min_disk': 0, 'min_ram': 0}
|
|
ret = munch.Munch(args.copy())
|
|
ret['id'] = '42'
|
|
ret['status'] = 'success'
|
|
mock_image_client.get.return_value = [ret]
|
|
mock_image_client.post.return_value = ret
|
|
mock_image_client.get.assert_called_with('/images')
|
|
self.assertEqual(
|
|
self._munch_images(ret), self.cloud.list_images())
|
|
|
|
@mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version')
|
|
@mock.patch.object(shade.OpenStackCloud, '_image_client')
|
|
def test_create_image_put_protected(
|
|
self, mock_image_client, mock_api_version):
|
|
mock_api_version.return_value = '2'
|
|
self.cloud.image_api_use_tasks = False
|
|
|
|
mock_image_client.get.return_value = []
|
|
self.assertEqual([], self.cloud.list_images())
|
|
|
|
args = {'name': '42 name',
|
|
'container_format': 'bare', 'disk_format': u'qcow2',
|
|
'owner_specified.shade.md5': mock.ANY,
|
|
'owner_specified.shade.sha256': mock.ANY,
|
|
'owner_specified.shade.object': 'images/42 name',
|
|
'protected': False,
|
|
'int_v': '12345',
|
|
'visibility': 'private',
|
|
'min_disk': 0, 'min_ram': 0}
|
|
ret = munch.Munch(args.copy())
|
|
ret['id'] = '42'
|
|
ret['status'] = 'success'
|
|
mock_image_client.get.side_effect = [
|
|
[],
|
|
[ret],
|
|
[ret],
|
|
]
|
|
mock_image_client.put.return_value = ret
|
|
mock_image_client.post.return_value = ret
|
|
self._call_create_image(
|
|
'42 name', min_disk='0', min_ram=0, properties={'int_v': 12345},
|
|
protected=False)
|
|
mock_image_client.post.assert_called_with('/images', json=args)
|
|
mock_image_client.put.assert_called_with(
|
|
'/images/42/file', data=mock.ANY,
|
|
headers={'Content-Type': 'application/octet-stream'})
|
|
self.assertEqual(self._munch_images(ret), self.cloud.list_images())
|
|
|
|
@mock.patch.object(occ.cloud_config.CloudConfig, 'get_api_version')
|
|
@mock.patch.object(shade.OpenStackCloud, '_image_client')
|
|
def test_create_image_put_user_prop(
|
|
self, mock_image_client, mock_api_version):
|
|
mock_api_version.return_value = '2'
|
|
self.cloud.image_api_use_tasks = False
|
|
|
|
mock_image_client.get.return_value = []
|
|
self.assertEqual([], self.cloud.list_images())
|
|
|
|
args = {'name': '42 name',
|
|
'container_format': 'bare', 'disk_format': u'qcow2',
|
|
'owner_specified.shade.md5': mock.ANY,
|
|
'owner_specified.shade.sha256': mock.ANY,
|
|
'owner_specified.shade.object': 'images/42 name',
|
|
'int_v': '12345',
|
|
'xenapi_use_agent': 'False',
|
|
'visibility': 'private',
|
|
'min_disk': 0, 'min_ram': 0}
|
|
ret = munch.Munch(args.copy())
|
|
ret['id'] = '42'
|
|
ret['status'] = 'success'
|
|
mock_image_client.get.return_value = [ret]
|
|
mock_image_client.post.return_value = ret
|
|
self._call_create_image(
|
|
'42 name', min_disk='0', min_ram=0, properties={'int_v': 12345})
|
|
mock_image_client.get.assert_called_with('/images')
|
|
self.assertEqual(
|
|
self._munch_images(ret), self.cloud.list_images())
|
|
|
|
|
|
class TestImageV1Only(base.RequestsMockTestCase):
|
|
|
|
def setUp(self):
|
|
super(TestImageV1Only, self).setUp()
|
|
self.use_glance(image_version_json='image-version-v1.json')
|
|
|
|
def test_config_v1(self):
|
|
|
|
self.cloud.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.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.assertEqual(
|
|
'1', self.cloud_config.get_api_version('image'))
|
|
|
|
|
|
class TestImageV2Only(base.RequestsMockTestCase):
|
|
|
|
def setUp(self):
|
|
super(TestImageV2Only, self).setUp()
|
|
self.use_glance(image_version_json='image-version-v2.json')
|
|
|
|
def test_config_v1(self):
|
|
self.cloud.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.assertEqual(
|
|
'2', self.cloud_config.get_api_version('image'))
|
|
|
|
def test_config_v2(self):
|
|
self.cloud.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'))
|
|
|
|
|
|
class TestImageVersionDiscovery(BaseTestImage):
|
|
|
|
def test_version_discovery_skip(self):
|
|
self.cloud.cloud_config.config['image_endpoint_override'] = \
|
|
'https://image.example.com/v2/override'
|
|
|
|
self.register_uris([
|
|
dict(method='GET',
|
|
uri='https://image.example.com/v2/override/images',
|
|
json={'images': []})
|
|
])
|
|
self.assertEqual([], self.cloud.list_images())
|
|
self.assertEqual(
|
|
self.cloud._image_client.endpoint_override,
|
|
'https://image.example.com/v2/override')
|
|
self.assert_calls()
|
|
|
|
|
|
class TestImageVolume(BaseTestImage):
|
|
|
|
def test_create_image_volume(self):
|
|
|
|
volume_id = 'some-volume'
|
|
|
|
self.register_uris([
|
|
dict(method='POST',
|
|
uri='{endpoint}/volumes/{id}/action'.format(
|
|
endpoint=CINDER_URL, id=volume_id),
|
|
json={'os-volume_upload_image': {'image_id': self.image_id}},
|
|
validate=dict(json={
|
|
u'os-volume_upload_image': {
|
|
u'container_format': u'bare',
|
|
u'disk_format': u'qcow2',
|
|
u'force': False,
|
|
u'image_name': u'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='https://image.example.com/v2/images',
|
|
json=self.fake_search_return)
|
|
])
|
|
|
|
self.cloud.create_image(
|
|
'fake_image', self.imagefile.name, wait=True, timeout=1,
|
|
volume={'id': volume_id})
|
|
|
|
self.assert_calls()
|
|
|
|
def test_create_image_volume_duplicate(self):
|
|
|
|
volume_id = 'some-volume'
|
|
|
|
self.register_uris([
|
|
dict(method='POST',
|
|
uri='{endpoint}/volumes/{id}/action'.format(
|
|
endpoint=CINDER_URL, id=volume_id),
|
|
json={'os-volume_upload_image': {'image_id': self.image_id}},
|
|
validate=dict(json={
|
|
u'os-volume_upload_image': {
|
|
u'container_format': u'bare',
|
|
u'disk_format': u'qcow2',
|
|
u'force': True,
|
|
u'image_name': u'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='https://image.example.com/v2/images',
|
|
json=self.fake_search_return)
|
|
])
|
|
|
|
self.cloud.create_image(
|
|
'fake_image', self.imagefile.name, wait=True, timeout=1,
|
|
volume={'id': volume_id}, allow_duplicates=True)
|
|
|
|
self.assert_calls()
|
|
|
|
|
|
class TestImageBrokenDiscovery(base.RequestsMockTestCase):
|
|
|
|
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='https://image.example.com/v2/images',
|
|
json={'images': []})
|
|
])
|
|
self.assertEqual([], self.cloud.list_images())
|
|
self.assertEqual(
|
|
self.cloud._image_client.endpoint_override,
|
|
'https://image.example.com/v2/')
|
|
self.assert_calls()
|
|
|
|
|
|
class TestImageDiscoveryOptimization(base.RequestsMockTestCase):
|
|
|
|
def setUp(self):
|
|
super(TestImageDiscoveryOptimization, self).setUp()
|
|
self.use_keystone_v3(catalog='catalog-versioned-image.json')
|
|
|
|
def test_version_discovery_skip(self):
|
|
self.cloud.cloud_config.config['image_api_version'] = '2'
|
|
|
|
self.register_uris([
|
|
dict(method='GET',
|
|
uri='https://image.example.com/v2/images',
|
|
json={'images': []})
|
|
])
|
|
self.assertEqual([], self.cloud.list_images())
|
|
self.assert_calls()
|