From 2bb79824ce2a687bd602e077b371738cc6453e16 Mon Sep 17 00:00:00 2001 From: Rob Walker Date: Sun, 26 Jul 2015 09:37:20 +1000 Subject: [PATCH 1/3] Volume attachments to show in instance. Volumes and Snapshots to be searchable by their id Placement of instance to match region connection Times for creation and attachment to show based on api call --- moto/ec2/models.py | 25 +++++++++++++++++++++-- moto/ec2/responses/elastic_block_store.py | 22 ++++++++++++++------ moto/ec2/responses/instances.py | 19 +++++++++-------- 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 3f3fc4491..19d005867 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals from collections import defaultdict import copy from datetime import datetime + import itertools import re @@ -97,6 +98,9 @@ from .utils import ( ) +def utc_date_and_time(): + return datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') + def validate_resource_ids(resource_ids): for resource_id in resource_ids: if not is_valid_resource_id(resource_id): @@ -309,14 +313,17 @@ class Instance(BotoInstance, TaggedEC2Resource): in_ec2_classic = not bool(self.subnet_id) self.key_name = kwargs.get("key_name") self.source_dest_check = "true" - self.launch_time = datetime.utcnow().isoformat() + self.launch_time = utc_date_and_time() associate_public_ip = kwargs.get("associate_public_ip", False) if in_ec2_classic: # If we are in EC2-Classic, autoassign a public IP associate_public_ip = True self.block_device_mapping = BlockDeviceMapping() - self.block_device_mapping['/dev/sda1'] = BlockDeviceType(volume_id=random_volume_id()) + # Default have an instance with root volume should you not wish to override with attach volume cmd. + # However this is a ghost volume and wont show up in get_all_volumes or snapshot-able. + self.block_device_mapping['/dev/sda1'] = BlockDeviceType(volume_id=random_volume_id(), status='attached', + attach_time=utc_date_and_time()) amis = self.ec2_backend.describe_images(filters={'image-id': image_id}) ami = amis[0] if amis else None @@ -343,6 +350,10 @@ class Instance(BotoInstance, TaggedEC2Resource): private_ip=kwargs.get("private_ip"), associate_public_ip=associate_public_ip) + @property + def get_block_device_mapping(self): + return self.block_device_mapping.items() + @property def private_ip(self): return self.nics[0].private_ip_address @@ -1349,6 +1360,7 @@ class SecurityGroupIngress(object): class VolumeAttachment(object): def __init__(self, volume, instance, device): self.volume = volume + self.attach_time = utc_date_and_time() self.instance = instance self.device = device @@ -1373,6 +1385,7 @@ class Volume(TaggedEC2Resource): self.id = volume_id self.size = size self.zone = zone + self.create_time = utc_date_and_time() self.attachment = None self.ec2_backend = ec2_backend @@ -1404,6 +1417,7 @@ class Snapshot(TaggedEC2Resource): self.id = snapshot_id self.volume = volume self.description = description + self.start_time = utc_date_and_time() self.create_volume_permission_groups = set() self.ec2_backend = ec2_backend @@ -1444,6 +1458,13 @@ class EBSBackend(object): return False volume.attachment = VolumeAttachment(volume, instance, device_path) + # Modify instance to capture mount of block device. + bdt = BlockDeviceType(volume_id=volume_id, status=volume.status, size=volume.size, + attach_time=utc_date_and_time()) + try: + instance.block_device_mapping[device_path] = bdt + except: + instance.block_device_mapping.setdefault(device_path, bdt) return volume.attachment def detach_volume(self, volume_id, instance_id, device_path): diff --git a/moto/ec2/responses/elastic_block_store.py b/moto/ec2/responses/elastic_block_store.py index 96586a9bb..abb371260 100644 --- a/moto/ec2/responses/elastic_block_store.py +++ b/moto/ec2/responses/elastic_block_store.py @@ -42,12 +42,22 @@ class ElasticBlockStore(BaseResponse): return DELETE_VOLUME_RESPONSE def describe_snapshots(self): + # querystring for multiple snapshotids results in SnapshotId.1, SnapshotId.2 etc + snapshot_ids = ','.join([','.join(s[1]) for s in self.querystring.items() if 'SnapshotId' in s[0]]) snapshots = self.ec2_backend.describe_snapshots() + # Describe snapshots to handle filter on snapshot_ids + snapshots = [s for s in snapshots if s.id in snapshot_ids] if snapshot_ids else snapshots + # snapshots = self.ec2_backend.describe_snapshots() template = self.response_template(DESCRIBE_SNAPSHOTS_RESPONSE) return template.render(snapshots=snapshots) def describe_volumes(self): + # querystring for multiple volumeids results in VolumeId.1, VolumeId.2 etc + volume_ids = ','.join([','.join(v[1]) for v in self.querystring.items() if 'VolumeId' in v[0]]) volumes = self.ec2_backend.describe_volumes() + # Describe volumes to handle filter on volume_ids + volumes = [v for v in volumes if v.id in volume_ids] if volume_ids else volumes + # volumes = self.ec2_backend.describe_volumes() template = self.response_template(DESCRIBE_VOLUMES_RESPONSE) return template.render(volumes=volumes) @@ -103,7 +113,7 @@ CREATE_VOLUME_RESPONSE = """ @@ -174,7 +184,7 @@ CREATE_SNAPSHOT_RESPONSE = """ebs /dev/sda1 + {% for device_name,deviceobject in instance.get_block_device_mapping %} - /dev/sda1 + {{ device_name }} - {{ instance.block_device_mapping['/dev/sda1'].volume_id }} - attached - 2015-01-01T00:00:00.000Z - true + {{ deviceobject.volume_id }} + {{ deviceobject.status }} + {{ deviceobject.attach_time }} + {{ deviceobject.delete_on_termination }} + {{deviceobject.size}} + {% endfor %} {{ instance.virtualization_type }} ABCDE1234567890123 @@ -547,7 +550,7 @@ EC2_INSTANCE_STATUS = """ {% for instance in instances %} {{ instance.id }} - us-east-1d + {{ instance.placement }} {{ instance.state_code }} {{ instance.state }} From e33777b60c27a58d1af19c80ecf06527ee633e16 Mon Sep 17 00:00:00 2001 From: Rob Walker Date: Sun, 26 Jul 2015 09:51:37 +1000 Subject: [PATCH 2/3] tidy up imports. --- moto/ec2/models.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 19d005867..59d936008 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -1,18 +1,17 @@ from __future__ import unicode_literals -from collections import defaultdict +import boto import copy -from datetime import datetime - import itertools import re +import six -import boto +from collections import defaultdict +from datetime import datetime from boto.ec2.instance import Instance as BotoInstance, Reservation from boto.ec2.blockdevicemapping import BlockDeviceMapping, BlockDeviceType from boto.ec2.spotinstancerequest import SpotInstanceRequest as BotoSpotRequest from boto.ec2.launchspecification import LaunchSpecification -import six from moto.core import BaseBackend from moto.core.models import Model From 7b3f17e8f0b53f38ba5b3c8ec0c6ce3537ca289b Mon Sep 17 00:00:00 2001 From: Rob Walker Date: Sun, 26 Jul 2015 09:53:03 +1000 Subject: [PATCH 3/3] add modified tests for volume and snapshot changes. --- tests/test_ec2/test_elastic_block_store.py | 33 ++++++++++++++++++++++ tests/test_ec2/test_instances.py | 31 +++++++++++++++++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/tests/test_ec2/test_elastic_block_store.py b/tests/test_ec2/test_elastic_block_store.py index 50fe6b7ad..5ab5fcdc5 100644 --- a/tests/test_ec2/test_elastic_block_store.py +++ b/tests/test_ec2/test_elastic_block_store.py @@ -33,6 +33,19 @@ def test_create_and_delete_volume(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none +@mock_ec2 +def test_filter_volume_by_id(): + conn = boto.connect_ec2('the_key', 'the_secret') + volume1 = conn.create_volume(80, "us-east-1a") + volume2 = conn.create_volume(36, "us-east-1b") + volume3 = conn.create_volume(20, "us-east-1c") + vol1 = conn.get_all_volumes(volume_ids=volume3.id) + vol1.should.have.length_of(1) + vol1[0].size.should.equal(20) + vol1[0].zone.should.equal('us-east-1c') + vol2 = conn.get_all_volumes(volume_ids=[volume1.id, volume2.id]) + vol2.should.have.length_of(2) + @mock_ec2 def test_volume_attach_and_detach(): @@ -85,6 +98,7 @@ def test_create_snapshot(): snapshots = conn.get_all_snapshots() snapshots.should.have.length_of(1) snapshots[0].description.should.equal('a test snapshot') + snapshots[0].start_time.should_not.be.none # Create snapshot without description snapshot = volume.create_snapshot() @@ -100,6 +114,25 @@ def test_create_snapshot(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none +@mock_ec2 +def test_filter_snapshot_by_id(): + conn = boto.connect_ec2('the_key', 'the_secret') + volume1 = conn.create_volume(36, "us-east-1a") + snap1 = volume1.create_snapshot('a test snapshot 1') + volume2 = conn.create_volume(42, 'us-east-1a') + snap2 = volume2.create_snapshot('a test snapshot 2') + volume3 = conn.create_volume(84, 'us-east-1a') + snap3 = volume3.create_snapshot('a test snapshot 3') + snapshots1 = conn.get_all_snapshots(snapshot_ids=snap2.id) + snapshots1.should.have.length_of(1) + snapshots1[0].volume_id.should.equal(volume2.id) + snapshots1[0].region.name.should.equal('us-east-1') + snapshots2 = conn.get_all_snapshots(snapshot_ids=[snap2.id, snap3.id]) + snapshots2.should.have.length_of(2) + for s in snapshots2: + s.start_time.should_not.be.none + s.volume_id.should.be.within([volume2.id, volume3.id]) + s.region.name.should.equal('us-east-1') @mock_ec2 def test_snapshot_attribute(): diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py index 5ee732d5e..629fc67ee 100644 --- a/tests/test_ec2/test_instances.py +++ b/tests/test_ec2/test_instances.py @@ -53,7 +53,7 @@ def test_instance_launch_and_terminate(): instances.should.have.length_of(1) instances[0].id.should.equal(instance.id) instances[0].state.should.equal('running') - instances[0].launch_time.should.equal("2014-01-01T05:00:00") + instances[0].launch_time.should.equal("2014-01-01T05:00:00Z") instances[0].vpc_id.should.equal(None) root_device_name = instances[0].root_device_name @@ -66,6 +66,35 @@ def test_instance_launch_and_terminate(): instance = reservations[0].instances[0] instance.state.should.equal('terminated') +@freeze_time("2014-01-01 05:00:00") +@mock_ec2 +def test_instance_attach_volume(): + conn = boto.connect_ec2('the_key', 'the_secret') + reservation = conn.run_instances('ami-1234abcd') + instance = reservation.instances[0] + + vol1 = conn.create_volume(size=36, zone=conn.region.name) + vol1.attach(instance.id, "/dev/sda1") + vol1.update() + vol2 = conn.create_volume(size=65, zone=conn.region.name) + vol2.attach(instance.id, "/dev/sdb1") + vol2.update() + vol3 = conn.create_volume(size=130, zone=conn.region.name) + vol3.attach(instance.id, "/dev/sdc1") + vol3.update() + + reservations = conn.get_all_instances() + instance = reservations[0].instances[0] + + instance.block_device_mapping.should.have.length_of(3) + + for v in conn.get_all_volumes(volume_ids=[instance.block_device_mapping['/dev/sdc1'].volume_id]): + v.attach_data.instance_id.should.equal(instance.id) + v.attach_data.attach_time.should.equal(instance.launch_time) # can do due to freeze_time decorator. + v.create_time.should.equal(instance.launch_time) # can do due to freeze_time decorator. + v.region.name.should.equal(instance.region.name) + v.status.should.equal('in-use') + @mock_ec2 def test_get_instances_by_id():