diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 31a2d7305..c62ed115f 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -3,7 +3,14 @@ from collections import defaultdict from boto.ec2.instance import Instance, InstanceState, Reservation from moto.core import BaseBackend -from .utils import random_instance_id, random_reservation_id, random_ami_id, random_security_group_id, random_volume_id +from .utils import ( + random_ami_id, + random_instance_id, + random_reservation_id, + random_security_group_id, + random_snapshot_id, + random_volume_id, +) class InstanceBackend(object): @@ -306,10 +313,18 @@ class Volume(object): return 'available' +class Snapshot(object): + def __init__(self, snapshot_id, volume, description): + self.id = snapshot_id + self.volume = volume + self.description = description + + class EBSBackend(object): def __init__(self): self.volumes = {} self.attachments = {} + self.snapshots = {} super(EBSBackend, self).__init__() def create_volume(self, size, zone_name): @@ -348,6 +363,21 @@ class EBSBackend(object): volume.attachment = None return old_attachment + def create_snapshot(self, volume_id, description): + snapshot_id = random_snapshot_id() + volume = self.volumes.get(volume_id) + snapshot = Snapshot(snapshot_id, volume, description) + self.snapshots[snapshot_id] = snapshot + return snapshot + + def describe_snapshots(self): + return self.snapshots.values() + + def delete_snapshot(self, snapshot_id): + if snapshot_id in self.snapshots: + return self.snapshots.pop(snapshot_id) + return False + class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend, RegionsAndZonesBackend, SecurityGroupBackend, EBSBackend): pass diff --git a/moto/ec2/responses/elastic_block_store.py b/moto/ec2/responses/elastic_block_store.py index d28d0c476..31975073b 100644 --- a/moto/ec2/responses/elastic_block_store.py +++ b/moto/ec2/responses/elastic_block_store.py @@ -21,7 +21,13 @@ class ElasticBlockStore(object): raise NotImplementedError('ElasticBlockStore.copy_snapshot is not yet implemented') def create_snapshot(self): - raise NotImplementedError('ElasticBlockStore.create_snapshot is not yet implemented') + description = None + if 'Description' in self.querystring: + description = self.querystring.get('Description')[0] + volume_id = self.querystring.get('VolumeId')[0] + snapshot = ec2_backend.create_snapshot(volume_id, description) + template = Template(CREATE_SNAPSHOT_RESPONSE) + return template.render(snapshot=snapshot) def create_volume(self): size = self.querystring.get('Size')[0] @@ -31,7 +37,12 @@ class ElasticBlockStore(object): return template.render(volume=volume) def delete_snapshot(self): - raise NotImplementedError('ElasticBlockStore.delete_snapshot is not yet implemented') + snapshot_id = self.querystring.get('SnapshotId')[0] + success = ec2_backend.delete_snapshot(snapshot_id) + if not success: + # Snapshot doesn't exist + return "Snapshot with id {} does not exist".format(snapshot_id), dict(status=404) + return DELETE_SNAPSHOT_RESPONSE def delete_volume(self): volume_id = self.querystring.get('VolumeId')[0] @@ -45,7 +56,9 @@ class ElasticBlockStore(object): raise NotImplementedError('ElasticBlockStore.describe_snapshot_attribute is not yet implemented') def describe_snapshots(self): - raise NotImplementedError('ElasticBlockStore.describe_snapshots is not yet implemented') + snapshots = ec2_backend.describe_snapshots() + template = Template(DESCRIBE_SNAPSHOTS_RESPONSE) + return template.render(snapshots=snapshots) def describe_volumes(self): volumes = ec2_backend.describe_volumes() @@ -150,3 +163,39 @@ DETATCH_VOLUME_RESPONSE = """ + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + {{ snapshot.id }} + {{ snapshot.volume.id }} + pending + YYYY-MM-DDTHH:MM:SS.000Z + 60% + 111122223333 + {{ snapshot.volume.size }} + {{ snapshot.description }} +""" + +DESCRIBE_SNAPSHOTS_RESPONSE = """ + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + {% for snapshot in snapshots %} + + {{ snapshot.id }} + {{ snapshot.volume.id }} + pending + YYYY-MM-DDTHH:MM:SS.SSSZ + 30% + 111122223333 + {{ snapshot.volume.size }} + {{ snapshot.description }} + + + + {% endfor %} + +""" + +DELETE_SNAPSHOT_RESPONSE = """ + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true +""" diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py index b074136f3..08b07b58c 100644 --- a/moto/ec2/utils.py +++ b/moto/ec2/utils.py @@ -30,6 +30,10 @@ def random_volume_id(): return random_id(prefix='vol') +def random_snapshot_id(): + return random_id(prefix='snap') + + def instance_ids_from_querystring(querystring_dict): instance_ids = [] for key, value in querystring_dict.iteritems(): diff --git a/tests/test_ec2/test_elastic_block_store.py b/tests/test_ec2/test_elastic_block_store.py index e9f0b6471..1c156cc45 100644 --- a/tests/test_ec2/test_elastic_block_store.py +++ b/tests/test_ec2/test_elastic_block_store.py @@ -48,3 +48,25 @@ def test_volume_attach_and_detach(): volume.volume_state().should.equal('available') conn.detach_volume.when.called_with(volume.id, instance.id, "/dev/sdh").should.throw(EC2ResponseError) + + +@mock_ec2 +def test_create_snapshot(): + conn = boto.connect_ec2('the_key', 'the_secret') + volume = conn.create_volume(80, "us-east-1a") + + volume.create_snapshot('a test snapshot') + + snapshots = conn.get_all_snapshots() + snapshots.should.have.length_of(1) + snapshots[0].description.should.equal('a test snapshot') + + # Create snapshot without description + snapshot = volume.create_snapshot() + conn.get_all_snapshots().should.have.length_of(2) + + snapshot.delete() + conn.get_all_snapshots().should.have.length_of(1) + + # Deleting something that was already deleted should throw an error + snapshot.delete.when.called_with().should.throw(EC2ResponseError)