volume attaching and detaching working
This commit is contained in:
parent
7464fcbbe9
commit
f92a3ec00f
@ -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
|
||||
|
||||
|
||||
|
@ -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 = """<CreateVolumeResponse xmlns="http://ec2.amazonaws.com/doc/2012-12-01/">
|
||||
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
|
||||
<volumeId>{{ volume.id }}</volumeId>
|
||||
<size>{{ volume.size }}</size>
|
||||
<snapshotId/>
|
||||
<availabilityZone>{{ volume.zone.name }}</availabilityZone>
|
||||
<status>creating</status>
|
||||
<createTime>YYYY-MM-DDTHH:MM:SS.000Z</createTime>
|
||||
<volumeType>standard</volumeType>
|
||||
</CreateVolumeResponse>"""
|
||||
|
||||
DESCRIBE_VOLUMES_RESPONSE = """<DescribeVolumesResponse xmlns="http://ec2.amazonaws.com/doc/2012-12-01/">
|
||||
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
|
||||
<volumeSet>
|
||||
{% for volume in volumes %}
|
||||
<item>
|
||||
<volumeId>{{ volume.id }}</volumeId>
|
||||
<size>{{ volume.size }}</size>
|
||||
<snapshotId/>
|
||||
<availabilityZone>{{ volume.zone.name }}</availabilityZone>
|
||||
<status>{{ volume.status }}</status>
|
||||
<createTime>YYYY-MM-DDTHH:MM:SS.SSSZ</createTime>
|
||||
<attachmentSet>
|
||||
{% if volume.attachment %}
|
||||
<item>
|
||||
<volumeId>{{ volume.id }}</volumeId>
|
||||
<instanceId>{{ volume.attachment.instance.id }}</instanceId>
|
||||
<device>{{ volume.attachment.device }}</device>
|
||||
<status>attached</status>
|
||||
<attachTime>YYYY-MM-DDTHH:MM:SS.SSSZ</attachTime>
|
||||
<deleteOnTermination>false</deleteOnTermination>
|
||||
</item>
|
||||
{% endif %}
|
||||
</attachmentSet>
|
||||
<volumeType>standard</volumeType>
|
||||
</item>
|
||||
{% endfor %}
|
||||
</volumeSet>
|
||||
</DescribeVolumesResponse>"""
|
||||
|
||||
DELETE_VOLUME_RESPONSE = """<DeleteVolumeResponse xmlns="http://ec2.amazonaws.com/doc/2012-12-01/">
|
||||
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
|
||||
<return>true</return>
|
||||
</DeleteVolumeResponse>"""
|
||||
|
||||
ATTACHED_VOLUME_RESPONSE = """<AttachVolumeResponse xmlns="http://ec2.amazonaws.com/doc/2012-12-01/">
|
||||
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
|
||||
<volumeId>{{ attachment.volume.id }}</volumeId>
|
||||
<instanceId>{{ attachment.instance.id }}</instanceId>
|
||||
<device>{{ attachment.device }}</device>
|
||||
<status>attaching</status>
|
||||
<attachTime>YYYY-MM-DDTHH:MM:SS.000Z</attachTime>
|
||||
</AttachVolumeResponse>"""
|
||||
|
||||
DETATCH_VOLUME_RESPONSE = """<DetachVolumeResponse xmlns="http://ec2.amazonaws.com/doc/2012-12-01/">
|
||||
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
|
||||
<volumeId>{{ attachment.volume.id }}</volumeId>
|
||||
<instanceId>{{ attachment.instance.id }}</instanceId>
|
||||
<device>{{ attachment.device }}</device>
|
||||
<status>detaching</status>
|
||||
<attachTime>YYYY-MM-DDTHH:MM:SS.000Z</attachTime>
|
||||
</DetachVolumeResponse>"""
|
||||
|
||||
|
@ -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():
|
||||
|
@ -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('<ami-image-id>')
|
||||
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)
|
||||
|
Loading…
Reference in New Issue
Block a user