EC2 - Error when deleting snapshots in use (#4721)

This commit is contained in:
Bert Blommers 2021-12-25 20:37:39 -01:00 committed by GitHub
parent 71daf79ffd
commit 973c55a36c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 62 additions and 6 deletions

View File

@ -275,6 +275,14 @@ class InvalidSnapshotIdError(EC2ClientError):
) # Note: AWS returns empty message for this, as of 2014.08.22. ) # Note: AWS returns empty message for this, as of 2014.08.22.
class InvalidSnapshotInUse(EC2ClientError):
def __init__(self, snapshot_id, ami_id):
super().__init__(
"InvalidSnapshot.InUse",
f"The snapshot {snapshot_id} is currently in use by {ami_id}",
)
class InvalidVolumeIdError(EC2ClientError): class InvalidVolumeIdError(EC2ClientError):
def __init__(self, volume_id): def __init__(self, volume_id):
super().__init__( super().__init__(

View File

@ -84,6 +84,7 @@ from .exceptions import (
InvalidSecurityGroupDuplicateError, InvalidSecurityGroupDuplicateError,
InvalidSecurityGroupNotFoundError, InvalidSecurityGroupNotFoundError,
InvalidSnapshotIdError, InvalidSnapshotIdError,
InvalidSnapshotInUse,
InvalidSubnetConflictError, InvalidSubnetConflictError,
InvalidSubnetIdError, InvalidSubnetIdError,
InvalidSubnetRangeError, InvalidSubnetRangeError,
@ -1668,6 +1669,7 @@ class Ami(TaggedEC2Resource):
root_device_name="/dev/sda1", root_device_name="/dev/sda1",
sriov="simple", sriov="simple",
region_name="us-east-1a", region_name="us-east-1a",
snapshot_description=None,
): ):
self.ec2_backend = ec2_backend self.ec2_backend = ec2_backend
self.id = ami_id self.id = ami_id
@ -1721,8 +1723,11 @@ class Ami(TaggedEC2Resource):
# AWS auto-creates these, we should reflect the same. # AWS auto-creates these, we should reflect the same.
volume = self.ec2_backend.create_volume(size=15, zone_name=region_name) volume = self.ec2_backend.create_volume(size=15, zone_name=region_name)
snapshot_description = (
snapshot_description or "Auto-created snapshot for AMI %s" % self.id
)
self.ebs_snapshot = self.ec2_backend.create_snapshot( self.ebs_snapshot = self.ec2_backend.create_snapshot(
volume.id, "Auto-created snapshot for AMI %s" % self.id, owner_id volume.id, snapshot_description, owner_id, from_ami=ami_id
) )
self.ec2_backend.delete_volume(volume.id) self.ec2_backend.delete_volume(volume.id)
@ -1802,6 +1807,7 @@ class AmiBackend(object):
name=name, name=name,
description=description, description=description,
owner_id=OWNER_ID, owner_id=OWNER_ID,
snapshot_description=f"Created by CreateImage({instance_id}) for {ami_id}",
) )
for tag in tags: for tag in tags:
ami.add_tag(tag["Key"], tag["Value"]) ami.add_tag(tag["Key"], tag["Value"])
@ -3470,6 +3476,7 @@ class Snapshot(TaggedEC2Resource):
description, description,
encrypted=False, encrypted=False,
owner_id=OWNER_ID, owner_id=OWNER_ID,
from_ami=None,
): ):
self.id = snapshot_id self.id = snapshot_id
self.volume = volume self.volume = volume
@ -3481,6 +3488,7 @@ class Snapshot(TaggedEC2Resource):
self.status = "completed" self.status = "completed"
self.encrypted = encrypted self.encrypted = encrypted
self.owner_id = owner_id self.owner_id = owner_id
self.from_ami = from_ami
def get_filter_value(self, filter_name): def get_filter_value(self, filter_name):
if filter_name == "description": if filter_name == "description":
@ -3609,12 +3617,14 @@ class EBSBackend(object):
volume.attachment = None volume.attachment = None
return old_attachment return old_attachment
def create_snapshot(self, volume_id, description, owner_id=None): def create_snapshot(self, volume_id, description, owner_id=None, from_ami=None):
snapshot_id = random_snapshot_id() snapshot_id = random_snapshot_id()
volume = self.get_volume(volume_id) volume = self.get_volume(volume_id)
params = [self, snapshot_id, volume, description, volume.encrypted] params = [self, snapshot_id, volume, description, volume.encrypted]
if owner_id: if owner_id:
params.append(owner_id) params.append(owner_id)
if from_ami:
params.append(from_ami)
snapshot = Snapshot(*params) snapshot = Snapshot(*params)
self.snapshots[snapshot_id] = snapshot self.snapshots[snapshot_id] = snapshot
return snapshot return snapshot
@ -3653,6 +3663,9 @@ class EBSBackend(object):
def delete_snapshot(self, snapshot_id): def delete_snapshot(self, snapshot_id):
if snapshot_id in self.snapshots: if snapshot_id in self.snapshots:
snapshot = self.snapshots[snapshot_id]
if snapshot.from_ami and snapshot.from_ami in self.amis:
raise InvalidSnapshotInUse(snapshot_id, snapshot.from_ami)
return self.snapshots.pop(snapshot_id) return self.snapshots.pop(snapshot_id)
raise InvalidSnapshotIdError(snapshot_id) raise InvalidSnapshotIdError(snapshot_id)

View File

@ -64,9 +64,7 @@ def test_ami_create_and_delete():
) )
[s.id for s in snapshots].should.contain(retrieved_image_snapshot_id) [s.id for s in snapshots].should.contain(retrieved_image_snapshot_id)
snapshot = [s for s in snapshots if s.id == retrieved_image_snapshot_id][0] snapshot = [s for s in snapshots if s.id == retrieved_image_snapshot_id][0]
snapshot.description.should.equal( snapshot.description.should.match("Created by CreateImage")
"Auto-created snapshot for AMI {0}".format(retrieved_image.id)
)
# root device should be in AMI's block device mappings # root device should be in AMI's block device mappings
root_mapping = retrieved_image.block_device_mapping.get( root_mapping = retrieved_image.block_device_mapping.get(
@ -167,8 +165,9 @@ def test_ami_create_and_delete_boto3():
snapshot = [s for s in snapshots if s["SnapshotId"] == retrieved_image_snapshot_id][ snapshot = [s for s in snapshots if s["SnapshotId"] == retrieved_image_snapshot_id][
0 0
] ]
image_id = retrieved_image["ImageId"]
snapshot["Description"].should.equal( snapshot["Description"].should.equal(
"Auto-created snapshot for AMI {0}".format(retrieved_image["ImageId"]) f"Created by CreateImage({instance_id}) for {image_id}"
) )
# root device should be in AMI's block device mappings # root device should be in AMI's block device mappings
@ -1794,3 +1793,39 @@ def test_describe_images_dryrun():
ex.value.response["Error"]["Message"].should.equal( ex.value.response["Error"]["Message"].should.equal(
"An error occurred (DryRunOperation) when calling the DescribeImages operation: Request would have succeeded, but DryRun flag is set" "An error occurred (DryRunOperation) when calling the DescribeImages operation: Request would have succeeded, but DryRun flag is set"
) )
@mock_ec2
def test_delete_snapshot_from_create_image():
ec2_client = boto3.client("ec2", region_name="us-east-1")
resp = ec2_client.run_instances(ImageId=EXAMPLE_AMI_ID, MinCount=1, MaxCount=1)
instance_id = resp["Instances"][0]["InstanceId"]
ami = ec2_client.create_image(InstanceId=instance_id, Name="test")
ami_id = ami["ImageId"]
snapshots = ec2_client.describe_snapshots(
Filters=[
{
"Name": "description",
"Values": ["Created by CreateImage(" + instance_id + "*"],
}
]
)["Snapshots"]
snapshot_id = snapshots[0]["SnapshotId"]
with pytest.raises(ClientError) as exc:
ec2_client.delete_snapshot(SnapshotId=snapshot_id)
err = exc.value.response["Error"]
err["Code"].should.equal("InvalidSnapshot.InUse")
err["Message"].should.equal(
f"The snapshot {snapshot_id} is currently in use by {ami_id}"
)
# Deregister the Ami first
ec2_client.deregister_image(ImageId=ami_id)
# Now we can delete the snapshot without problems
ec2_client.delete_snapshot(SnapshotId=snapshot_id)
with pytest.raises(ClientError) as exc:
ec2_client.describe_snapshots(SnapshotIds=[snapshot_id])
exc.value.response["Error"]["Code"].should.equal("InvalidSnapshot.NotFound")