Port S3 utils tests

Change-Id: I392e03f91a3809cdfe029c9a26969ecd74615ed6
This commit is contained in:
Feodor Tersin 2014-12-29 23:37:22 +04:00
parent b8067571ed
commit 855113ef2f
4 changed files with 268 additions and 69 deletions

View File

@ -523,8 +523,6 @@ def _block_device_properties_root_device_name(properties):
def _s3_create(context, metadata):
"""Gets a manifest from s3 and makes an image."""
image_path = tempfile.mkdtemp(dir=CONF.image_decryption_dir)
image_location = metadata['properties']['image_location'].lstrip('/')
bucket_name = image_location.split('/')[0]
manifest_path = image_location[len(bucket_name) + 1:]
@ -532,7 +530,21 @@ def _s3_create(context, metadata):
key = bucket.get_key(manifest_path)
manifest = key.get_contents_as_string()
manifest, image = _s3_parse_manifest(context, metadata, manifest)
(image_metadata, image_parts,
encrypted_key, encrypted_iv) = _s3_parse_manifest(context, manifest)
properties = metadata['properties']
properties.update(image_metadata['properties'])
properties['image_state'] = 'pending'
metadata.update(image_metadata)
metadata.update({'properties': properties,
'is_public': False})
# TODO(bcwaldon): right now, this removes user-defined ids
# We need to re-enable this.
metadata.pop('id', None)
glance = clients.glance(context)
image = glance.images.create(**metadata)
def _update_image_state(image_state):
image.update(properties={'image_state': image_state})
@ -540,16 +552,14 @@ def _s3_create(context, metadata):
def delayed_create():
"""This handles the fetching and decrypting of the part files."""
context.update_store()
try:
_update_image_state('downloading')
image_path = tempfile.mkdtemp(dir=CONF.image_decryption_dir)
_update_image_state('downloading')
try:
parts = []
elements = manifest.find('image').getiterator('filename')
for fn_element in elements:
part = _s3_download_file(bucket, fn_element.text,
image_path)
for part_name in image_parts:
part = _s3_download_file(bucket, part_name, image_path)
parts.append(part)
# NOTE(vish): this may be suboptimal, should we use cat?
@ -564,13 +574,7 @@ def _s3_create(context, metadata):
return
_update_image_state('decrypting')
try:
hex_key = manifest.find('image/ec2_encrypted_key').text
encrypted_key = binascii.a2b_hex(hex_key)
hex_iv = manifest.find('image/ec2_encrypted_iv').text
encrypted_iv = binascii.a2b_hex(hex_iv)
dec_filename = os.path.join(image_path, 'image.tar.gz')
_s3_decrypt_image(context, enc_filename, encrypted_key,
encrypted_iv, dec_filename)
@ -579,7 +583,6 @@ def _s3_create(context, metadata):
return
_update_image_state('untarring')
try:
unz_filename = _s3_untarzip_image(image_path, dec_filename)
except Exception:
@ -598,6 +601,10 @@ def _s3_create(context, metadata):
shutil.rmtree(image_path)
except glance_exception.HTTPNotFound:
# TODO(ft): the image was deleted underneath us, add logging
return
except Exception:
# TODO(ft): add logging
return
eventlet.spawn_n(delayed_create)
@ -605,42 +612,16 @@ def _s3_create(context, metadata):
return image
def _s3_parse_manifest(context, metadata, manifest):
def _s3_parse_manifest(context, manifest):
manifest = etree.fromstring(manifest)
image_format = 'ami'
try:
kernel_id = manifest.find('machine_configuration/kernel_id').text
if kernel_id == 'true':
image_format = 'aki'
kernel_id = None
except Exception:
kernel_id = None
try:
ramdisk_id = manifest.find('machine_configuration/ramdisk_id').text
if ramdisk_id == 'true':
image_format = 'ari'
ramdisk_id = None
except Exception:
ramdisk_id = None
try:
arch = manifest.find('machine_configuration/architecture').text
except Exception:
arch = 'x86_64'
# NOTE(yamahata):
# EC2 ec2-budlne-image --block-device-mapping accepts
# <virtual name>=<device name> where
# virtual name = {ami, root, swap, ephemeral<N>}
# where N is no negative integer
# device name = the device name seen by guest kernel.
# They are converted into
# block_device_mapping/mapping/{virtual, device}
#
# Do NOT confuse this with ec2-register's block device mapping
# argument.
properties = {'architecture': arch}
mappings = []
try:
block_device_mapping = manifest.findall('machine_configuration/'
@ -652,36 +633,39 @@ def _s3_parse_manifest(context, metadata, manifest):
except Exception:
mappings = []
properties = metadata['properties']
properties['architecture'] = arch
def _translate_dependent_image_id(image_key, image_id):
image_uuid = ec2utils.ec2_id_to_glance_id(context, image_id)
properties[image_key] = image_uuid
if kernel_id:
_translate_dependent_image_id('kernel_id', kernel_id)
if ramdisk_id:
_translate_dependent_image_id('ramdisk_id', ramdisk_id)
if mappings:
properties['mappings'] = mappings
metadata.update({'disk_format': image_format,
'container_format': image_format,
'is_public': False,
'properties': properties})
metadata['properties']['image_state'] = 'pending'
image_format = 'ami'
# TODO(bcwaldon): right now, this removes user-defined ids
# We need to re-enable this.
metadata.pop('id', None)
def set_dependent_image_id(image_key, kind):
try:
image_key_path = ('machine_configuration/%(image_key)s' %
{'image_key': image_key})
image_id = manifest.find(image_key_path).text
except Exception:
return
if image_id == 'true':
image_format = kind
else:
images = db_api.get_public_items(context, kind, (image_id,))
image = (images[0] if len(images) else
ec2utils.get_db_item(context, kind, image_id))
properties[image_key] = image['os_id']
glance = clients.glance(context)
image = glance.images.create(**metadata)
set_dependent_image_id('kernel_id', 'aki')
set_dependent_image_id('ramdisk_id', 'ari')
return manifest, image
metadata = {'disk_format': image_format,
'container_format': image_format,
'properties': properties}
image_parts = [
fn_element.text
for fn_element in manifest.find('image').getiterator('filename')]
encrypted_key = manifest.find('image/ec2_encrypted_key').text
encrypted_iv = manifest.find('image/ec2_encrypted_iv').text
return metadata, image_parts, encrypted_key, encrypted_iv
def _s3_download_file(bucket, filename, local_dir):
@ -693,6 +677,8 @@ def _s3_download_file(bucket, filename, local_dir):
def _s3_decrypt_image(context, encrypted_filename, encrypted_key,
encrypted_iv, decrypted_filename):
encrypted_key = binascii.a2b_hex(encrypted_key)
encrypted_iv = binascii.a2b_hex(encrypted_iv)
cert_client = clients.nova_cert(context)
try:
key = cert_client.decrypt_text(base64.b64encode(encrypted_key))

BIN
ec2api/tests/abs.tar.gz Normal file

Binary file not shown.

BIN
ec2api/tests/rel.tar.gz Normal file

Binary file not shown.

View File

@ -13,17 +13,83 @@
# 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.image._s3_create')
@ -298,3 +364,150 @@ class ImagePrivateTestCase(test_base.BaseTestCase):
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'))