Merge pull request #180 from DreadPirateShawn/AMIsModifyAndDescribe

AMIs: Added ModifyImageAttribute and DescribeImages filtering.
This commit is contained in:
Steve Pulec 2014-08-27 21:28:15 -04:00
commit cfda83dcba
5 changed files with 296 additions and 27 deletions

View File

@ -119,6 +119,14 @@ class InvalidAMIAttributeItemValueError(EC2ClientError):
.format(value, attribute))
class MalformedAMIIdError(EC2ClientError):
def __init__(self, ami_id):
super(MalformedAMIIdError, self).__init__(
"InvalidAMIID.Malformed",
"Invalid id: \"{0}\" (expecting \"ami-...\")"
.format(ami_id))
class InvalidSnapshotIdError(EC2ClientError):
def __init__(self, snapshot_id):
super(InvalidSnapshotIdError, self).__init__(

View File

@ -29,6 +29,7 @@ from .exceptions import (
InvalidSecurityGroupNotFoundError,
InvalidPermissionNotFoundError,
InvalidInstanceIdError,
MalformedAMIIdError,
InvalidAMIIdError,
InvalidAMIAttributeItemValueError,
InvalidSnapshotIdError,
@ -90,6 +91,14 @@ class Instance(BotoInstance, TaggedEC2Instance):
self.block_device_mapping = BlockDeviceMapping()
self.block_device_mapping['/dev/sda1'] = BlockDeviceType()
amis = ec2_backend.describe_images(filters={'image-id': image_id})
ami = amis[0] if amis else None
self.platform = ami.platform if ami else None
self.virtualization_type = ami.virtualization_type if ami else 'paravirtual'
self.architecture = ami.architecture if ami else 'x86_64'
@classmethod
def create_from_cloudformation_json(cls, resource_name, cloudformation_json):
properties = cloudformation_json['Properties']
@ -328,13 +337,34 @@ class TagBackend(object):
class Ami(TaggedEC2Instance):
def __init__(self, ami_id, instance, name, description):
self.id = ami_id
self.state = "available"
self.instance = instance
self.instance_id = instance.id
self.virtualization_type = instance.virtualization_type
self.architecture = instance.architecture
self.kernel_id = instance.kernel
self.platform = instance.platform
self.name = name
self.description = description
self.launch_permission_groups = set()
self.virtualization_type = instance.virtualization_type
self.kernel_id = instance.kernel
# AWS auto-creates these, we should reflect the same.
volume = ec2_backend.create_volume(15, "us-east-1a")
self.ebs_snapshot = ec2_backend.create_snapshot(volume.id, "Auto-created snapshot for AMI %s" % self.id)
def get_filter_value(self, filter_name):
if filter_name == 'virtualization-type':
return self.virtualization_type
elif filter_name == 'kernel-id':
return self.kernel_id
elif filter_name in ['architecture', 'platform']:
return getattr(self,filter_name)
elif filter_name == 'image-id':
return self.id
else:
ec2_backend.raise_not_implemented_error("The filter '{0}' for DescribeImages".format(filter_name))
class AmiBackend(object):
@ -350,14 +380,22 @@ class AmiBackend(object):
self.amis[ami_id] = ami
return ami
def describe_images(self, ami_ids=()):
images = []
for ami_id in ami_ids:
if ami_id in self.amis:
images.append(self.amis[ami_id])
else:
raise InvalidAMIIdError(ami_id)
return images or self.amis.values()
def describe_images(self, ami_ids=(), filters=None):
if filters:
images = self.amis.values()
for (_filter, _filter_value) in filters.iteritems():
images = [ ami for ami in images if ami.get_filter_value(_filter) in _filter_value ]
return images
else:
images = []
for ami_id in ami_ids:
if ami_id in self.amis:
images.append(self.amis[ami_id])
elif not ami_id.startswith("ami-"):
raise MalformedAMIIdError(ami_id)
else:
raise InvalidAMIIdError(ami_id)
return images or self.amis.values()
def deregister_image(self, ami_id):
if ami_id in self.amis:
@ -365,6 +403,30 @@ class AmiBackend(object):
return True
raise InvalidAMIIdError(ami_id)
def get_launch_permission_groups(self, ami_id):
ami = self.describe_images(ami_ids=[ami_id])[0]
return ami.launch_permission_groups
def add_launch_permission(self, ami_id, user_id=None, group=None):
if user_id:
ec2_backend.raise_not_implemented_error("The UserId parameter for ModifyImageAttribute")
if group != 'all':
raise InvalidAMIAttributeItemValueError("UserGroup", group)
ami = self.describe_images(ami_ids=[ami_id])[0]
ami.launch_permission_groups.add(group)
return True
def remove_launch_permission(self, ami_id, user_id=None, group=None):
if user_id:
ec2_backend.raise_not_implemented_error("The UserId parameter for ModifyImageAttribute")
if group != 'all':
raise InvalidAMIAttributeItemValueError("UserGroup", group)
ami = self.describe_images(ami_ids=[ami_id])[0]
ami.launch_permission_groups.discard(group)
return True
class Region(object):
def __init__(self, name, endpoint):

View File

@ -3,7 +3,7 @@ from jinja2 import Template
from moto.core.responses import BaseResponse
from moto.ec2.models import ec2_backend
from moto.ec2.utils import instance_ids_from_querystring, image_ids_from_querystring
from moto.ec2.utils import instance_ids_from_querystring, image_ids_from_querystring, filters_from_querystring
class AmisResponse(BaseResponse):
@ -25,17 +25,29 @@ class AmisResponse(BaseResponse):
template = Template(DEREGISTER_IMAGE_RESPONSE)
return template.render(success=str(success).lower())
def describe_image_attribute(self):
raise NotImplementedError('AMIs.describe_image_attribute is not yet implemented')
def describe_images(self):
ami_ids = image_ids_from_querystring(self.querystring)
images = ec2_backend.describe_images(ami_ids=ami_ids)
filters = filters_from_querystring(self.querystring)
images = ec2_backend.describe_images(ami_ids=ami_ids, filters=filters)
template = Template(DESCRIBE_IMAGES_RESPONSE)
return template.render(images=images)
def describe_image_attribute(self):
ami_id = self.querystring.get('ImageId')[0]
groups = ec2_backend.get_launch_permission_groups(ami_id)
template = Template(DESCRIBE_IMAGE_ATTRIBUTES_RESPONSE)
return template.render(ami_id=ami_id, groups=groups)
def modify_image_attribute(self):
raise NotImplementedError('AMIs.modify_image_attribute is not yet implemented')
ami_id = self.querystring.get('ImageId')[0]
operation_type = self.querystring.get('OperationType')[0]
group = self.querystring.get('UserGroup.1', [None])[0]
user_id = self.querystring.get('UserId.1', [None])[0]
if (operation_type == 'add'):
ec2_backend.add_launch_permission(ami_id, user_id=user_id, group=group)
elif (operation_type == 'remove'):
ec2_backend.remove_launch_permission(ami_id, user_id=user_id, group=group)
return MODIFY_IMAGE_ATTRIBUTE_RESPONSE
def register_image(self):
raise NotImplementedError('AMIs.register_image is not yet implemented')
@ -57,15 +69,18 @@ DESCRIBE_IMAGES_RESPONSE = """<DescribeImagesResponse xmlns="http://ec2.amazonaw
<item>
<imageId>{{ image.id }}</imageId>
<imageLocation>amazon/getting-started</imageLocation>
<imageState>available</imageState>
<imageState>{{ image.state }}</imageState>
<imageOwnerId>111122223333</imageOwnerId>
<isPublic>true</isPublic>
<architecture>i386</architecture>
<architecture>{{ image.architecture }}</architecture>
<imageType>machine</imageType>
<kernelId>{{ image.kernel_id }}</kernelId>
<ramdiskId>ari-1a2b3c4d</ramdiskId>
<imageOwnerAlias>amazon</imageOwnerAlias>
<name>{{ image.name }}</name>
{% if image.platform %}
<platform>{{ image.platform }}</platform>
{% endif %}
<description>{{ image.description }}</description>
<rootDeviceType>ebs</rootDeviceType>
<rootDeviceName>/dev/sda</rootDeviceName>
@ -73,7 +88,7 @@ DESCRIBE_IMAGES_RESPONSE = """<DescribeImagesResponse xmlns="http://ec2.amazonaw
<item>
<deviceName>/dev/sda1</deviceName>
<ebs>
<snapshotId>snap-1a2b3c4d</snapshotId>
<snapshotId>{{ image.ebs_snapshot.id }}</snapshotId>
<volumeSize>15</volumeSize>
<deleteOnTermination>false</deleteOnTermination>
<volumeType>standard</volumeType>
@ -109,3 +124,27 @@ DEREGISTER_IMAGE_RESPONSE = """<DeregisterImageResponse xmlns="http://ec2.amazon
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<return>{{ success }}</return>
</DeregisterImageResponse>"""
DESCRIBE_IMAGE_ATTRIBUTES_RESPONSE = """
<DescribeImageAttributeResponse xmlns="http://ec2.amazonaws.com/doc/2013-08-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<imageId>{{ ami_id }}</imageId>
{% if not groups %}
<launchPermission/>
{% endif %}
{% if groups %}
<launchPermission>
{% for group in groups %}
<item>
<group>{{ group }}</group>
</item>
{% endfor %}
</launchPermission>
{% endif %}
</DescribeImageAttributeResponse>"""
MODIFY_IMAGE_ATTRIBUTE_RESPONSE = """
<ModifyImageAttributeResponse xmlns="http://ec2.amazonaws.com/doc/2013-08-15/">
<return>true</return>
</ModifyImageAttributeResponse>
"""

View File

@ -199,7 +199,12 @@ EC2_RUN_INSTANCES = """<RunInstancesResponse xmlns="http://ec2.amazonaws.com/doc
</item>
{% endfor %}
</groupSet>
<virtualizationType>paravirtual</virtualizationType>
{% if instance.platform %}
<platform>{{ instance.platform }}</platform>
{% endif %}
<virtualizationType>{{ instance.virtualization_type }}</virtualizationType>
<architecture>{{ instance.architecture }}</architecture>
<kernelId>{{ instance.kernel }}</kernelId>
<clientToken/>
<hypervisor>xen</hypervisor>
<ebsOptimized>false</ebsOptimized>
@ -238,7 +243,9 @@ EC2_DESCRIBE_INSTANCES = """<DescribeInstancesResponse xmlns='http://ec2.amazona
<groupName/>
<tenancy>default</tenancy>
</placement>
<platform>windows</platform>
{% if instance.platform %}
<platform>{{ instance.platform }}</platform>
{% endif %}
<monitoring>
<state>disabled</state>
</monitoring>
@ -255,11 +262,12 @@ EC2_DESCRIBE_INSTANCES = """<DescribeInstancesResponse xmlns='http://ec2.amazona
</item>
{% endfor %}
</groupSet>
<architecture>x86_64</architecture>
<architecture>{{ instance.architecture }}</architecture>
<kernelId>{{ instance.kernel }}</kernelId>
<rootDeviceType>ebs</rootDeviceType>
<rootDeviceName>/dev/sda1</rootDeviceName>
<blockDeviceMapping />
<virtualizationType>hvm</virtualizationType>
<virtualizationType>{{ instance.virtualization_type }}</virtualizationType>
<clientToken>ABCDE1234567890123</clientToken>
<tagSet>
{% for tag in instance.get_tags() %}

View File

@ -16,16 +16,36 @@ def test_ami_create_and_delete():
conn = boto.connect_ec2('the_key', 'the_secret')
reservation = conn.run_instances('ami-1234abcd')
instance = reservation.instances[0]
image = conn.create_image(instance.id, "test-ami", "this is a test ami")
image_id = conn.create_image(instance.id, "test-ami", "this is a test ami")
all_images = conn.get_all_images()
all_images[0].id.should.equal(image)
image = all_images[0]
success = conn.deregister_image(image)
image.id.should.equal(image_id)
image.virtualization_type.should.equal(instance.virtualization_type)
image.architecture.should.equal(instance.architecture)
image.kernel_id.should.equal(instance.kernel)
image.platform.should.equal(instance.platform)
# Validate auto-created volume and snapshot
volumes = conn.get_all_volumes()
volumes.should.have.length_of(1)
volume = volumes[0]
snapshots = conn.get_all_snapshots()
snapshots.should.have.length_of(1)
snapshot = snapshots[0]
image.block_device_mapping.current_value.snapshot_id.should.equal(snapshot.id)
snapshot.description.should.equal("Auto-created snapshot for AMI {0}".format(image.id))
snapshot.volume_id.should.equal(volume.id)
# Deregister
success = conn.deregister_image(image_id)
success.should.be.true
with assert_raises(EC2ResponseError) as cm:
conn.deregister_image(image)
conn.deregister_image(image_id)
cm.exception.code.should.equal('InvalidAMIID.NotFound')
cm.exception.status.should.equal(400)
cm.exception.request_id.should_not.be.none
@ -75,6 +95,44 @@ def test_ami_pulls_attributes_from_instance():
image.kernel_id.should.equal('test-kernel')
@mock_ec2
def test_ami_filters():
conn = boto.connect_ec2('the_key', 'the_secret')
reservationA = conn.run_instances('ami-1234abcd')
instanceA = reservationA.instances[0]
instanceA.modify_attribute("architecture", "i386")
instanceA.modify_attribute("kernel", "k-1234abcd")
instanceA.modify_attribute("platform", "windows")
instanceA.modify_attribute("virtualization_type", "hvm")
imageA_id = conn.create_image(instanceA.id, "test-ami-A", "this is a test ami")
imageA = conn.get_image(imageA_id)
reservationB = conn.run_instances('ami-abcd1234')
instanceB = reservationB.instances[0]
instanceB.modify_attribute("architecture", "x86_64")
instanceB.modify_attribute("kernel", "k-abcd1234")
instanceB.modify_attribute("platform", "linux")
instanceB.modify_attribute("virtualization_type", "paravirtual")
imageB_id = conn.create_image(instanceB.id, "test-ami-B", "this is a test ami")
imageB = conn.get_image(imageB_id)
amis_by_architecture = conn.get_all_images(filters={'architecture': 'x86_64'})
set([ami.id for ami in amis_by_architecture]).should.equal(set([imageB.id]))
amis_by_kernel = conn.get_all_images(filters={'kernel-id': 'k-abcd1234'})
set([ami.id for ami in amis_by_kernel]).should.equal(set([imageB.id]))
amis_by_virtualization = conn.get_all_images(filters={'virtualization-type': 'paravirtual'})
set([ami.id for ami in amis_by_virtualization]).should.equal(set([imageB.id]))
amis_by_platform = conn.get_all_images(filters={'platform': 'windows'})
set([ami.id for ami in amis_by_platform]).should.equal(set([imageA.id]))
amis_by_id = conn.get_all_images(filters={'image-id': imageA.id})
set([ami.id for ami in amis_by_id]).should.equal(set([imageA.id]))
@mock_ec2
def test_getting_missing_ami():
conn = boto.connect_ec2('the_key', 'the_secret')
@ -85,3 +143,97 @@ def test_getting_missing_ami():
cm.exception.status.should.equal(400)
cm.exception.request_id.should_not.be.none
@mock_ec2
def test_getting_malformed_ami():
conn = boto.connect_ec2('the_key', 'the_secret')
with assert_raises(EC2ResponseError) as cm:
conn.get_image('foo-missing')
cm.exception.code.should.equal('InvalidAMIID.Malformed')
cm.exception.status.should.equal(400)
cm.exception.request_id.should_not.be.none
@mock_ec2
def test_ami_attribute():
conn = boto.connect_ec2('the_key', 'the_secret')
reservation = conn.run_instances('ami-1234abcd')
instance = reservation.instances[0]
image_id = conn.create_image(instance.id, "test-ami", "this is a test ami")
image = conn.get_image(image_id)
# Baseline
attributes = conn.get_image_attribute(image.id, attribute='launchPermission')
attributes.name.should.equal('launch_permission')
attributes.attrs.should.have.length_of(0)
ADD_GROUP_ARGS = {'image_id': image.id,
'attribute': 'launchPermission',
'operation': 'add',
'groups': 'all'}
REMOVE_GROUP_ARGS = {'image_id': image.id,
'attribute': 'launchPermission',
'operation': 'remove',
'groups': 'all'}
# Add 'all' group and confirm
conn.modify_image_attribute(**ADD_GROUP_ARGS)
attributes = conn.get_image_attribute(image.id, attribute='launchPermission')
attributes.attrs['groups'].should.have.length_of(1)
attributes.attrs['groups'].should.equal(['all'])
# Add is idempotent
conn.modify_image_attribute.when.called_with(**ADD_GROUP_ARGS).should_not.throw(EC2ResponseError)
# Remove 'all' group and confirm
conn.modify_image_attribute(**REMOVE_GROUP_ARGS)
attributes = conn.get_image_attribute(image.id, attribute='launchPermission')
attributes.attrs.should.have.length_of(0)
# Remove is idempotent
conn.modify_image_attribute.when.called_with(**REMOVE_GROUP_ARGS).should_not.throw(EC2ResponseError)
# Error: Add with group != 'all'
with assert_raises(EC2ResponseError) as cm:
conn.modify_image_attribute(image.id,
attribute='launchPermission',
operation='add',
groups='everyone')
cm.exception.code.should.equal('InvalidAMIAttributeItemValue')
cm.exception.status.should.equal(400)
cm.exception.request_id.should_not.be.none
# Error: Add with invalid image ID
with assert_raises(EC2ResponseError) as cm:
conn.modify_image_attribute("ami-abcd1234",
attribute='launchPermission',
operation='add',
groups='all')
cm.exception.code.should.equal('InvalidAMIID.NotFound')
cm.exception.status.should.equal(400)
cm.exception.request_id.should_not.be.none
# Error: Remove with invalid image ID
with assert_raises(EC2ResponseError) as cm:
conn.modify_image_attribute("ami-abcd1234",
attribute='launchPermission',
operation='remove',
groups='all')
cm.exception.code.should.equal('InvalidAMIID.NotFound')
cm.exception.status.should.equal(400)
cm.exception.request_id.should_not.be.none
# Error: Add or remove with user ID instead of group
conn.modify_image_attribute.when.called_with(image.id,
attribute='launchPermission',
operation='add',
user_ids=['user']).should.throw(NotImplementedError)
conn.modify_image_attribute.when.called_with(image.id,
attribute='launchPermission',
operation='remove',
user_ids=['user']).should.throw(NotImplementedError)