diff --git a/moto/ec2/exceptions.py b/moto/ec2/exceptions.py new file mode 100644 index 000000000..07122d2b2 --- /dev/null +++ b/moto/ec2/exceptions.py @@ -0,0 +1,4 @@ +class InvalidIdError(RuntimeError): + def __init__(self, instance_id): + super(InvalidIdError, self).__init__() + self.instance_id = instance_id diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 978f5cf80..6a48c453e 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -4,6 +4,7 @@ from collections import defaultdict from boto.ec2.instance import Instance as BotoInstance, Reservation from moto.core import BaseBackend +from .exceptions import InvalidIdError from .utils import ( random_ami_id, random_instance_id, @@ -137,6 +138,10 @@ class InstanceBackend(object): reservation_copy = copy.deepcopy(reservation) reservation_copy.instances = [instance for instance in reservation_copy.instances if instance.id in instance_ids] reservations.append(reservation_copy) + found_instance_ids = [instance.id for reservation in reservations for instance in reservation.instances] + if len(found_instance_ids) != len(instance_ids): + invalid_id = list(set(instance_ids).difference(set(found_instance_ids)))[0] + raise InvalidIdError(invalid_id) return reservations def all_reservations(self): diff --git a/moto/ec2/responses/instances.py b/moto/ec2/responses/instances.py index ef7f53fb8..388fb8446 100644 --- a/moto/ec2/responses/instances.py +++ b/moto/ec2/responses/instances.py @@ -2,17 +2,26 @@ from jinja2 import Template from moto.core.utils import camelcase_to_underscores from moto.ec2.models import ec2_backend -from moto.ec2.utils import instance_ids_from_querystring +from moto.ec2.utils import instance_ids_from_querystring, filters_from_querystring, filter_reservations +from moto.ec2.exceptions import InvalidIdError class InstanceResponse(object): def describe_instances(self): instance_ids = instance_ids_from_querystring(self.querystring) - template = Template(EC2_DESCRIBE_INSTANCES) if instance_ids: - reservations = ec2_backend.get_reservations_by_instance_ids(instance_ids) + try: + reservations = ec2_backend.get_reservations_by_instance_ids(instance_ids) + except InvalidIdError as exc: + template = Template(EC2_INVALID_INSTANCE_ID) + return template.render(instance_id=exc.instance_id), dict(status=400) else: reservations = ec2_backend.all_reservations() + + filter_dict = filters_from_querystring(self.querystring) + reservations = filter_reservations(reservations, filter_dict) + + template = Template(EC2_DESCRIBE_INSTANCES) return template.render(reservations=reservations) def run_instances(self): @@ -263,3 +272,10 @@ EC2_MODIFY_INSTANCE_ATTRIBUTE = """ +InvalidInstanceID.NotFound +The instance ID '{{ instance_id }}' does not exist + +39070fe4-6f6d-4565-aecd-7850607e4555""" diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py index 258927f05..841444b8d 100644 --- a/moto/ec2/utils.py +++ b/moto/ec2/utils.py @@ -1,4 +1,5 @@ import random +import re def random_id(prefix=''): @@ -53,7 +54,7 @@ def resource_ids_from_querystring(querystring_dict): prefix = 'ResourceId' response_values = {} for key, value in querystring_dict.iteritems(): - if prefix in key: + if key.startswith(prefix): resource_index = key.replace(prefix + ".", "") tag_key = querystring_dict.get("Tag.{}.Key".format(resource_index))[0] @@ -65,3 +66,42 @@ def resource_ids_from_querystring(querystring_dict): response_values[value[0]] = (tag_key, tag_value) return response_values + + +def filters_from_querystring(querystring_dict): + response_values = {} + for key, value in querystring_dict.iteritems(): + match = re.search("Filter.(\d).Name", key) + if match: + filter_index = match.groups()[0] + value_prefix = "Filter.{}.Value".format(filter_index) + filter_values = [filter_value[0] for filter_key, filter_value in querystring_dict.iteritems() if filter_key.startswith(value_prefix)] + response_values[value[0]] = filter_values + return response_values + + +filter_dict_attribute_mapping = { + 'instance-state-name': 'state' +} + + +def passes_filter_dict(instance, filter_dict): + for filter_name, filter_values in filter_dict.iteritems(): + if filter_name in filter_dict_attribute_mapping: + instance_attr = filter_dict_attribute_mapping[filter_name] + else: + raise NotImplementedError("Filter dicts have not been implemented in Moto for '%s' yet. Feel free to open an issue at https://github.com/spulec/moto/issues", filter_name) + instance_value = getattr(instance, instance_attr) + if instance_value not in filter_values: + return False + return True + + +def filter_reservations(reservations, filter_dict): + for reservation in reservations: + new_instances = [] + for instance in reservation.instances: + if passes_filter_dict(instance, filter_dict): + new_instances.append(instance) + reservation.instances = new_instances + return reservations diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py index 941c2c42a..7176566b5 100644 --- a/tests/test_ec2/test_instances.py +++ b/tests/test_ec2/test_instances.py @@ -2,6 +2,7 @@ import base64 import boto from boto.ec2.instance import Reservation, InstanceAttribute +from boto.exception import EC2ResponseError import sure # flake8: noqa from moto import mock_ec2 @@ -69,6 +70,34 @@ def test_get_instances_by_id(): instance_ids = [instance.id for instance in reservation.instances] instance_ids.should.equal([instance1.id, instance2.id]) + # Call get_all_instances with a bad id should raise an error + conn.get_all_instances.when.called_with(instance_ids=[instance1.id, "i-1234abcd"]).should.throw( + EC2ResponseError, + "The instance ID 'i-1234abcd' does not exist" + ) + + +@mock_ec2 +def test_get_instances_filtering_by_state(): + conn = boto.connect_ec2() + reservation = conn.run_instances('ami-1234abcd', min_count=3) + instance1, instance2, instance3 = reservation.instances + + conn.terminate_instances([instance1.id]) + + reservations = conn.get_all_instances(filters={'instance-state-name': 'pending'}) + reservations.should.have.length_of(1) + # Since we terminated instance1, only instance2 and instance3 should be returned + instance_ids = [instance.id for instance in reservations[0].instances] + set(instance_ids).should.equal(set([instance2.id, instance3.id])) + + reservations = conn.get_all_instances([instance2.id], filters={'instance-state-name': 'pending'}) + reservations.should.have.length_of(1) + instance_ids = [instance.id for instance in reservations[0].instances] + instance_ids.should.equal([instance2.id]) + + conn.get_all_instances.when.called_with(filters={'not-implemented-filter': 'foobar'}).should.throw(NotImplementedError) + @mock_ec2 def test_instance_start_and_stop():