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)