diff --git a/moto/ec2/models.py b/moto/ec2/models.py index de6899a96..31a2d7305 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -3,7 +3,7 @@ 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 +from .utils import random_instance_id, random_reservation_id, random_ami_id, random_security_group_id, random_volume_id class InstanceBackend(object): @@ -192,6 +192,11 @@ class RegionsAndZonesBackend(object): def describe_availability_zones(self): return self.zones + def get_zone_by_name(self, name): + for zone in self.zones: + if zone.name == name: + return zone + class SecurityRule(object): def __init__(self, ip_protocol, from_port, to_port, ip_ranges, source_groups): @@ -228,6 +233,7 @@ class SecurityGroupBackend(object): def __init__(self): self.groups = {} + super(SecurityGroupBackend, self).__init__() def create_security_group(self, name, description): group_id = random_security_group_id() @@ -278,7 +284,72 @@ class SecurityGroupBackend(object): return False -class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend, RegionsAndZonesBackend, SecurityGroupBackend): +class VolumeAttachment(object): + def __init__(self, volume, instance, device): + self.volume = volume + self.instance = instance + self.device = device + + +class Volume(object): + def __init__(self, volume_id, size, zone): + self.id = volume_id + self.size = size + self.zone = zone + self.attachment = None + + @property + def status(self): + if self.attachment: + return 'in-use' + else: + return 'available' + + +class EBSBackend(object): + def __init__(self): + self.volumes = {} + self.attachments = {} + super(EBSBackend, self).__init__() + + def create_volume(self, size, zone_name): + volume_id = random_volume_id() + zone = self.get_zone_by_name(zone_name) + volume = Volume(volume_id, size, zone) + self.volumes[volume_id] = volume + return volume + + def describe_volumes(self): + return self.volumes.values() + + def delete_volume(self, volume_id): + if volume_id in self.volumes: + return self.volumes.pop(volume_id) + return False + + def attach_volume(self, volume_id, instance_id, device_path): + volume = self.volumes.get(volume_id) + instance = self.get_instance(instance_id) + + if not volume or not instance: + return False + + volume.attachment = VolumeAttachment(volume, instance, device_path) + return volume.attachment + + def detach_volume(self, volume_id, instance_id, device_path): + volume = self.volumes.get(volume_id) + instance = self.get_instance(instance_id) + + if not volume or not instance: + return False + + old_attachment = volume.attachment + volume.attachment = None + return old_attachment + + +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 1c9b8b2ef..d28d0c476 100644 --- a/moto/ec2/responses/elastic_block_store.py +++ b/moto/ec2/responses/elastic_block_store.py @@ -5,8 +5,17 @@ from moto.ec2.utils import resource_ids_from_querystring class ElasticBlockStore(object): + def __init__(self, querystring): + self.querystring = querystring + def attach_volume(self): - raise NotImplementedError('ElasticBlockStore.attach_volume is not yet implemented') + volume_id = self.querystring.get('VolumeId')[0] + instance_id = self.querystring.get('InstanceId')[0] + device_path = self.querystring.get('Device')[0] + + attachment = ec2_backend.attach_volume(volume_id, instance_id, device_path) + template = Template(ATTACHED_VOLUME_RESPONSE) + return template.render(attachment=attachment) def copy_snapshot(self): raise NotImplementedError('ElasticBlockStore.copy_snapshot is not yet implemented') @@ -15,13 +24,22 @@ class ElasticBlockStore(object): raise NotImplementedError('ElasticBlockStore.create_snapshot is not yet implemented') def create_volume(self): - raise NotImplementedError('ElasticBlockStore.create_volume is not yet implemented') + size = self.querystring.get('Size')[0] + zone = self.querystring.get('AvailabilityZone')[0] + volume = ec2_backend.create_volume(size, zone) + template = Template(CREATE_VOLUME_RESPONSE) + return template.render(volume=volume) def delete_snapshot(self): raise NotImplementedError('ElasticBlockStore.delete_snapshot is not yet implemented') def delete_volume(self): - raise NotImplementedError('ElasticBlockStore.delete_volume is not yet implemented') + volume_id = self.querystring.get('VolumeId')[0] + success = ec2_backend.delete_volume(volume_id) + if not success: + # Volume doesn't exist + return "Volume with id {} does not exist".format(volume_id), dict(status=404) + return DELETE_VOLUME_RESPONSE def describe_snapshot_attribute(self): raise NotImplementedError('ElasticBlockStore.describe_snapshot_attribute is not yet implemented') @@ -30,7 +48,9 @@ class ElasticBlockStore(object): raise NotImplementedError('ElasticBlockStore.describe_snapshots is not yet implemented') def describe_volumes(self): - raise NotImplementedError('ElasticBlockStore.describe_volumes is not yet implemented') + volumes = ec2_backend.describe_volumes() + template = Template(DESCRIBE_VOLUMES_RESPONSE) + return template.render(volumes=volumes) def describe_volume_attribute(self): raise NotImplementedError('ElasticBlockStore.describe_volume_attribute is not yet implemented') @@ -39,7 +59,17 @@ class ElasticBlockStore(object): raise NotImplementedError('ElasticBlockStore.describe_volume_status is not yet implemented') def detach_volume(self): - raise NotImplementedError('ElasticBlockStore.detach_volume is not yet implemented') + volume_id = self.querystring.get('VolumeId')[0] + instance_id = self.querystring.get('InstanceId')[0] + device_path = self.querystring.get('Device')[0] + + attachment = ec2_backend.detach_volume(volume_id, instance_id, device_path) + if not attachment: + # Volume wasn't attached + return "Volume {} can not be detached from {} because it is not attached".format(volume_id, instance_id), dict(status=404) + template = Template(DETATCH_VOLUME_RESPONSE) + return template.render(attachment=attachment) + def enable_volume_io(self): raise NotImplementedError('ElasticBlockStore.enable_volume_io is not yet implemented') @@ -56,3 +86,67 @@ class ElasticBlockStore(object): def reset_snapshot_attribute(self): raise NotImplementedError('ElasticBlockStore.reset_snapshot_attribute is not yet implemented') + +CREATE_VOLUME_RESPONSE = """ + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + {{ volume.id }} + {{ volume.size }} + + {{ volume.zone.name }} + creating + YYYY-MM-DDTHH:MM:SS.000Z + standard +""" + +DESCRIBE_VOLUMES_RESPONSE = """ + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + {% for volume in volumes %} + + {{ volume.id }} + {{ volume.size }} + + {{ volume.zone.name }} + {{ volume.status }} + YYYY-MM-DDTHH:MM:SS.SSSZ + + {% if volume.attachment %} + + {{ volume.id }} + {{ volume.attachment.instance.id }} + {{ volume.attachment.device }} + attached + YYYY-MM-DDTHH:MM:SS.SSSZ + false + + {% endif %} + + standard + + {% endfor %} + +""" + +DELETE_VOLUME_RESPONSE = """ + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true +""" + +ATTACHED_VOLUME_RESPONSE = """ + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + {{ attachment.volume.id }} + {{ attachment.instance.id }} + {{ attachment.device }} + attaching + YYYY-MM-DDTHH:MM:SS.000Z +""" + +DETATCH_VOLUME_RESPONSE = """ + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + {{ attachment.volume.id }} + {{ attachment.instance.id }} + {{ attachment.device }} + detaching + YYYY-MM-DDTHH:MM:SS.000Z +""" + diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py index bbe808d4f..b074136f3 100644 --- a/moto/ec2/utils.py +++ b/moto/ec2/utils.py @@ -26,6 +26,10 @@ def random_security_group_id(): return random_id(prefix='sg') +def random_volume_id(): + return random_id(prefix='vol') + + 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 f63bd503c..e9f0b6471 100644 --- a/tests/test_ec2/test_elastic_block_store.py +++ b/tests/test_ec2/test_elastic_block_store.py @@ -1,9 +1,50 @@ import boto +from boto.exception import EC2ResponseError + from sure import expect from moto import mock_ec2 @mock_ec2 -def test_elastic_block_store(): - pass +def test_create_and_delete_volume(): + conn = boto.connect_ec2('the_key', 'the_secret') + volume = conn.create_volume(80, "us-east-1a") + + all_volumes = conn.get_all_volumes() + all_volumes.should.have.length_of(1) + all_volumes[0].size.should.equal(80) + all_volumes[0].zone.should.equal("us-east-1a") + + volume = all_volumes[0] + volume.delete() + + conn.get_all_volumes().should.have.length_of(0) + + # Deleting something that was already deleted should throw an error + volume.delete.when.called_with().should.throw(EC2ResponseError) + + +@mock_ec2 +def test_volume_attach_and_detach(): + conn = boto.connect_ec2('the_key', 'the_secret') + reservation = conn.run_instances('') + instance = reservation.instances[0] + volume = conn.create_volume(80, "us-east-1a") + + volume.update() + volume.volume_state().should.equal('available') + + volume.attach(instance.id, "/dev/sdh") + + volume.update() + volume.volume_state().should.equal('in-use') + + volume.attach_data.instance_id.should.equal(instance.id) + + volume.detach() + + volume.update() + volume.volume_state().should.equal('available') + + conn.detach_volume.when.called_with(volume.id, instance.id, "/dev/sdh").should.throw(EC2ResponseError)