From a4fdce2e551941806e8a50f9ec0a6fe8536d4c87 Mon Sep 17 00:00:00 2001 From: dreadpirateshawn Date: Mon, 29 Sep 2014 12:06:36 -0700 Subject: [PATCH 1/4] AMI: Implement copy_image. --- moto/ec2/models.py | 47 +++++++++++++++++++++++++++---------- moto/ec2/responses/amis.py | 14 +++++++++++ tests/test_ec2/test_amis.py | 43 +++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 12 deletions(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 20fe46b25..61ad7ddfe 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -576,21 +576,37 @@ 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.launch_permission_groups = set() + self.name = name + self.description = description - self.name = name - self.description = description - self.launch_permission_groups = set() + 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.autocreate_volume_and_snapshot() + + def autocreate_volume_and_snapshot(self): # 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) @@ -613,11 +629,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 95fe9f920..8235ae1d6 100644 --- a/tests/test_ec2/test_amis.py +++ b/tests/test_ec2/test_amis.py @@ -51,6 +51,49 @@ def test_ami_create_and_delete(): cm.exception.request_id.should_not.be.none +@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') From 1940f7c17a95b11dc832d40de37e9639cec1635a Mon Sep 17 00:00:00 2001 From: dreadpirateshawn Date: Mon, 29 Sep 2014 12:18:39 -0700 Subject: [PATCH 2/4] AMI: Implement copy_image. (part 2, tweaked launch_permission_groups init) --- moto/ec2/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 61ad7ddfe..cf945f790 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -587,7 +587,6 @@ class Ami(TaggedEC2Instance): self.architecture = instance.architecture self.kernel_id = instance.kernel self.platform = instance.platform - self.launch_permission_groups = set() self.name = name self.description = description @@ -604,6 +603,7 @@ class Ami(TaggedEC2Instance): self.name = name if name else source_ami.name self.description = description if description else source_ami.description + self.launch_permission_groups = set() self.autocreate_volume_and_snapshot() def autocreate_volume_and_snapshot(self): From 635a0e0f64b0f2ad8762b89758b2abbb437f5170 Mon Sep 17 00:00:00 2001 From: dreadpirateshawn Date: Mon, 29 Sep 2014 12:28:53 -0700 Subject: [PATCH 3/4] AMI: Implement copy_image. (part 3, added boto version threshold) --- tests/test_ec2/test_amis.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_ec2/test_amis.py b/tests/test_ec2/test_amis.py index 8235ae1d6..7c698fdd3 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,7 @@ 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') From 2dfd1799aecb99f9310507e1ba20e075f6b0c67d Mon Sep 17 00:00:00 2001 From: dreadpirateshawn Date: Wed, 1 Oct 2014 07:59:02 -0700 Subject: [PATCH 4/4] AMI: Implement copy_image. (part 4, minor refactor for clarity) --- moto/ec2/models.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index cf945f790..1cb23afbf 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -604,9 +604,7 @@ class Ami(TaggedEC2Instance): self.description = description if description else source_ami.description self.launch_permission_groups = set() - self.autocreate_volume_and_snapshot() - def autocreate_volume_and_snapshot(self): # 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)