Files
ec2-api/ec2api/tests/test_image.py
Feodor Tersin 8955dc133d Port create image tests
Change-Id: I2ac0cf314ae40a40f7b364cffe4fd71c5216891a
2015-01-20 18:41:15 +04:00

570 lines
24 KiB
Python

# Copyright 2014
# The Cloudscaling Group, Inc.
#
# 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 copy
import os
import tempfile
import eventlet
import mock
from oslo.config import cfg
from oslotest import base as test_base
from ec2api.api import image as image_api
from ec2api import exception
from ec2api.tests import base
from ec2api.tests import fakes
from ec2api.tests import matchers
from ec2api.tests import tools
AMI_MANIFEST_XML = """<?xml version="1.0" ?>
<manifest>
<version>2011-06-17</version>
<bundler>
<name>test-s3</name>
<version>0</version>
<release>0</release>
</bundler>
<machine_configuration>
<architecture>x86_64</architecture>
<block_device_mapping>
<mapping>
<virtual>ami</virtual>
<device>sda1</device>
</mapping>
<mapping>
<virtual>root</virtual>
<device>/dev/sda1</device>
</mapping>
<mapping>
<virtual>ephemeral0</virtual>
<device>sda2</device>
</mapping>
<mapping>
<virtual>swap</virtual>
<device>sda3</device>
</mapping>
</block_device_mapping>
<kernel_id>%(aki-id)s</kernel_id>
<ramdisk_id>%(ari-id)s</ramdisk_id>
</machine_configuration>
<image>
<ec2_encrypted_key>foo</ec2_encrypted_key>
<user_encrypted_key>foo</user_encrypted_key>
<ec2_encrypted_iv>foo</ec2_encrypted_iv>
<parts count="1">
<part index="0">
<filename>foo</filename>
</part>
</parts>
</image>
</manifest>
""" % {'aki-id': fakes.ID_EC2_IMAGE_AKI_1,
'ari-id': fakes.ID_EC2_IMAGE_ARI_1}
FILE_MANIFEST_XML = """<?xml version="1.0" ?>
<manifest>
<image>
<ec2_encrypted_key>foo</ec2_encrypted_key>
<user_encrypted_key>foo</user_encrypted_key>
<ec2_encrypted_iv>foo</ec2_encrypted_iv>
<parts count="1">
<part index="0">
<filename>foo</filename>
</part>
</parts>
</image>
</manifest>
"""
class ImageTestCase(base.ApiTestCase):
@mock.patch('ec2api.api.instance._is_ebs_instance')
def _test_create_image(self, instance_status, no_reboot, is_ebs_instance):
self.db_api.get_item_by_id.return_value = fakes.DB_INSTANCE_2
os_instance = mock.MagicMock()
os_instance.configure_mock(id=fakes.ID_OS_INSTANCE_2,
status=instance_status)
stop_called = iter([False, True])
os_instance.stop.side_effect = lambda: next(stop_called)
os_instance.get.side_effect = lambda: (setattr(os_instance, 'status',
'SHUTOFF')
if next(stop_called) else None)
os_image = mock.MagicMock()
os_image.configure_mock(id=fakes.random_os_id())
os_instance.create_image.return_value = os_image
self.nova_servers.get.return_value = os_instance
is_ebs_instance.return_value = True
image_id = fakes.random_ec2_id('ami')
self.db_api.add_item.side_effect = fakes.get_db_api_add_item(image_id)
resp = self.execute('CreateImage',
{'InstanceId': fakes.ID_EC2_INSTANCE_2,
'Name': 'fake_name',
'NoReboot': str(no_reboot)})
self.assertEqual({'http_status_code': 200,
'imageId': image_id},
resp)
self.db_api.get_item_by_id.assert_called_once_with(
mock.ANY, 'i', fakes.ID_EC2_INSTANCE_2)
self.nova_servers.get.assert_called_once_with(fakes.ID_OS_INSTANCE_2)
is_ebs_instance.assert_called_once_with(mock.ANY, os_instance)
self.db_api.add_item.assert_called_once_with(
mock.ANY, 'ami', {'os_id': os_image.id,
'is_public': False})
if not no_reboot:
os_instance.stop.assert_called_once_with()
os_instance.get.assert_called_once_with()
os_instance.start.assert_called_once_with()
os_instance.create_image.assert_called_once_with('fake_name')
self.db_api.reset_mock()
self.nova_servers.reset_mock()
def test_create_image(self):
self._test_create_image('ACTIVE', False)
self._test_create_image('SHUTOFF', True)
@mock.patch('ec2api.api.instance._is_ebs_instance')
def test_create_image_invalid_parameters(self, is_ebs_instance):
self.db_api.get_item_by_id.return_value = fakes.DB_INSTANCE_1
is_ebs_instance.return_value = False
resp = self.execute('CreateImage',
{'InstanceId': fakes.ID_EC2_INSTANCE_1,
'Name': 'fake_name'})
self.assertEqual(400, resp['http_status_code'])
self.assertEqual('InvalidParameterValue', resp['Error']['Code'])
@mock.patch('ec2api.api.image._s3_create')
def test_register_image(self, s3_create):
s3_create.return_value = fakes.OSImage(fakes.OS_IMAGE_1)
self.db_api.add_item.side_effect = (
fakes.get_db_api_add_item(fakes.ID_EC2_IMAGE_1))
resp = self.execute(
'RegisterImage',
{'ImageLocation': fakes.LOCATION_IMAGE_1})
self.assertThat(resp, matchers.DictMatches(
{'http_status_code': 200,
'imageId': fakes.ID_EC2_IMAGE_1}))
s3_create.assert_called_once_with(
mock.ANY,
{'name': fakes.LOCATION_IMAGE_1,
'properties': {'image_location': fakes.LOCATION_IMAGE_1}})
s3_create.reset_mock()
resp = self.execute(
'RegisterImage',
{'ImageLocation': fakes.LOCATION_IMAGE_1,
'Name': 'an image name'})
self.assertThat(resp, matchers.DictMatches(
{'http_status_code': 200,
'imageId': fakes.ID_EC2_IMAGE_1}))
s3_create.assert_called_once_with(
mock.ANY,
{'name': 'an image name',
'properties': {'image_location': fakes.LOCATION_IMAGE_1}})
def test_register_image_invalid_parameters(self):
resp = self.execute('RegisterImage', {})
self.assertEqual(400, resp['http_status_code'])
self.assertEqual('MissingParameter', resp['Error']['Code'])
def test_deregister_image(self):
self._setup_model()
resp = self.execute('DeregisterImage',
{'ImageId': fakes.ID_EC2_IMAGE_1})
self.assertThat(resp, matchers.DictMatches({'http_status_code': 200,
'return': True}))
self.db_api.delete_item.assert_called_once_with(
mock.ANY, fakes.ID_EC2_IMAGE_1)
self.glance.images.delete.assert_called_once_with(
fakes.ID_OS_IMAGE_1)
def test_deregister_image_invalid_parameters(self):
self._setup_model()
resp = self.execute('DeregisterImage',
{'ImageId': fakes.random_ec2_id('ami')})
self.assertEqual(400, resp['http_status_code'])
self.assertEqual('InvalidAMIID.NotFound', resp['Error']['Code'])
def test_describe_images(self):
self._setup_model()
resp = self.execute('DescribeImages', {})
self.assertEqual(200, resp['http_status_code'])
resp.pop('http_status_code')
self.assertThat(resp, matchers.DictMatches(
{'imagesSet': [fakes.EC2_IMAGE_1, fakes.EC2_IMAGE_2]},
orderless_lists=True))
self.db_api.get_items_by_ids.assert_any_call(mock.ANY, 'ami', set([]))
self.db_api.get_items_by_ids = tools.CopyingMock(
side_effect=self.db_api.get_items_by_ids.side_effect)
resp = self.execute('DescribeImages',
{'ImageId.1': fakes.ID_EC2_IMAGE_1})
self.assertEqual(200, resp['http_status_code'])
resp.pop('http_status_code')
self.assertThat(resp, matchers.DictMatches(
{'imagesSet': [fakes.EC2_IMAGE_1]},
orderless_lists=True))
self.db_api.get_items_by_ids.assert_any_call(
mock.ANY, 'ami', set([fakes.ID_EC2_IMAGE_1]))
def test_describe_images_invalid_parameters(self):
self._setup_model()
resp = self.execute('DescribeImages',
{'ImageId.1': fakes.random_ec2_id('ami')})
self.assertEqual(400, resp['http_status_code'])
self.assertEqual('InvalidAMIID.NotFound', resp['Error']['Code'])
self.glance.images.list.side_effect = lambda: []
resp = self.execute('DescribeImages',
{'ImageId.1': fakes.ID_EC2_IMAGE_1})
self.assertEqual(400, resp['http_status_code'])
self.assertEqual('InvalidAMIID.NotFound', resp['Error']['Code'])
def test_describe_image_attributes(self):
self._setup_model()
def do_check(attr, ec2_image_id, response):
self.db_api.reset_mock()
self.glance.reset_mock()
resp = self.execute('DescribeImageAttribute',
{'ImageId': ec2_image_id,
'Attribute': attr})
response['http_status_code'] = 200
response['imageId'] = ec2_image_id
self.assertThat(resp, matchers.DictMatches(response,
orderless_lists=True))
do_check('launchPermission',
fakes.ID_EC2_IMAGE_2,
{'launchPermission': [{'group': 'all'}]})
do_check('kernel',
fakes.ID_EC2_IMAGE_1,
{'kernel': {'value': fakes.ID_EC2_IMAGE_AKI_1}})
do_check('ramdisk',
fakes.ID_EC2_IMAGE_1,
{'ramdisk': {'value': fakes.ID_EC2_IMAGE_ARI_1}})
do_check('rootDeviceName',
fakes.ID_EC2_IMAGE_1,
{'rootDeviceName': fakes.ROOT_DEVICE_NAME_IMAGE_1})
do_check('rootDeviceName',
fakes.ID_EC2_IMAGE_2,
{'rootDeviceName': fakes.ROOT_DEVICE_NAME_IMAGE_2})
do_check('blockDeviceMapping',
fakes.ID_EC2_IMAGE_1,
{'blockDeviceMapping':
fakes.EC2_IMAGE_1['blockDeviceMapping']})
do_check('blockDeviceMapping',
fakes.ID_EC2_IMAGE_2,
{'blockDeviceMapping':
fakes.EC2_IMAGE_2['blockDeviceMapping']})
@mock.patch.object(fakes.OSImage, 'update', autospec=True)
def test_modify_image_attributes(self, osimage_update):
self._setup_model()
resp = self.execute('ModifyImageAttribute',
{'imageId': fakes.ID_EC2_IMAGE_1,
'attribute': 'launchPermission',
'operationType': 'add',
'userGroup.1': 'all'})
self.assertThat(resp, matchers.DictMatches({'http_status_code': 200,
'return': True}))
osimage_update.assert_called_once_with(
mock.ANY, is_public=True)
self.assertEqual(fakes.ID_OS_IMAGE_1,
osimage_update.call_args[0][0].id)
def _setup_model(self):
self.db_api.get_item_by_id.side_effect = (
fakes.get_db_api_get_item_by_id({
fakes.ID_EC2_IMAGE_1: fakes.DB_IMAGE_1,
fakes.ID_EC2_IMAGE_2: fakes.DB_IMAGE_2}))
self.db_api.get_items_by_ids.side_effect = (
fakes.get_db_api_get_items({
'ami': [fakes.DB_IMAGE_1, fakes.DB_IMAGE_2],
'ari': [],
'aki': []}))
self.db_api.get_items.side_effect = (
fakes.get_db_api_get_items({
'snap': [fakes.DB_SNAPSHOT_1, fakes.DB_SNAPSHOT_2]}))
self.db_api.get_public_items.return_value = []
self.db_api.get_item_ids.side_effect = (
fakes.get_db_api_get_item_by_id({
(fakes.ID_OS_IMAGE_ARI_1,): [(fakes.ID_EC2_IMAGE_ARI_1,
fakes.ID_OS_IMAGE_ARI_1)],
(fakes.ID_OS_IMAGE_AKI_1,): [(fakes.ID_EC2_IMAGE_AKI_1,
fakes.ID_OS_IMAGE_AKI_1)],
(fakes.ID_OS_SNAPSHOT_1,): [(fakes.ID_EC2_SNAPSHOT_1,
fakes.ID_OS_SNAPSHOT_1)],
(fakes.ID_OS_SNAPSHOT_2,): [(fakes.ID_EC2_SNAPSHOT_2,
fakes.ID_OS_SNAPSHOT_2)],
(fakes.ID_OS_VOLUME_1,): [(fakes.ID_EC2_VOLUME_1,
fakes.ID_OS_VOLUME_1)],
(fakes.ID_OS_VOLUME_2,): [(fakes.ID_EC2_VOLUME_2,
fakes.ID_OS_VOLUME_2)]}))
self.glance.images.list.side_effect = (
lambda: [fakes.OSImage(fakes.OS_IMAGE_1),
fakes.OSImage(fakes.OS_IMAGE_2)])
self.glance.images.get.side_effect = (
lambda os_id: (fakes.OSImage(fakes.OS_IMAGE_1)
if os_id == fakes.ID_OS_IMAGE_1 else
fakes.OSImage(fakes.OS_IMAGE_2)
if os_id == fakes.ID_OS_IMAGE_2 else
None))
class ImagePrivateTestCase(test_base.BaseTestCase):
def test_format_image(self):
image_ids = {fakes.ID_OS_IMAGE_1: fakes.ID_EC2_IMAGE_1,
fakes.ID_OS_IMAGE_AKI_1: fakes.ID_EC2_IMAGE_AKI_1,
fakes.ID_OS_IMAGE_ARI_1: fakes.ID_EC2_IMAGE_ARI_1}
os_image = copy.deepcopy(fakes.OS_IMAGE_1)
os_image['properties'] = {'image_location': 'location'}
os_image['name'] = None
image = image_api._format_image(
'fake_context', fakes.DB_IMAGE_1, fakes.OSImage(os_image),
None, image_ids)
self.assertEqual('location', image['imageLocation'])
self.assertEqual('location', image['name'])
os_image['properties'] = {}
os_image['name'] = 'fake_name'
image = image_api._format_image(
'fake_context', fakes.DB_IMAGE_1, fakes.OSImage(os_image),
None, image_ids)
self.assertEqual('None (fake_name)', image['imageLocation'])
self.assertEqual('fake_name', image['name'])
def test_cloud_format_mappings(self):
properties = {
'mappings': [
{'virtual': 'ami', 'device': '/dev/sda'},
{'virtual': 'root', 'device': 'sda'},
{'virtual': 'ephemeral0', 'device': 'sdb'},
{'virtual': 'swap', 'device': 'sdc'},
{'virtual': 'ephemeral1', 'device': 'sdd'},
{'virtual': 'ephemeral2', 'device': 'sde'},
{'virtual': 'ephemeral', 'device': 'sdf'},
{'virtual': '/dev/sdf1', 'device': 'root'}],
}
expected = {
'blockDeviceMapping': [
{'virtualName': 'ephemeral0', 'deviceName': '/dev/sdb'},
{'virtualName': 'swap', 'deviceName': '/dev/sdc'},
{'virtualName': 'ephemeral1', 'deviceName': '/dev/sdd'},
{'virtualName': 'ephemeral2', 'deviceName': '/dev/sde'},
]
}
result = {}
image_api._cloud_format_mappings('fake_context', properties, result)
self.assertThat(result,
matchers.DictMatches(expected, orderless_lists=True))
def test_block_device_properties_root_device_name(self):
root_device0 = '/dev/sda'
root_device1 = '/dev/sdb'
mappings = [{'virtual': 'root',
'device': root_device0}]
properties0 = {'mappings': mappings}
properties1 = {'mappings': mappings,
'root_device_name': root_device1}
self.assertIsNone(
image_api._block_device_properties_root_device_name({}))
self.assertEqual(
image_api._block_device_properties_root_device_name(properties0),
root_device0)
self.assertEqual(
image_api._block_device_properties_root_device_name(properties1),
root_device1)
class S3TestCase(base.ApiTestCase):
# TODO(ft): 'execute' feature isn't used here, but some mocks and
# fake context are. ApiTestCase should be split to some classes to use
# its feature optimally
def test_s3_parse_manifest(self):
self.db_api.get_public_items.side_effect = (
fakes.get_db_api_get_items({
'aki': ({'id': fakes.ID_EC2_IMAGE_AKI_1,
'os_id': fakes.ID_OS_IMAGE_AKI_1},),
'ari': ({'id': fakes.ID_EC2_IMAGE_ARI_1,
'os_id': fakes.ID_OS_IMAGE_ARI_1},)}))
self.db_api.get_item_by_id.return_value = None
fake_context = self._create_context()
metadata, image_parts, key, iv = image_api._s3_parse_manifest(
fake_context, AMI_MANIFEST_XML)
expected_metadata = {
'disk_format': 'ami',
'container_format': 'ami',
'properties': {'architecture': 'x86_64',
'kernel_id': fakes.ID_OS_IMAGE_AKI_1,
'ramdisk_id': fakes.ID_OS_IMAGE_ARI_1,
'mappings': [
{"device": "sda1", "virtual": "ami"},
{"device": "/dev/sda1", "virtual": "root"},
{"device": "sda2", "virtual": "ephemeral0"},
{"device": "sda3", "virtual": "swap"}]}}
self.assertThat(metadata,
matchers.DictMatches(expected_metadata,
orderless_lists=True))
self.assertThat(image_parts,
matchers.ListMatches(['foo']))
self.assertEqual('foo', key)
self.assertEqual('foo', iv)
self.db_api.get_public_items.assert_any_call(
mock.ANY, 'aki', (fakes.ID_EC2_IMAGE_AKI_1,))
self.db_api.get_public_items.assert_any_call(
mock.ANY, 'ari', (fakes.ID_EC2_IMAGE_ARI_1,))
@mock.patch.object(fakes.OSImage, 'update', autospec=True)
def test_s3_create_image_locations(self, osimage_update):
conf = cfg.CONF
conf.set_override('image_decryption_dir', None)
self.addCleanup(conf.reset)
_handle, tempf = tempfile.mkstemp()
fake_context = self._create_context()
with mock.patch(
'ec2api.api.image._s3_conn') as s3_conn, mock.patch(
'ec2api.api.image._s3_download_file'
) as s3_download_file, mock.patch(
'ec2api.api.image._s3_decrypt_image'
) as s3_decrypt_image, mock.patch(
'ec2api.api.image._s3_untarzip_image'
) as s3_untarzip_image:
(s3_conn.return_value.
get_bucket.return_value.
get_key.return_value.
get_contents_as_string.return_value) = FILE_MANIFEST_XML
s3_download_file.return_value = tempf
s3_untarzip_image.return_value = tempf
(self.glance.images.create.return_value) = (
fakes.OSImage({'id': fakes.random_os_id(),
'owner': fakes.ID_OS_PROJECT,
'is_public': False,
'status': 'queued',
'container_format': 'ami',
'name': 'fake_name',
'properties': {}}))
data = [
({'properties': {
'image_location': 'testbucket_1/test.img.manifest.xml'}},
'testbucket_1', 'test.img.manifest.xml'),
({'properties': {
'image_location': '/testbucket_2/test.img.manifest.xml'}},
'testbucket_2', 'test.img.manifest.xml')]
for mdata, bucket, manifest in data:
image = image_api._s3_create(fake_context, mdata)
eventlet.sleep()
osimage_update.assert_called_with(
image, properties={'image_state': 'available'})
osimage_update.assert_any_call(
image, data=mock.ANY)
s3_conn.return_value.get_bucket.assert_called_with(bucket)
(s3_conn.return_value.get_bucket.return_value.
get_key.assert_called_with(manifest))
(s3_conn.return_value.get_bucket.return_value.
get_key.return_value.
get_contents_as_string.assert_called_with())
s3_download_file.assert_called_with(
s3_conn.return_value.get_bucket.return_value,
'foo', mock.ANY)
s3_decrypt_image.assert_called_with(
fake_context, mock.ANY, 'foo', 'foo', mock.ANY)
s3_untarzip_image.assert_called_with(mock.ANY, mock.ANY)
@mock.patch('ec2api.api.image.eventlet.spawn_n')
def test_s3_create_bdm(self, spawn_n):
metadata = {'properties': {
'image_location': 'fake_bucket/fake_manifest',
'root_device_name': '/dev/sda1',
'block_device_mapping': [
{'device_name': '/dev/sda1',
'snapshot_id': fakes.ID_OS_SNAPSHOT_1,
'delete_on_termination': True},
{'device_name': '/dev/sda2',
'virtual_name': 'ephemeral0'},
{'device_name': '/dev/sdb0',
'no_device': True}]}}
fake_context = self._create_context()
with mock.patch(
'ec2api.api.image._s3_conn') as s3_conn:
(s3_conn.return_value.
get_bucket.return_value.
get_key.return_value.
get_contents_as_string.return_value) = FILE_MANIFEST_XML
image_api._s3_create(fake_context, metadata)
self.glance.images.create.assert_called_once_with(
disk_format='ami', container_format='ami', is_public=False,
properties={'architecture': 'x86_64',
'image_state': 'pending',
'root_device_name': '/dev/sda1',
'block_device_mapping': [
{'device_name': '/dev/sda1',
'snapshot_id': fakes.ID_OS_SNAPSHOT_1,
'delete_on_termination': True},
{'device_name': '/dev/sda2',
'virtual_name': 'ephemeral0'},
{'device_name': '/dev/sdb0',
'no_device': True}],
'image_location': 'fake_bucket/fake_manifest'})
def test_s3_malicious_tarballs(self):
self.assertRaises(exception.Invalid,
image_api._s3_test_for_malicious_tarball,
"/unused", os.path.join(os.path.dirname(__file__), 'abs.tar.gz'))
self.assertRaises(exception.Invalid,
image_api._s3_test_for_malicious_tarball,
"/unused", os.path.join(os.path.dirname(__file__), 'rel.tar.gz'))