diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py index 133a11adb..0e5e55bd4 100644 --- a/moto/ec2/exceptions.py +++ b/moto/ec2/exceptions.py @@ -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__( diff --git a/moto/ec2/models.py b/moto/ec2/models.py index df15b7f10..0987cbf60 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -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): diff --git a/moto/ec2/responses/amis.py b/moto/ec2/responses/amis.py index 4dcb0a246..3b69abd79 100644 --- a/moto/ec2/responses/amis.py +++ b/moto/ec2/responses/amis.py @@ -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 = """ + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + {{ ami_id }} + {% if not groups %} + + {% endif %} + {% if groups %} + + {% for group in groups %} + + {{ group }} + + {% endfor %} + + {% endif %} +""" + +MODIFY_IMAGE_ATTRIBUTE_RESPONSE = """ + + true + +""" diff --git a/moto/ec2/responses/instances.py b/moto/ec2/responses/instances.py index b4b3bedb7..af2191d3e 100644 --- a/moto/ec2/responses/instances.py +++ b/moto/ec2/responses/instances.py @@ -198,7 +198,12 @@ EC2_RUN_INSTANCES = """ {% endfor %} - x86_64 + {{ instance.architecture }} + {{ instance.kernel }} ebs /dev/sda1 - hvm + {{ instance.virtualization_type }} ABCDE1234567890123 {% for tag in instance.get_tags() %} diff --git a/tests/test_ec2/test_amis.py b/tests/test_ec2/test_amis.py index 27201a2d1..0e5a17cbc 100644 --- a/tests/test_ec2/test_amis.py +++ b/tests/test_ec2/test_amis.py @@ -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) +