AMIs: Added ModifyImageAttribute and DescribeImages filtering.
This commit is contained in:
parent
b69179818c
commit
f9246def75
@ -118,6 +118,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__(
|
||||
|
@ -28,6 +28,7 @@ from .exceptions import (
|
||||
InvalidSecurityGroupNotFoundError,
|
||||
InvalidPermissionNotFoundError,
|
||||
InvalidInstanceIdError,
|
||||
MalformedAMIIdError,
|
||||
InvalidAMIIdError,
|
||||
InvalidAMIAttributeItemValueError,
|
||||
InvalidSnapshotIdError,
|
||||
@ -89,6 +90,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']
|
||||
@ -327,13 +336,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):
|
||||
@ -349,14 +379,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:
|
||||
@ -364,6 +402,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):
|
||||
|
@ -2,7 +2,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):
|
||||
@ -24,17 +24,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')
|
||||
@ -56,15 +68,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>
|
||||
@ -72,7 +87,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>
|
||||
@ -108,3 +123,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>
|
||||
"""
|
||||
|
@ -198,7 +198,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>
|
||||
@ -237,7 +242,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>
|
||||
@ -254,11 +261,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() %}
|
||||
|
@ -15,16 +15,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
|
||||
@ -74,6 +94,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')
|
||||
@ -84,3 +142,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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user