diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 03dc29c5b..3bf695216 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -593,19 +593,33 @@ class TagBackend(object): class Ami(TaggedEC2Instance): - def __init__(self, ami_id, instance, name, description): + def __init__(self, ami_id, instance=None, source_ami=None, name=None, description=None): 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 + if instance: + 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 + + elif source_ami: + """ + http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/CopyingAMIs.html + "We don't copy launch permissions, user-defined tags, or Amazon S3 bucket permissions from the source AMI to the new AMI." + ~ 2014.09.29 + """ + self.virtualization_type = source_ami.virtualization_type + self.architecture = source_ami.architecture + self.kernel_id = source_ami.kernel_id + self.platform = source_ami.platform + self.name = name if name else source_ami.name + self.description = description if description else source_ami.description - self.name = name - self.description = description self.launch_permission_groups = set() # AWS auto-creates these, we should reflect the same. @@ -637,11 +651,18 @@ class AmiBackend(object): self.amis = {} super(AmiBackend, self).__init__() - def create_image(self, instance_id, name, description): + def create_image(self, instance_id, name=None, description=None): # TODO: check that instance exists and pull info from it. ami_id = random_ami_id() instance = self.get_instance(instance_id) - ami = Ami(ami_id, instance, name, description) + ami = Ami(ami_id, instance=instance, source_ami=None, name=name, description=description) + self.amis[ami_id] = ami + return ami + + def copy_image(self, source_image_id, source_region, name=None, description=None): + source_ami = ec2_backends[source_region].describe_images(ami_ids=[source_image_id])[0] + ami_id = random_ami_id() + ami = Ami(ami_id, instance=None, source_ami=source_ami, name=name, description=description) self.amis[ami_id] = ami return ami diff --git a/moto/ec2/responses/amis.py b/moto/ec2/responses/amis.py index 7ea0eaf5a..8fc9a499e 100644 --- a/moto/ec2/responses/amis.py +++ b/moto/ec2/responses/amis.py @@ -19,6 +19,15 @@ class AmisResponse(BaseResponse): template = Template(CREATE_IMAGE_RESPONSE) return template.render(image=image) + def copy_image(self): + source_image_id = self.querystring.get('SourceImageId')[0] + source_region = self.querystring.get('SourceRegion')[0] + name = self.querystring.get('Name')[0] if self.querystring.get('Name') else None + description = self.querystring.get('Description')[0] if self.querystring.get('Description') else None + image = ec2_backend.copy_image(source_image_id, source_region, name, description) + template = Template(COPY_IMAGE_RESPONSE) + return template.render(image=image) + def deregister_image(self): ami_id = self.querystring.get('ImageId')[0] success = ec2_backend.deregister_image(ami_id) @@ -61,6 +70,11 @@ CREATE_IMAGE_RESPONSE = """ + 60bc441d-fa2c-494d-b155-5d6a3EXAMPLE + {{ image.id }} +""" + # TODO almost all of these params should actually be templated based on the ec2 image DESCRIBE_IMAGES_RESPONSE = """ 59dbff89-35bd-4eac-99ed-be587EXAMPLE diff --git a/tests/test_ec2/test_amis.py b/tests/test_ec2/test_amis.py index 49221cc96..18bc9629b 100644 --- a/tests/test_ec2/test_amis.py +++ b/tests/test_ec2/test_amis.py @@ -9,6 +9,7 @@ from boto.exception import EC2ResponseError import sure # noqa from moto import mock_ec2 +from tests.helpers import requires_boto_gte @mock_ec2 @@ -51,6 +52,50 @@ def test_ami_create_and_delete(): cm.exception.request_id.should_not.be.none +@requires_boto_gte("2.14.0") +@mock_ec2 +def test_ami_copy(): + conn = boto.connect_ec2('the_key', 'the_secret') + reservation = conn.run_instances('ami-1234abcd') + instance = reservation.instances[0] + + source_image_id = conn.create_image(instance.id, "test-ami", "this is a test ami") + source_image = conn.get_all_images(image_ids=[source_image_id])[0] + + # Boto returns a 'CopyImage' object with an image_id attribute here. Use the image_id to fetch the full info. + copy_image_ref = conn.copy_image(source_image.region.name, source_image.id, "test-copy-ami", "this is a test copy ami") + copy_image_id = copy_image_ref.image_id + copy_image = conn.get_all_images(image_ids=[copy_image_id])[0] + + copy_image.id.should.equal(copy_image_id) + copy_image.virtualization_type.should.equal(source_image.virtualization_type) + copy_image.architecture.should.equal(source_image.architecture) + copy_image.kernel_id.should.equal(source_image.kernel_id) + copy_image.platform.should.equal(source_image.platform) + + # Validate auto-created volume and snapshot + conn.get_all_volumes().should.have.length_of(2) + conn.get_all_snapshots().should.have.length_of(2) + + copy_image.block_device_mapping.current_value.snapshot_id.should_not.equal( + source_image.block_device_mapping.current_value.snapshot_id) + + # Copy from non-existent source ID. + with assert_raises(EC2ResponseError) as cm: + conn.copy_image(source_image.region.name, 'ami-abcd1234', "test-copy-ami", "this is a test copy ami") + cm.exception.code.should.equal('InvalidAMIID.NotFound') + cm.exception.status.should.equal(400) + cm.exception.request_id.should_not.be.none + + # Copy from non-existent source region. + with assert_raises(EC2ResponseError) as cm: + invalid_region = 'us-east-1' if (source_image.region.name != 'us-east-1') else 'us-west-1' + conn.copy_image(invalid_region, source_image.id, "test-copy-ami", "this is a test copy ami") + cm.exception.code.should.equal('InvalidAMIID.NotFound') + cm.exception.status.should.equal(400) + cm.exception.request_id.should_not.be.none + + @mock_ec2 def test_ami_tagging(): conn = boto.connect_vpc('the_key', 'the_secret')